@@ -9,7 +9,7 @@ import { spawn, spawnSync } from 'child_process';
99import { fileURLToPath } from 'url' ;
1010import { dirname , join , delimiter } from 'path' ;
1111import 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' ;
1313import { randomBytes } from 'crypto' ;
1414import { 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 ( / ^ h t t p s ? : \/ \/ / . 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 - z 0 - 9 ] [ a - z 0 - 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}
0 commit comments