Skip to content

Commit 3ad4870

Browse files
feat: jspod install <app> — one-command Solid app installs (#40)
Replaces the multi-step git-push recipe with a single subcommand. Hides the three sharp edges new users hit on the manual path: - Full clone (no --depth=1, which gets rejected by git-receive) - Dual-push (HEAD:main and HEAD:gh-pages) so updateInstead fires on any JSS version regardless of init.defaultBranch - Friendly skip for already-bundled paths like pilot (404 → "skipped") Defaults to solid-apps/<name> as the source (registry override is a follow-up, #25/#24). Authenticates via /idp/credentials with me/me unless overridden via --user / --password / JSS_SINGLE_USER_PASSWORD. jspod install chrome jspod install vellum win98 pdf jspod install # curated set jspod install --pod http://other.pod chrome Fixes #36
1 parent 8e77b5d commit 3ad4870

2 files changed

Lines changed: 167 additions & 2 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,24 @@ That's it. You have a working Solid pod with a passkey-capable identity provider
3030
--multiuser Enable multi-user mode (registration enabled)
3131
--no-auth Open pod, no IDP, no ACL (demos / dev only)
3232
--no-open Don't auto-open the browser on start
33+
--no-git Disable JSS's git HTTP backend
34+
--browser <style> Data browser: folder (default) or json
3335
-v, --version Print jspod version
3436
--help Show help
3537
```
3638

39+
## Install Solid apps
40+
41+
After your pod is running, drop more Solid apps in with one command:
42+
43+
```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
47+
```
48+
49+
Each app lands at `/public/apps/<name>/` and is reachable in the browser immediately. `jspod install --help` for options.
50+
3751
## The auth ladder
3852

3953
jspod ships you onto the lowest rung that's safe, and the climb is visible:

index.js

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Just works, batteries included
66
*/
77

8-
import { spawn } from 'child_process';
8+
import { spawn, spawnSync } from 'child_process';
99
import { fileURLToPath } from 'url';
1010
import { dirname, join, delimiter } from 'path';
1111
import chalk from 'chalk';
@@ -35,6 +35,155 @@ function formatUrl(host, port) {
3535

3636
// Parse CLI arguments
3737
const args = process.argv.slice(2);
38+
39+
// Subcommand dispatch — must run before the flag-parsing loop below, which
40+
// is shaped for the "start the server" command. New subcommands branch off
41+
// here and exit; the start path is reached only when args[0] isn't one.
42+
if (args[0] === 'install') {
43+
await runInstall(args.slice(1));
44+
process.exit(0);
45+
}
46+
47+
async function runInstall(rest) {
48+
const opts = {
49+
pod: 'http://localhost:5444',
50+
user: 'me',
51+
password: process.env.JSS_SINGLE_USER_PASSWORD || 'me',
52+
apps: []
53+
};
54+
for (let i = 0; i < rest.length; i++) {
55+
const a = rest[i];
56+
if (a === '--pod') opts.pod = rest[++i];
57+
else if (a === '--user') opts.user = rest[++i];
58+
else if (a === '--password') opts.password = rest[++i];
59+
else if (a === '--help' || a === '-h') { printInstallHelp(); process.exit(0); }
60+
else if (a.startsWith('--')) {
61+
console.error(chalk.red(`✗ Unknown flag: ${a}`));
62+
printInstallHelp();
63+
process.exit(1);
64+
}
65+
else opts.apps.push(a);
66+
}
67+
if (opts.apps.length === 0) {
68+
opts.apps = ['chrome', 'vellum', 'win98', 'pdf', 'hub'];
69+
}
70+
opts.pod = opts.pod.replace(/\/$/, '');
71+
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));
74+
console.log('');
75+
76+
// Authenticate against the local pod's IDP. Token is needed to push to
77+
// /public/apps/<name>/ on a default jspod (private-write inherits from
78+
// /public/.acl: public-read, owner-write).
79+
let token;
80+
try {
81+
const r = await fetch(`${opts.pod}/idp/credentials`, {
82+
method: 'POST',
83+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
84+
body: new URLSearchParams({ username: opts.user, password: opts.password })
85+
});
86+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
87+
const j = await r.json();
88+
token = j.access_token;
89+
if (!token) throw new Error('no access_token in response');
90+
} catch (e) {
91+
console.error(chalk.red(`✗ Could not authenticate against ${opts.pod}: ${e.message}`));
92+
console.error(chalk.dim(' Is jspod running? → ') + chalk.bold('npx jspod'));
93+
process.exit(1);
94+
}
95+
96+
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`));
100+
continue;
101+
}
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}`);
105+
106+
// Clean stale tmp from a previous failed run
107+
if (existsSync(tmp)) spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
108+
109+
// Full clone (no --depth: shallow pushes are rejected by JSS git-receive)
110+
const clone = spawnSync('git', ['clone', '--quiet', source, tmp], {
111+
stdio: ['ignore', 'pipe', 'pipe']
112+
});
113+
if (clone.status !== 0) {
114+
console.error(chalk.red(`✗ ${app}: clone failed`));
115+
const err = clone.stderr?.toString?.().trim() || '';
116+
if (err) console.error(chalk.dim(` ${err.slice(0, 300)}`));
117+
continue;
118+
}
119+
120+
// Push to the pod. `updateInstead` (which extracts the working tree)
121+
// only fires when the push targets the branch HEAD points at on the
122+
// server. JSS 0.0.197+ auto-inits with HEAD=main; older versions
123+
// honor the operator's `init.defaultBranch` (often `main`, sometimes
124+
// `gh-pages` for GitHub-Pages-heavy users). Push to both — the one
125+
// matching server-side HEAD extracts; the other just creates a ref.
126+
// Idempotent on re-run.
127+
const pushArgs = (branch) => ['-C', tmp, '-c',
128+
`http.extraHeader=Authorization: Bearer ${token}`,
129+
'push', dest, `HEAD:${branch}`];
130+
131+
const pushMain = spawnSync('git', pushArgs('main'),
132+
{ stdio: ['ignore', 'pipe', 'pipe'] });
133+
const errMain = pushMain.stderr?.toString?.() || '';
134+
135+
// If the first push failed for a "won't auto-init" reason (path
136+
// already has content, e.g. jspod's bundled pilot), don't keep going.
137+
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)`));
139+
spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
140+
continue;
141+
}
142+
143+
const pushPages = spawnSync('git', pushArgs('gh-pages'),
144+
{ stdio: ['ignore', 'pipe', 'pipe'] });
145+
146+
if (pushMain.status !== 0 && pushPages.status !== 0) {
147+
console.error(chalk.red(`✗ ${app}: push failed`));
148+
const err = (errMain + '\n' + (pushPages.stderr?.toString?.() || '')).trim();
149+
console.error(chalk.dim(` ${err.slice(0, 400)}`));
150+
spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
151+
continue;
152+
}
153+
154+
console.log(chalk.green(`✓ ${app}`) + chalk.dim(` → ${dest}/`));
155+
okCount++;
156+
spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
157+
}
158+
159+
console.log('');
160+
console.log(chalk.bold(`${okCount}/${opts.apps.length} installed.`));
161+
if (okCount > 0) {
162+
console.log(chalk.dim('Open in browser: ') + chalk.cyan(`${opts.pod}/public/apps/`));
163+
}
164+
}
165+
166+
function printInstallHelp() {
167+
console.log(chalk.cyan(`
168+
╔═══════════════════════════════════════════════════════════════════╗
169+
║ jspod install - Help ║
170+
╚═══════════════════════════════════════════════════════════════════╝
171+
`));
172+
console.log(chalk.white('Usage:'));
173+
console.log(chalk.yellow(' jspod install') + chalk.dim(' [options] [<app>...]\n'));
174+
console.log(chalk.white('Options:'));
175+
console.log(chalk.green(' --pod ') + chalk.yellow('<url>') + chalk.dim(' Target pod (default: http://localhost:5444)'));
176+
console.log(chalk.green(' --user ') + chalk.yellow('<name>') + chalk.dim(' Username (default: me)'));
177+
console.log(chalk.green(' --password ') + chalk.yellow('<pw>') + chalk.dim(' Password (default: $JSS_SINGLE_USER_PASSWORD or "me")'));
178+
console.log(chalk.green(' --help') + chalk.dim(' Show this help message\n'));
179+
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'));
183+
console.log(chalk.dim(' jspod install --pod http://192.168.0.1:5444 chrome'));
184+
console.log('');
185+
}
186+
38187
const options = {
39188
port: 5444,
40189
host: 'localhost',
@@ -141,7 +290,9 @@ for (let i = 0; i < args.length; i++) {
141290
╚═══════════════════════════════════════════════════════════════════╝
142291
`));
143292
console.log(chalk.white('Usage:'));
144-
console.log(chalk.yellow(' jspod') + chalk.dim(' [options]\n'));
293+
console.log(chalk.yellow(' jspod') + chalk.dim(' [options]') + chalk.dim(' Start the pod (default)'));
294+
console.log(chalk.yellow(' jspod install') + chalk.dim(' [<app>...]') + chalk.dim(' Install Solid apps from solid-apps/<name>'));
295+
console.log(chalk.dim(' (see `jspod install --help`)\n'));
145296
console.log(chalk.white('Options:'));
146297
console.log(chalk.green(' -p, --port ') + chalk.yellow('<number>') + chalk.dim(' Port to listen on (default: 5444)'));
147298
console.log(chalk.green(' -h, --host ') + chalk.yellow('<address>') + chalk.dim(' Host to bind to (default: localhost)'));

0 commit comments

Comments
 (0)