Skip to content

Commit 0a3cbf0

Browse files
Merge pull request #78 from JavaScriptSolidServer/issue-48-delegate-install
Delegate jspod install to upstream jss install; bump jss to ^0.0.207
2 parents 90172f7 + 6faf168 commit 0a3cbf0

2 files changed

Lines changed: 38 additions & 256 deletions

File tree

index.js

Lines changed: 37 additions & 255 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
* library shouldn't touch.
88
*/
99

10-
import { spawn, spawnSync } from 'child_process';
10+
import { spawn } from 'child_process';
1111
import { fileURLToPath } from 'url';
12-
import { dirname, join } from 'path';
12+
import { dirname, join, delimiter } from 'path';
1313
import chalk from 'chalk';
14-
import { existsSync, readFileSync, promises as fsPromises } from 'fs';
15-
import { tmpdir } from 'os';
14+
import { readFileSync } from 'fs';
1615
import { start, formatUrl } from './lib/start.js';
1716

1817
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -29,265 +28,48 @@ if (args[0] === 'install') {
2928
process.exit(0);
3029
}
3130

31+
// `jspod install` is a thin wrapper over upstream `jss install` — the
32+
// plumbing (spec parsing, bundles, IdP auth, git dual-push) moved into
33+
// JSS itself in 0.0.198 (JSS#464; see jspod#48). jspod keeps two policies:
34+
// 1. --pod defaults to jspod's port (5444; jss alone defaults to 4443)
35+
// 2. bare `jspod install` → the `default` bundle from solid-apps/bundles
36+
// `jss install --help` is the source of truth for flags and spec forms.
3237
async function runInstall(rest) {
33-
const opts = {
34-
pod: 'http://localhost:5444',
35-
user: 'me',
36-
password: process.env.JSS_SINGLE_USER_PASSWORD || 'me',
37-
apps: [],
38-
bundles: []
39-
};
38+
const jssArgs = ['install', ...rest];
39+
if (!rest.includes('--pod')) jssArgs.push('--pod', 'http://localhost:5444');
40+
41+
// Detect a "bare" invocation (no app names, no --bundle) while stepping
42+
// over value-taking flags, so `jspod install --user bob` still gets the
43+
// default bundle but `jspod install chrome` does not.
44+
const valueFlags = new Set(['--pod', '--user', '--password', '--bundle', '--nostr-privkey']);
45+
let bare = true;
4046
for (let i = 0; i < rest.length; i++) {
4147
const a = rest[i];
42-
if (a === '--pod') opts.pod = rest[++i];
43-
else if (a === '--user') opts.user = rest[++i];
44-
else if (a === '--password') opts.password = rest[++i];
45-
else if (a === '--bundle') opts.bundles.push(rest[++i]);
46-
else if (a === '--help' || a === '-h') { printInstallHelp(); process.exit(0); }
47-
else if (a.startsWith('--')) {
48-
console.error(chalk.red(`✗ Unknown flag: ${a}`));
49-
printInstallHelp();
50-
process.exit(1);
51-
}
52-
else opts.apps.push(a);
48+
if (a === '--bundle') { bare = false; i++; }
49+
else if (valueFlags.has(a)) i++;
50+
else if (!a.startsWith('-')) bare = false;
5351
}
54-
55-
// Bare `jspod install` (no apps, no bundles) → install the `default`
56-
// bundle. The bundle definition lives at solid-apps/bundles, so updates
57-
// ship without a jspod release. Power users skip this by naming apps
58-
// or passing --bundle explicitly.
59-
if (opts.apps.length === 0 && opts.bundles.length === 0) {
60-
opts.bundles.push('default');
52+
if (bare && !rest.includes('--help') && !rest.includes('-h')) {
53+
jssArgs.push('--bundle', 'default');
6154
}
6255

63-
// Expand any --bundle sources into the apps[] list.
64-
for (const source of opts.bundles) {
65-
let bundleSpecs;
66-
try {
67-
bundleSpecs = await loadBundle(source);
68-
} catch (e) {
69-
console.error(chalk.red(`✗ Couldn't load bundle "${source}": ${e.message}`));
70-
process.exit(1);
56+
// Same PATH-prepend trick as lib/start.js so the locally-installed jss
57+
// binary wins regardless of how jspod itself was installed.
58+
const child = spawn('jss', jssArgs, {
59+
stdio: 'inherit',
60+
env: {
61+
...process.env,
62+
PATH: join(__dirname, 'node_modules', '.bin') + delimiter + process.env.PATH
7163
}
72-
console.log(chalk.dim(`bundle "${source}" → ${bundleSpecs.length} apps: ${bundleSpecs.join(', ')}`));
73-
opts.apps.push(...bundleSpecs);
74-
}
75-
opts.pod = opts.pod.replace(/\/$/, '');
76-
77-
console.log(chalk.bold.white(`\nInstalling ${opts.apps.length} app${opts.apps.length === 1 ? '' : 's'} → `) +
78-
chalk.green(opts.pod));
79-
console.log('');
80-
81-
// Authenticate against the local pod's IDP. Token is needed to push to
82-
// /public/apps/<name>/ on a default jspod (private-write inherits from
83-
// /public/.acl: public-read, owner-write).
84-
let token;
85-
try {
86-
const r = await fetch(`${opts.pod}/idp/credentials`, {
87-
method: 'POST',
88-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
89-
body: new URLSearchParams({ username: opts.user, password: opts.password })
90-
});
91-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
92-
const j = await r.json();
93-
token = j.access_token;
94-
if (!token) throw new Error('no access_token in response');
95-
} catch (e) {
96-
console.error(chalk.red(`✗ Could not authenticate against ${opts.pod}: ${e.message}`));
97-
console.error(chalk.dim(' Is jspod running? → ') + chalk.bold('npx jspod'));
98-
process.exit(1);
99-
}
100-
101-
let okCount = 0;
102-
for (const input of opts.apps) {
103-
const spec = parseAppSpec(input);
104-
if (spec.error) {
105-
console.error(chalk.red(`✗ ${input}: ${spec.error}`));
106-
continue;
107-
}
108-
const { source, name, ref } = spec;
109-
const dest = `${opts.pod}/public/apps/${name}`;
110-
// Respect the platform's tmp dir — `/tmp` is hardcoded out on
111-
// Termux (Android), where the writable tmp is at $PREFIX/tmp.
112-
const tmp = join(tmpdir(), `jspod-install-${name}-${process.pid}`);
113-
114-
// Clean stale tmp from a previous failed run
115-
if (existsSync(tmp)) spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
116-
117-
// Full clone (no --depth: shallow pushes are rejected by JSS git-receive).
118-
// --branch picks a tag or branch when pinned (e.g. `foo/bar#v2`).
119-
const cloneArgs = ['clone', '--quiet'];
120-
if (ref) cloneArgs.push('--branch', ref);
121-
cloneArgs.push(source, tmp);
122-
const clone = spawnSync('git', cloneArgs, {
123-
stdio: ['ignore', 'pipe', 'pipe']
64+
});
65+
const code = await new Promise((resolve) => {
66+
child.once('error', (e) => {
67+
console.error(chalk.red(`✗ Could not run jss: ${e.message}`));
68+
resolve(1);
12469
});
125-
if (clone.status !== 0) {
126-
console.error(chalk.red(`✗ ${input}: clone failed`));
127-
const err = clone.stderr?.toString?.().trim() || '';
128-
if (err) console.error(chalk.dim(` ${err.slice(0, 300)}`));
129-
continue;
130-
}
131-
132-
// Push to the pod. `updateInstead` (which extracts the working tree)
133-
// only fires when the push targets the branch HEAD points at on the
134-
// server. JSS 0.0.197+ auto-inits with HEAD=main; older versions
135-
// honor the operator's `init.defaultBranch` (often `main`, sometimes
136-
// `gh-pages` for GitHub-Pages-heavy users). Push to both — the one
137-
// matching server-side HEAD extracts; the other just creates a ref.
138-
// Idempotent on re-run.
139-
const pushArgs = (branch) => ['-C', tmp, '-c',
140-
`http.extraHeader=Authorization: Bearer ${token}`,
141-
'push', dest, `HEAD:${branch}`];
142-
143-
const pushMain = spawnSync('git', pushArgs('main'),
144-
{ stdio: ['ignore', 'pipe', 'pipe'] });
145-
const errMain = pushMain.stderr?.toString?.() || '';
146-
147-
// If the first push failed for a "won't auto-init" reason (path
148-
// already has content, e.g. jspod's bundled pilot), don't keep going.
149-
if (pushMain.status !== 0 && (errMain.includes('not found') || errMain.includes('404'))) {
150-
console.log(chalk.yellow(`⊘ ${input}: skipped (path already in use — bundled or manually placed)`));
151-
spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
152-
continue;
153-
}
154-
155-
const pushPages = spawnSync('git', pushArgs('gh-pages'),
156-
{ stdio: ['ignore', 'pipe', 'pipe'] });
157-
158-
if (pushMain.status !== 0 && pushPages.status !== 0) {
159-
console.error(chalk.red(`✗ ${input}: push failed`));
160-
const err = (errMain + '\n' + (pushPages.stderr?.toString?.() || '')).trim();
161-
console.error(chalk.dim(` ${err.slice(0, 400)}`));
162-
spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
163-
continue;
164-
}
165-
166-
console.log(chalk.green(`✓ ${input}`) + chalk.dim(` → ${dest}/`));
167-
okCount++;
168-
spawnSync('rm', ['-rf', tmp], { stdio: 'ignore' });
169-
}
170-
171-
console.log('');
172-
console.log(chalk.bold(`${okCount}/${opts.apps.length} installed.`));
173-
if (okCount > 0) {
174-
console.log(chalk.dim('Open in browser: ') + chalk.cyan(`${opts.pod}/public/apps/`));
175-
}
176-
}
177-
178-
// Resolve a --bundle <source> argument to a fetchable URL or local file path.
179-
function resolveBundleSource(source) {
180-
if (!source) throw new Error('bundle source required');
181-
if (/^https?:\/\//.test(source)) return { kind: 'url', loc: source };
182-
if (source.startsWith('./') || source.startsWith('/') || source.endsWith('.jsonld')) {
183-
if (source.startsWith('./') || source.startsWith('/')) {
184-
return { kind: 'file', loc: source };
185-
}
186-
}
187-
if (source.includes('/')) {
188-
const cleaned = source.replace(/^\/+|\/+$/g, '');
189-
if (cleaned.split('/').length !== 2) {
190-
throw new Error('expected <name>, <org>/<repo>, URL, or filesystem path');
191-
}
192-
return { kind: 'url', loc: `https://raw.githubusercontent.com/${cleaned}/HEAD/bundle.jsonld` };
193-
}
194-
if (!/^[a-z0-9][a-z0-9_.-]*$/i.test(source)) {
195-
throw new Error(`invalid bundle name "${source}"`);
196-
}
197-
return { kind: 'url', loc: `https://raw.githubusercontent.com/solid-apps/bundles/HEAD/${source}.jsonld` };
198-
}
199-
200-
async function loadBundle(source) {
201-
const { kind, loc } = resolveBundleSource(source);
202-
let text;
203-
if (kind === 'file') {
204-
text = await fsPromises.readFile(loc, 'utf8');
205-
} else {
206-
const r = await fetch(loc);
207-
if (!r.ok) throw new Error(`HTTP ${r.status} fetching ${loc}`);
208-
text = await r.text();
209-
}
210-
let doc;
211-
try { doc = JSON.parse(text); }
212-
catch (e) { throw new Error(`bundle is not valid JSON: ${e.message}`); }
213-
const items = doc['schema:itemListElement'] || doc.itemListElement || [];
214-
if (!Array.isArray(items)) throw new Error('bundle has no schema:itemListElement array');
215-
return items.map(item => {
216-
if (typeof item === 'string') return item;
217-
if (item && typeof item === 'object') return item['app:spec'] || item.spec || null;
218-
return null;
219-
}).filter(Boolean);
220-
}
221-
222-
function parseAppSpec(input) {
223-
let base = input;
224-
let renameName = null;
225-
const eqIx = base.lastIndexOf('=');
226-
if (eqIx > 0) {
227-
renameName = base.slice(eqIx + 1);
228-
base = base.slice(0, eqIx);
229-
}
230-
let ref = null;
231-
const hashIx = base.lastIndexOf('#');
232-
if (hashIx > 0) {
233-
ref = base.slice(hashIx + 1) || null;
234-
base = base.slice(0, hashIx);
235-
}
236-
let source, name;
237-
if (/^https?:\/\//.test(base)) {
238-
source = base.replace(/\.git$/, '').replace(/\/$/, '');
239-
name = source.split('/').pop();
240-
} else if (base.includes('/')) {
241-
const cleaned = base.replace(/\.git$/, '').replace(/^\/+|\/+$/g, '');
242-
if (cleaned.split('/').length !== 2) {
243-
return { error: 'expected <org>/<repo> shorthand' };
244-
}
245-
source = `https://github.com/${cleaned}`;
246-
name = cleaned.split('/').pop();
247-
} else {
248-
source = `https://github.com/solid-apps/${base}`;
249-
name = base;
250-
}
251-
if (renameName) name = renameName;
252-
if (!/^[a-z0-9][a-z0-9_.-]*$/i.test(name)) {
253-
return { error: `invalid pod-path name "${name}"` };
254-
}
255-
if (ref && !/^[a-z0-9][a-z0-9_./-]*$/i.test(ref)) {
256-
return { error: `invalid ref "${ref}"` };
257-
}
258-
return { source, name, ref };
259-
}
260-
261-
function printInstallHelp() {
262-
console.log(chalk.cyan(`
263-
╔═══════════════════════════════════════════════════════════════════╗
264-
║ jspod install - Help ║
265-
╚═══════════════════════════════════════════════════════════════════╝
266-
`));
267-
console.log(chalk.white('Usage:'));
268-
console.log(chalk.yellow(' jspod install') + chalk.dim(' [options] [<app>...]\n'));
269-
console.log(chalk.white('Options:'));
270-
console.log(chalk.green(' --pod ') + chalk.yellow('<url>') + chalk.dim(' Target pod (default: http://localhost:5444)'));
271-
console.log(chalk.green(' --user ') + chalk.yellow('<name>') + chalk.dim(' Username (default: me)'));
272-
console.log(chalk.green(' --password ') + chalk.yellow('<pw>') + chalk.dim(' Password (default: $JSS_SINGLE_USER_PASSWORD or "me")'));
273-
console.log(chalk.green(' --bundle ') + chalk.yellow('<src>') + chalk.dim(' Install every app in a bundle. Source: <name> (e.g. starter,'));
274-
console.log(chalk.dim(' agentic, all), <org>/<repo>, https://… URL, or ./local.jsonld'));
275-
console.log(chalk.green(' --help') + chalk.dim(' Show this help message\n'));
276-
console.log(chalk.white('App spec:') + chalk.dim(' <name> | <org>/<repo> | https://github.com/<org>/<repo>'));
277-
console.log(chalk.dim(' Optional suffixes: #<branch-or-tag> =<pod-path-name>'));
278-
console.log('');
279-
console.log(chalk.white('Examples:'));
280-
console.log(chalk.dim(' jspod install chrome # solid-apps/chrome'));
281-
console.log(chalk.dim(' jspod install chrome vellum pdf # several at once'));
282-
console.log(chalk.dim(' jspod install # default bundle (home, plaza, vellum, plume, …)'));
283-
console.log(chalk.dim(' jspod install JavaScriptSolidServer/git # any GitHub org/repo'));
284-
console.log(chalk.dim(' jspod install litecut/litecut.github.io=litecut # rename pod path'));
285-
console.log(chalk.dim(' jspod install solid-apps/chrome#v1 # pin a tag or branch'));
286-
console.log(chalk.dim(' jspod install --bundle starter # curated starter set'));
287-
console.log(chalk.dim(' jspod install --bundle agentic # agent stack'));
288-
console.log(chalk.dim(' jspod install --bundle all # every solid-app'));
289-
console.log(chalk.dim(' jspod install --pod http://192.168.0.1:5444 chrome'));
290-
console.log('');
70+
child.once('exit', (c) => resolve(c ?? 1));
71+
});
72+
process.exit(code);
29173
}
29274

29375
const options = {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@
6060
},
6161
"dependencies": {
6262
"chalk": "^5.6.2",
63-
"javascript-solid-server": "^0.0.204"
63+
"javascript-solid-server": "^0.0.207"
6464
}
6565
}

0 commit comments

Comments
 (0)