@@ -69,8 +69,8 @@ async function runInstall(rest) {
6969 }
7070 opts . pod = opts . pod . replace ( / \/ $ / , '' ) ;
7171
72- console . log ( chalk . bold . white ( `\nInstalling ${ opts . apps . length } app${ opts . apps . length === 1 ? '' : 's' } from ` ) +
73- chalk . cyan ( 'solid-apps' ) + chalk . bold . white ( ' → ' ) + chalk . green ( opts . pod ) ) ;
72+ console . log ( chalk . bold . white ( `\nInstalling ${ opts . apps . length } app${ opts . apps . length === 1 ? '' : 's' } → ` ) +
73+ chalk . green ( opts . pod ) ) ;
7474 console . log ( '' ) ;
7575
7676 // Authenticate against the local pod's IDP. Token is needed to push to
@@ -94,24 +94,29 @@ async function runInstall(rest) {
9494 }
9595
9696 let okCount = 0 ;
97- for ( const app of opts . apps ) {
98- if ( ! / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 _ . - ] * $ / i. test ( app ) ) {
99- console . error ( chalk . red ( `✗ ${ app } : invalid app name` ) ) ;
97+ for ( const input of opts . apps ) {
98+ const spec = parseAppSpec ( input ) ;
99+ if ( spec . error ) {
100+ console . error ( chalk . red ( `✗ ${ input } : ${ spec . error } ` ) ) ;
100101 continue ;
101102 }
102- const source = `https://github.com/solid-apps/ ${ app } ` ;
103- const dest = `${ opts . pod } /public/apps/${ app } ` ;
104- const tmp = join ( '/tmp' , `jspod-install-${ app } -${ process . pid } ` ) ;
103+ const { source, name , ref } = spec ;
104+ const dest = `${ opts . pod } /public/apps/${ name } ` ;
105+ const tmp = join ( '/tmp' , `jspod-install-${ name } -${ process . pid } ` ) ;
105106
106107 // Clean stale tmp from a previous failed run
107108 if ( existsSync ( tmp ) ) spawnSync ( 'rm' , [ '-rf' , tmp ] , { stdio : 'ignore' } ) ;
108109
109- // Full clone (no --depth: shallow pushes are rejected by JSS git-receive)
110- const clone = spawnSync ( 'git' , [ 'clone' , '--quiet' , source , tmp ] , {
110+ // Full clone (no --depth: shallow pushes are rejected by JSS git-receive).
111+ // --branch picks a tag or branch when pinned (e.g. `foo/bar#v2`).
112+ const cloneArgs = [ 'clone' , '--quiet' ] ;
113+ if ( ref ) cloneArgs . push ( '--branch' , ref ) ;
114+ cloneArgs . push ( source , tmp ) ;
115+ const clone = spawnSync ( 'git' , cloneArgs , {
111116 stdio : [ 'ignore' , 'pipe' , 'pipe' ]
112117 } ) ;
113118 if ( clone . status !== 0 ) {
114- console . error ( chalk . red ( `✗ ${ app } : clone failed` ) ) ;
119+ console . error ( chalk . red ( `✗ ${ input } : clone failed` ) ) ;
115120 const err = clone . stderr ?. toString ?. ( ) . trim ( ) || '' ;
116121 if ( err ) console . error ( chalk . dim ( ` ${ err . slice ( 0 , 300 ) } ` ) ) ;
117122 continue ;
@@ -135,7 +140,7 @@ async function runInstall(rest) {
135140 // If the first push failed for a "won't auto-init" reason (path
136141 // already has content, e.g. jspod's bundled pilot), don't keep going.
137142 if ( pushMain . status !== 0 && ( errMain . includes ( 'not found' ) || errMain . includes ( '404' ) ) ) {
138- console . log ( chalk . yellow ( `⊘ ${ app } : skipped (path already in use — bundled or manually placed)` ) ) ;
143+ console . log ( chalk . yellow ( `⊘ ${ input } : skipped (path already in use — bundled or manually placed)` ) ) ;
139144 spawnSync ( 'rm' , [ '-rf' , tmp ] , { stdio : 'ignore' } ) ;
140145 continue ;
141146 }
@@ -144,14 +149,14 @@ async function runInstall(rest) {
144149 { stdio : [ 'ignore' , 'pipe' , 'pipe' ] } ) ;
145150
146151 if ( pushMain . status !== 0 && pushPages . status !== 0 ) {
147- console . error ( chalk . red ( `✗ ${ app } : push failed` ) ) ;
152+ console . error ( chalk . red ( `✗ ${ input } : push failed` ) ) ;
148153 const err = ( errMain + '\n' + ( pushPages . stderr ?. toString ?. ( ) || '' ) ) . trim ( ) ;
149154 console . error ( chalk . dim ( ` ${ err . slice ( 0 , 400 ) } ` ) ) ;
150155 spawnSync ( 'rm' , [ '-rf' , tmp ] , { stdio : 'ignore' } ) ;
151156 continue ;
152157 }
153158
154- console . log ( chalk . green ( `✓ ${ app } ` ) + chalk . dim ( ` → ${ dest } /` ) ) ;
159+ console . log ( chalk . green ( `✓ ${ input } ` ) + chalk . dim ( ` → ${ dest } /` ) ) ;
155160 okCount ++ ;
156161 spawnSync ( 'rm' , [ '-rf' , tmp ] , { stdio : 'ignore' } ) ;
157162 }
@@ -163,6 +168,55 @@ async function runInstall(rest) {
163168 }
164169}
165170
171+ // Parse the app spec string the user passed to `jspod install`. Accepts:
172+ // - bare name → github.com/solid-apps/<name> (default registry)
173+ // - "<org>/<repo>" → github.com/<org>/<repo>
174+ // - "https://..." full URL → as-is (must point at a git repo)
175+ // Each form may carry an optional "#<ref>" suffix to pin a tag or branch:
176+ // chrome#v1.2 / solid-apps/chrome#main / https://...#v2
177+ // And an optional "=<name>" suffix to override the pod-path name, useful
178+ // when the repo's last segment isn't what you want under /public/apps/
179+ // (e.g. "litecut/litecut.github.io=litecut").
180+ function parseAppSpec ( input ) {
181+ // Pull off the rename suffix first, then the ref suffix.
182+ let base = input ;
183+ let renameName = null ;
184+ const eqIx = base . lastIndexOf ( '=' ) ;
185+ if ( eqIx > 0 ) {
186+ renameName = base . slice ( eqIx + 1 ) ;
187+ base = base . slice ( 0 , eqIx ) ;
188+ }
189+ let ref = null ;
190+ const hashIx = base . lastIndexOf ( '#' ) ;
191+ if ( hashIx > 0 ) {
192+ ref = base . slice ( hashIx + 1 ) || null ;
193+ base = base . slice ( 0 , hashIx ) ;
194+ }
195+ let source , name ;
196+ if ( / ^ h t t p s ? : \/ \/ / . test ( base ) ) {
197+ source = base . replace ( / \. g i t $ / , '' ) . replace ( / \/ $ / , '' ) ;
198+ name = source . split ( '/' ) . pop ( ) ;
199+ } else if ( base . includes ( '/' ) ) {
200+ const cleaned = base . replace ( / \. g i t $ / , '' ) . replace ( / ^ \/ + | \/ + $ / g, '' ) ;
201+ if ( cleaned . split ( '/' ) . length !== 2 ) {
202+ return { error : 'expected <org>/<repo> shorthand' } ;
203+ }
204+ source = `https://github.com/${ cleaned } ` ;
205+ name = cleaned . split ( '/' ) . pop ( ) ;
206+ } else {
207+ source = `https://github.com/solid-apps/${ base } ` ;
208+ name = base ;
209+ }
210+ if ( renameName ) name = renameName ;
211+ if ( ! / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 _ . - ] * $ / i. test ( name ) ) {
212+ return { error : `invalid pod-path name "${ name } "` } ;
213+ }
214+ if ( ref && ! / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 _ . / - ] * $ / i. test ( ref ) ) {
215+ return { error : `invalid ref "${ ref } "` } ;
216+ }
217+ return { source, name, ref } ;
218+ }
219+
166220function printInstallHelp ( ) {
167221 console . log ( chalk . cyan ( `
168222╔═══════════════════════════════════════════════════════════════════╗
@@ -176,10 +230,16 @@ function printInstallHelp() {
176230 console . log ( chalk . green ( ' --user ' ) + chalk . yellow ( '<name>' ) + chalk . dim ( ' Username (default: me)' ) ) ;
177231 console . log ( chalk . green ( ' --password ' ) + chalk . yellow ( '<pw>' ) + chalk . dim ( ' Password (default: $JSS_SINGLE_USER_PASSWORD or "me")' ) ) ;
178232 console . log ( chalk . green ( ' --help' ) + chalk . dim ( ' Show this help message\n' ) ) ;
233+ console . log ( chalk . white ( 'App spec:' ) + chalk . dim ( ' <name> | <org>/<repo> | https://github.com/<org>/<repo>' ) ) ;
234+ console . log ( chalk . dim ( ' Optional suffixes: #<branch-or-tag> =<pod-path-name>' ) ) ;
235+ console . log ( '' ) ;
179236 console . log ( chalk . white ( 'Examples:' ) ) ;
180- console . log ( chalk . dim ( ' jspod install chrome # install solid-apps/chrome' ) ) ;
181- console . log ( chalk . dim ( ' jspod install chrome vellum pdf # several apps' ) ) ;
182- console . log ( chalk . dim ( ' jspod install # curated set: chrome vellum win98 pdf hub' ) ) ;
237+ console . log ( chalk . dim ( ' jspod install chrome # solid-apps/chrome' ) ) ;
238+ console . log ( chalk . dim ( ' jspod install chrome vellum pdf # several at once' ) ) ;
239+ console . log ( chalk . dim ( ' jspod install # curated set: chrome vellum win98 pdf hub' ) ) ;
240+ console . log ( chalk . dim ( ' jspod install JavaScriptSolidServer/git # any GitHub org/repo' ) ) ;
241+ console . log ( chalk . dim ( ' jspod install litecut/litecut.github.io=litecut # rename pod path' ) ) ;
242+ console . log ( chalk . dim ( ' jspod install solid-apps/chrome#v1 # pin a tag or branch' ) ) ;
183243 console . log ( chalk . dim ( ' jspod install --pod http://192.168.0.1:5444 chrome' ) ) ;
184244 console . log ( '' ) ;
185245}
0 commit comments