Skip to content

Commit 3b1fb36

Browse files
feat: auto-install default bundle on first run + --no-bootstrap (#55)
Closes #54. Three things land here, all gated by the existing `/public/apps/` state probe (no marker file, self-correcting): 1. **Bare `jspod install` delegates to `--bundle default`** — the hardcoded `chrome/vellum/win98/pdf/hub` list is removed; the default install set now lives at solid-apps/bundles/default.jsonld and updates without a jspod release. 2. **First-run bootstrap on `jspod`** — after the pod is ready and the bundled pilot is copied in, if `/public/apps/` didn't exist before this start (genuine fresh pod), spawn a child `jspod install --bundle default` so the welcome page is populated, not near-empty. Spawned as a child rather than calling runInstall() directly because runInstall has process.exit() paths that would kill the running pod on sub-errors. 3. **`--no-bootstrap` flag** — opts out of step 2 for power users who want a pristine pod with no opinions. The bootstrap is skipped when `--no-auth` is set (no IDP → no token mint). Pairs with solid-apps/bundles#1 (the default.jsonld bundle).
1 parent 15426d8 commit 3b1fb36

2 files changed

Lines changed: 58 additions & 9 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ That's it. You have a working Solid pod with a passkey-capable identity provider
4040

4141
## Install Solid apps
4242

43-
After your pod is running, drop more Solid apps in with one command:
43+
On the **first run**, jspod auto-installs the [`default`](https://github.com/solid-apps/bundles/blob/gh-pages/default.jsonld) bundle (home, plaza, vellum, plume, chat, timeline, charlie, chrome, explorer). Opt out with `--no-bootstrap`. On subsequent runs jspod just starts the pod — the bootstrap only triggers when `/public/apps/` was empty before this start.
44+
45+
Drop more apps in any time with one command:
4446

4547
```bash
4648
jspod install chrome # solid-apps/chrome (default registry)
4749
jspod install vellum win98 pdf # several at once
48-
jspod install # curated set: chrome vellum win98 pdf hub
50+
jspod install # default bundle (home, plaza, vellum, plume, …)
4951
jspod install JavaScriptSolidServer/git # any GitHub org/repo
5052
jspod install litecut/litecut.github.io=litecut # rename the pod path
5153
jspod install solid-apps/chrome#v1 # pin a branch or tag

index.js

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ async function runInstall(rest) {
6868
else opts.apps.push(a);
6969
}
7070

71+
// Bare `jspod install` (no apps, no bundles) → install the `default`
72+
// bundle. The bundle definition lives at solid-apps/bundles, so updates
73+
// ship without a jspod release. Power users skip this by naming apps
74+
// or passing --bundle explicitly.
75+
if (opts.apps.length === 0 && opts.bundles.length === 0) {
76+
opts.bundles.push('default');
77+
}
78+
7179
// Expand any --bundle sources into the apps[] list.
7280
for (const source of opts.bundles) {
7381
let bundleSpecs;
@@ -80,10 +88,6 @@ async function runInstall(rest) {
8088
console.log(chalk.dim(`bundle "${source}" → ${bundleSpecs.length} apps: ${bundleSpecs.join(', ')}`));
8189
opts.apps.push(...bundleSpecs);
8290
}
83-
84-
if (opts.apps.length === 0) {
85-
opts.apps = ['chrome', 'vellum', 'win98', 'pdf', 'hub'];
86-
}
8791
opts.pod = opts.pod.replace(/\/$/, '');
8892

8993
console.log(chalk.bold.white(`\nInstalling ${opts.apps.length} app${opts.apps.length === 1 ? '' : 's'} → `) +
@@ -310,7 +314,7 @@ function printInstallHelp() {
310314
console.log(chalk.white('Examples:'));
311315
console.log(chalk.dim(' jspod install chrome # solid-apps/chrome'));
312316
console.log(chalk.dim(' jspod install chrome vellum pdf # several at once'));
313-
console.log(chalk.dim(' jspod install # curated set: chrome vellum win98 pdf hub'));
317+
console.log(chalk.dim(' jspod install # default bundle (home, plaza, vellum, plume, …)'));
314318
console.log(chalk.dim(' jspod install JavaScriptSolidServer/git # any GitHub org/repo'));
315319
console.log(chalk.dim(' jspod install litecut/litecut.github.io=litecut # rename pod path'));
316320
console.log(chalk.dim(' jspod install solid-apps/chrome#v1 # pin a tag or branch'));
@@ -338,7 +342,11 @@ const options = {
338342
// keypair on first start, stores it at <pod>/private/privkey.jsonld,
339343
// and publishes the pubkey in the WebID profile). The nosdav-server
340344
// wrapper will flip this on by default.
341-
provisionKeys: false
345+
provisionKeys: false,
346+
// Auto-install the `default` bundle on the first run (when
347+
// /public/apps/ has nothing in it beyond the bundled pilot).
348+
// --no-bootstrap opts out. See #54.
349+
bootstrap: true
342350
};
343351

344352
// Auth-ladder rung-1 credentials. See issue #6: jspod ships a deliberately
@@ -423,6 +431,8 @@ for (let i = 0; i < args.length; i++) {
423431
options.mcp = true;
424432
} else if (arg === '--no-mcp') {
425433
options.mcp = false;
434+
} else if (arg === '--no-bootstrap') {
435+
options.bootstrap = false;
426436
} else if (arg === '--browser') {
427437
const raw = requireValue(arg, args[++i]);
428438
if (raw !== 'json' && raw !== 'folder') {
@@ -455,6 +465,7 @@ for (let i = 0; i < args.length; i++) {
455465
console.log(chalk.green(' --browser ') + chalk.yellow('<folder|json>') + chalk.dim(' Data browser style (default: folder)'));
456466
console.log(chalk.green(' --provision-keys') + chalk.dim(' Generate a Nostr-compatible owner keypair on first start'));
457467
console.log(chalk.green(' --mcp') + chalk.dim(' Expose /mcp (Model Context Protocol) tool surface for agents'));
468+
console.log(chalk.green(' --no-bootstrap') + chalk.dim(' Skip auto-install of the `default` app bundle on first run'));
458469
console.log(chalk.green(' -v, --version') + chalk.dim(' Show jspod version'));
459470
console.log(chalk.green(' --help') + chalk.dim(' Show this help message\n'));
460471
console.log(chalk.white('Examples:'));
@@ -852,9 +863,45 @@ ready.then((ok) => {
852863
// separate ACLs are needed for these subdirectories.
853864
const appsSrc = join(__dirname, 'apps');
854865
const appsDst = join(options.root, 'public', 'apps');
855-
if (existsSync(appsSrc) && !existsSync(appsDst)) {
866+
// Capture whether the apps dir already existed *before* we copy
867+
// pilot in. If it didn't, this is a genuine first run and we
868+
// bootstrap the `default` bundle below.
869+
const appsDirExisted = existsSync(appsDst);
870+
if (existsSync(appsSrc) && !appsDirExisted) {
856871
cpSync(appsSrc, appsDst, { recursive: true });
857872
}
873+
874+
// First-run bootstrap (#54): on a fresh pod, install the `default`
875+
// bundle so the welcome page is populated rather than near-empty.
876+
// Opt out with --no-bootstrap. Skipped on no-auth pods (the install
877+
// path needs the IDP to mint a bearer token).
878+
//
879+
// Spawn as a child process rather than calling runInstall() directly:
880+
// runInstall has several process.exit() calls that would kill the
881+
// running pod on any sub-error. The child runs concurrently with the
882+
// pod and bubbles its own status without affecting the parent.
883+
if (!appsDirExisted && options.bootstrap && options.auth) {
884+
console.log(chalk.bold.white(`\n📦 First run — installing the `) +
885+
chalk.yellow('default') +
886+
chalk.bold.white(` bundle (skip next time with `) +
887+
chalk.cyan('--no-bootstrap') +
888+
chalk.bold.white(')...\n'));
889+
const podUrl = browserUrl.replace(/\/$/, '');
890+
const child = spawn(
891+
process.execPath,
892+
[process.argv[1], 'install', '--pod', podUrl, '--bundle', 'default'],
893+
{ stdio: 'inherit' }
894+
);
895+
child.on('exit', (code) => {
896+
if (code !== 0) {
897+
console.error(chalk.red(`\n✗ Bootstrap exited with code ${code}.`));
898+
console.error(chalk.dim(' Install apps manually with `jspod install`.'));
899+
}
900+
});
901+
child.on('error', (e) => {
902+
console.error(chalk.red(`\n✗ Bootstrap failed to start: ${e.message}`));
903+
});
904+
}
858905
} catch {
859906
// best-effort: failures are silent; user falls back to whatever
860907
// JSS already wrote (or nothing for links.jsonld).

0 commit comments

Comments
 (0)