Skip to content

Commit 4ff91dd

Browse files
feat(install): support arbitrary GitHub repos, refs, and renames (#42)
jspod install previously hardcoded github.com/solid-apps/<name>. Accept three input shapes now: jspod install chrome # solid-apps/<name> jspod install <org>/<repo> # any GitHub repo jspod install https://github.com/<org>/<repo> # full URL Optional suffixes apply to any form: ...#<branch-or-tag> pin a ref (uses `git clone --branch <ref>`) ...=<pod-path-name> rename the pod path (e.g. for repos whose name doesn't fit a clean /public/apps/<x>/ slot, like litecut/litecut.github.io=litecut) The pod-path name is derived from the repo's last segment when not overridden. Banner no longer hardcodes "solid-apps" since sources vary. Help and README updated with examples. Fixes #25.
1 parent 5e7c1d9 commit 4ff91dd

2 files changed

Lines changed: 84 additions & 21 deletions

File tree

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,15 @@ That's it. You have a working Solid pod with a passkey-capable identity provider
4141
After your pod is running, drop more Solid apps in with one command:
4242

4343
```bash
44-
jspod install chrome # install solid-apps/chrome
45-
jspod install vellum win98 pdf # several at once
46-
jspod install # curated set: chrome vellum win98 pdf hub
44+
jspod install chrome # solid-apps/chrome (default registry)
45+
jspod install vellum win98 pdf # several at once
46+
jspod install # curated set: chrome vellum win98 pdf hub
47+
jspod install JavaScriptSolidServer/git # any GitHub org/repo
48+
jspod install litecut/litecut.github.io=litecut # rename the pod path
49+
jspod install solid-apps/chrome#v1 # pin a branch or tag
4750
```
4851

49-
Each app lands at `/public/apps/<name>/` and is reachable in the browser immediately. `jspod install --help` for options.
52+
Each app lands at `/public/apps/<name>/` and is reachable in the browser immediately. `jspod install --help` for the full spec.
5053

5154
## The auth ladder
5255

index.js

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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-z0-9][a-z0-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 (/^https?:\/\//.test(base)) {
197+
source = base.replace(/\.git$/, '').replace(/\/$/, '');
198+
name = source.split('/').pop();
199+
} else if (base.includes('/')) {
200+
const cleaned = base.replace(/\.git$/, '').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-z0-9][a-z0-9_.-]*$/i.test(name)) {
212+
return { error: `invalid pod-path name "${name}"` };
213+
}
214+
if (ref && !/^[a-z0-9][a-z0-9_./-]*$/i.test(ref)) {
215+
return { error: `invalid ref "${ref}"` };
216+
}
217+
return { source, name, ref };
218+
}
219+
166220
function 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

Comments
 (0)