Skip to content

Commit 6be5da4

Browse files
feat: --bundle support in jspod install + 0.0.40 (#53)
jspod's install subcommand has its own arg parser and didn't recognise --bundle. Adds resolveBundleSource + loadBundle, mirroring JSS's bundle resolver (bare name → solid-apps/bundles/HEAD/<name>.jsonld; org/repo → HEAD/bundle.jsonld; URL or filesystem path as-is). Expanded bundle items are appended to the apps[] list and run through the existing install loop. So now: jspod install --bundle starter jspod install --bundle agentic jspod install --bundle all jspod install --bundle solid-apps/bundles jspod install --bundle https://my.pod/bundles/dev.jsonld jspod install --bundle ./local-bundle.jsonld all work, matching the README that landed in 03c462b.
1 parent 03c462b commit 6be5da4

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

index.js

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { spawn, spawnSync } from 'child_process';
99
import { fileURLToPath } from 'url';
1010
import { dirname, join, delimiter } from 'path';
1111
import chalk from 'chalk';
12-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync, copyFileSync, cpSync } from 'fs';
12+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync, copyFileSync, cpSync, promises as fsPromises } from 'fs';
1313
import { randomBytes } from 'crypto';
1414
import { createServer } from 'net';
1515

@@ -49,13 +49,15 @@ async function runInstall(rest) {
4949
pod: 'http://localhost:5444',
5050
user: 'me',
5151
password: process.env.JSS_SINGLE_USER_PASSWORD || 'me',
52-
apps: []
52+
apps: [],
53+
bundles: []
5354
};
5455
for (let i = 0; i < rest.length; i++) {
5556
const a = rest[i];
5657
if (a === '--pod') opts.pod = rest[++i];
5758
else if (a === '--user') opts.user = rest[++i];
5859
else if (a === '--password') opts.password = rest[++i];
60+
else if (a === '--bundle') opts.bundles.push(rest[++i]);
5961
else if (a === '--help' || a === '-h') { printInstallHelp(); process.exit(0); }
6062
else if (a.startsWith('--')) {
6163
console.error(chalk.red(`✗ Unknown flag: ${a}`));
@@ -64,6 +66,20 @@ async function runInstall(rest) {
6466
}
6567
else opts.apps.push(a);
6668
}
69+
70+
// Expand any --bundle sources into the apps[] list.
71+
for (const source of opts.bundles) {
72+
let bundleSpecs;
73+
try {
74+
bundleSpecs = await loadBundle(source);
75+
} catch (e) {
76+
console.error(chalk.red(`✗ Couldn't load bundle "${source}": ${e.message}`));
77+
process.exit(1);
78+
}
79+
console.log(chalk.dim(`bundle "${source}" → ${bundleSpecs.length} apps: ${bundleSpecs.join(', ')}`));
80+
opts.apps.push(...bundleSpecs);
81+
}
82+
6783
if (opts.apps.length === 0) {
6884
opts.apps = ['chrome', 'vellum', 'win98', 'pdf', 'hub'];
6985
}
@@ -168,6 +184,59 @@ async function runInstall(rest) {
168184
}
169185
}
170186

187+
// Resolve a --bundle <source> argument to a fetchable URL or local file path.
188+
// Mirrors the JSS resolver:
189+
// <name> → https://raw.githubusercontent.com/solid-apps/bundles/HEAD/<name>.jsonld
190+
// <org>/<repo> → https://raw.githubusercontent.com/<org>/<repo>/HEAD/bundle.jsonld
191+
// https://... → fetch as-is
192+
// ./path or /abs → read from filesystem
193+
function resolveBundleSource(source) {
194+
if (!source) throw new Error('bundle source required');
195+
if (/^https?:\/\//.test(source)) return { kind: 'url', loc: source };
196+
if (source.startsWith('./') || source.startsWith('/') || source.endsWith('.jsonld')) {
197+
// Local filesystem path
198+
if (source.startsWith('./') || source.startsWith('/')) {
199+
return { kind: 'file', loc: source };
200+
}
201+
}
202+
if (source.includes('/')) {
203+
// org/repo form
204+
const cleaned = source.replace(/^\/+|\/+$/g, '');
205+
if (cleaned.split('/').length !== 2) {
206+
throw new Error('expected <name>, <org>/<repo>, URL, or filesystem path');
207+
}
208+
return { kind: 'url', loc: `https://raw.githubusercontent.com/${cleaned}/HEAD/bundle.jsonld` };
209+
}
210+
// Bare name → solid-apps/bundles
211+
if (!/^[a-z0-9][a-z0-9_.-]*$/i.test(source)) {
212+
throw new Error(`invalid bundle name "${source}"`);
213+
}
214+
return { kind: 'url', loc: `https://raw.githubusercontent.com/solid-apps/bundles/HEAD/${source}.jsonld` };
215+
}
216+
217+
// Fetch a bundle and return the list of app specs (strings).
218+
async function loadBundle(source) {
219+
const { kind, loc } = resolveBundleSource(source);
220+
let text;
221+
if (kind === 'file') {
222+
text = await fsPromises.readFile(loc, 'utf8');
223+
} else {
224+
const r = await fetch(loc);
225+
if (!r.ok) throw new Error(`HTTP ${r.status} fetching ${loc}`);
226+
text = await r.text();
227+
}
228+
let doc;
229+
try { doc = JSON.parse(text); }
230+
catch (e) { throw new Error(`bundle is not valid JSON: ${e.message}`); }
231+
const items = doc['schema:itemListElement'] || doc.itemListElement || [];
232+
if (!Array.isArray(items)) throw new Error('bundle has no schema:itemListElement array');
233+
return items.map(item => {
234+
if (typeof item === 'string') return item;
235+
if (item && typeof item === 'object') return item['app:spec'] || item.spec || null;
236+
return null;
237+
}).filter(Boolean);
238+
}
239+
171240
// Parse the app spec string the user passed to `jspod install`. Accepts:
172241
// - bare name → github.com/solid-apps/<name> (default registry)
173242
// - "<org>/<repo>" → github.com/<org>/<repo>
@@ -229,6 +298,8 @@ function printInstallHelp() {
229298
console.log(chalk.green(' --pod ') + chalk.yellow('<url>') + chalk.dim(' Target pod (default: http://localhost:5444)'));
230299
console.log(chalk.green(' --user ') + chalk.yellow('<name>') + chalk.dim(' Username (default: me)'));
231300
console.log(chalk.green(' --password ') + chalk.yellow('<pw>') + chalk.dim(' Password (default: $JSS_SINGLE_USER_PASSWORD or "me")'));
301+
console.log(chalk.green(' --bundle ') + chalk.yellow('<src>') + chalk.dim(' Install every app in a bundle. Source: <name> (e.g. starter,'));
302+
console.log(chalk.dim(' agentic, all), <org>/<repo>, https://… URL, or ./local.jsonld'));
232303
console.log(chalk.green(' --help') + chalk.dim(' Show this help message\n'));
233304
console.log(chalk.white('App spec:') + chalk.dim(' <name> | <org>/<repo> | https://github.com/<org>/<repo>'));
234305
console.log(chalk.dim(' Optional suffixes: #<branch-or-tag> =<pod-path-name>'));
@@ -240,6 +311,9 @@ function printInstallHelp() {
240311
console.log(chalk.dim(' jspod install JavaScriptSolidServer/git # any GitHub org/repo'));
241312
console.log(chalk.dim(' jspod install litecut/litecut.github.io=litecut # rename pod path'));
242313
console.log(chalk.dim(' jspod install solid-apps/chrome#v1 # pin a tag or branch'));
314+
console.log(chalk.dim(' jspod install --bundle starter # curated starter set'));
315+
console.log(chalk.dim(' jspod install --bundle agentic # agent stack'));
316+
console.log(chalk.dim(' jspod install --bundle all # every solid-app'));
243317
console.log(chalk.dim(' jspod install --pod http://192.168.0.1:5444 chrome'));
244318
console.log('');
245319
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jspod",
3-
"version": "0.0.39",
3+
"version": "0.0.40",
44
"description": "JavaScript Solid Pod - Just works, batteries included",
55
"type": "module",
66
"main": "index.js",

0 commit comments

Comments
 (0)