Phase 1 of the phased install plan (#464 comment). The meaningful core: a single working jss install <name> against the default registry. Once it lands, phases 2–6 (full spec, did:nostr resolution, NIP-98 signed install, curated set, bundles) are mechanical extensions.
Scope
jss install <name> resolves to https://github.com/solid-apps/<name>, clones, and pushes the working tree to <pod>/public/apps/<name>/. Re-runs are idempotent. Each app reports a per-line status.
Acceptance criteria
$ jss start & # in another terminal / background
$ jss install chrome
Installing 1 app → http://localhost:4443
✓ chrome → http://localhost:4443/public/apps/chrome/
1/1 installed.
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4443/public/apps/chrome/index.html
200
$ jss install chrome # idempotent
✓ chrome → http://localhost:4443/public/apps/chrome/ # or "⊘ skipped" — both fine
Flags
| Flag |
Default |
Purpose |
--pod <url> |
http://localhost:4443 (post-#477) |
Target pod URL |
--user <name> |
me |
Username for token fetch |
--password <pw> |
$JSS_SINGLE_USER_PASSWORD or me |
Password (env wins over flag default) |
--help / -h |
— |
Subcommand help |
Behavior
- Auth.
POST <pod>/idp/credentials with username=<user>&password=<password>, extract access_token. If pod is in --public mode (no IDP), skip token fetch.
- Clone.
git clone https://github.com/solid-apps/<name> /tmp/jss-install-<name>-<pid>. No --depth=1 — shallow clones are rejected by git-receive on push.
- Push. Dual push to handle both
init.defaultBranch conventions:
git -C /tmp/... -c http.extraHeader="Authorization: Bearer <token>" push <pod>/public/apps/<name> HEAD:main
git -C /tmp/... -c http.extraHeader="Authorization: Bearer <token>" push <pod>/public/apps/<name> HEAD:gh-pages
Whichever matches server-side HEAD triggers updateInstead and extracts the working tree. The other just creates a stranded ref (harmless).
- Skip-on-existing. If the auto-init's path-empty check refuses (e.g. JSS bundles content there), translate the
repository not found / 404 from the push into a friendly ⊘ <name>: skipped (path already in use — bundled or manually placed).
- Cleanup.
rm -rf /tmp/jss-install-<name>-<pid> always, even on failure.
- Output. Per-app
✓ <name> → <pod>/public/apps/<name>/, ⊘ <name>: skipped, or ✗ <name>: <reason>. Trailing N/M installed. summary.
- Exit code. 0 if all installs succeeded or skipped; non-zero if any failed.
Validation
<name> must match /^[a-z0-9][a-z0-9_.-]*$/i. Reject .., slashes (Phase 2 introduces them with a different shape), shell metacharacters.
Implementation notes
- Reference implementation: jspod's
runInstall in bin/index.js — ~120 LoC. Port shape is direct; just drop the jspod-specific banner integration and the bundled-pilot heuristic (JSS doesn't bundle apps).
- Use
spawnSync for git operations (already used in src/handlers/git.js's auto-init path).
- New file:
src/cli/install.js exporting an async function the CLI dispatches to.
- CLI dispatch: extend
bin/jss.js's subcommand handling.
- No new runtime dependencies needed.
Test plan
Depends on
- #477 — port default fallback (
--pod default should be 4443, not 3000)
Not in scope (later phases)
<org>/<repo> shorthand (Phase 2)
- Full URLs,
#<ref> pinning, =<name> rename (Phase 2)
--did resolution (Phase 3)
--nostr-privkey NIP-98 auth (Phase 4)
- No-arg curated set (Phase 5)
--bundle (Phase 6)
Parent
#464 (phased plan in the comment thread)
Phase 1 of the phased install plan (#464 comment). The meaningful core: a single working
jss install <name>against the default registry. Once it lands, phases 2–6 (full spec, did:nostr resolution, NIP-98 signed install, curated set, bundles) are mechanical extensions.Scope
jss install <name>resolves tohttps://github.com/solid-apps/<name>, clones, and pushes the working tree to<pod>/public/apps/<name>/. Re-runs are idempotent. Each app reports a per-line status.Acceptance criteria
Flags
--pod <url>http://localhost:4443(post-#477)--user <name>me--password <pw>$JSS_SINGLE_USER_PASSWORDorme--help/-hBehavior
POST <pod>/idp/credentialswithusername=<user>&password=<password>, extractaccess_token. If pod is in--publicmode (no IDP), skip token fetch.git clone https://github.com/solid-apps/<name> /tmp/jss-install-<name>-<pid>. No--depth=1— shallow clones are rejected by git-receive on push.init.defaultBranchconventions:updateInsteadand extracts the working tree. The other just creates a stranded ref (harmless).repository not found/ 404 from the push into a friendly⊘ <name>: skipped (path already in use — bundled or manually placed).rm -rf /tmp/jss-install-<name>-<pid>always, even on failure.✓ <name> → <pod>/public/apps/<name>/,⊘ <name>: skipped, or✗ <name>: <reason>. TrailingN/M installed.summary.Validation
<name>must match/^[a-z0-9][a-z0-9_.-]*$/i. Reject.., slashes (Phase 2 introduces them with a different shape), shell metacharacters.Implementation notes
runInstallin bin/index.js — ~120 LoC. Port shape is direct; just drop the jspod-specific banner integration and the bundled-pilot heuristic (JSS doesn't bundle apps).spawnSyncfor git operations (already used insrc/handlers/git.js's auto-init path).src/cli/install.jsexporting an async function the CLI dispatches to.bin/jss.js's subcommand handling.Test plan
jss install chromeagainst a fresh pod → working tree extracts,index.htmlreturns 200jss install nonexistent-name-that-doesnt-resolve→✗ <name>: clone failed, exit non-zerojss install chrome --pod http://other:5444against a remote pod with the same auth flow--passwordover env:JSS_SINGLE_USER_PASSWORD=foo jss install chromeuses the env valuejss install chrome --user wrong --password wrong→✗ Could not authenticate against <pod>, exit non-zerojss install --helpshows the subcommand helpDepends on
--poddefault should be4443, not3000)Not in scope (later phases)
<org>/<repo>shorthand (Phase 2)#<ref>pinning,=<name>rename (Phase 2)--didresolution (Phase 3)--nostr-privkeyNIP-98 auth (Phase 4)--bundle(Phase 6)Parent
#464 (phased plan in the comment thread)