@openparachute/hub 0.3.0-rc.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -17
- package/package.json +15 -4
- package/src/__tests__/admin-auth.test.ts +197 -0
- package/src/__tests__/admin-config.test.ts +281 -0
- package/src/__tests__/admin-grants.test.ts +271 -0
- package/src/__tests__/admin-handlers.test.ts +530 -0
- package/src/__tests__/admin-host-admin-token.test.ts +115 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
- package/src/__tests__/admin-vaults.test.ts +615 -0
- package/src/__tests__/auth-codes.test.ts +253 -0
- package/src/__tests__/auth.test.ts +1063 -17
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/clients.test.ts +264 -0
- package/src/__tests__/cloudflare-state.test.ts +167 -7
- package/src/__tests__/csrf.test.ts +117 -0
- package/src/__tests__/expose-cloudflare.test.ts +232 -37
- package/src/__tests__/expose-off-auto.test.ts +15 -9
- package/src/__tests__/expose-public-auto.test.ts +153 -0
- package/src/__tests__/expose.test.ts +216 -24
- package/src/__tests__/grants.test.ts +164 -0
- package/src/__tests__/hub-db.test.ts +153 -0
- package/src/__tests__/hub-server.test.ts +984 -26
- package/src/__tests__/hub.test.ts +56 -49
- package/src/__tests__/install.test.ts +327 -3
- package/src/__tests__/jwks.test.ts +37 -0
- package/src/__tests__/jwt-sign.test.ts +361 -0
- package/src/__tests__/lifecycle.test.ts +616 -5
- package/src/__tests__/module-manifest.test.ts +183 -0
- package/src/__tests__/oauth-handlers.test.ts +3112 -0
- package/src/__tests__/oauth-ui.test.ts +253 -0
- package/src/__tests__/operator-token.test.ts +140 -0
- package/src/__tests__/providers-detect.test.ts +158 -0
- package/src/__tests__/scope-explanations.test.ts +108 -0
- package/src/__tests__/scope-registry.test.ts +220 -0
- package/src/__tests__/services-manifest.test.ts +137 -1
- package/src/__tests__/sessions.test.ts +116 -0
- package/src/__tests__/setup.test.ts +361 -0
- package/src/__tests__/signing-keys.test.ts +153 -0
- package/src/__tests__/upgrade.test.ts +541 -0
- package/src/__tests__/users.test.ts +154 -0
- package/src/__tests__/well-known.test.ts +127 -10
- package/src/admin-auth.ts +126 -0
- package/src/admin-config-ui.ts +534 -0
- package/src/admin-config.ts +226 -0
- package/src/admin-grants.ts +160 -0
- package/src/admin-handlers.ts +365 -0
- package/src/admin-host-admin-token.ts +83 -0
- package/src/admin-vault-admin-token.ts +98 -0
- package/src/admin-vaults.ts +359 -0
- package/src/auth-codes.ts +189 -0
- package/src/cli.ts +202 -25
- package/src/clients.ts +210 -0
- package/src/cloudflare/config.ts +25 -6
- package/src/cloudflare/state.ts +108 -28
- package/src/commands/auth.ts +851 -19
- package/src/commands/expose-cloudflare.ts +85 -45
- package/src/commands/expose-interactive.ts +20 -44
- package/src/commands/expose-off-auto.ts +27 -11
- package/src/commands/expose-public-auto.ts +179 -0
- package/src/commands/expose.ts +63 -32
- package/src/commands/install.ts +337 -48
- package/src/commands/lifecycle.ts +269 -38
- package/src/commands/setup.ts +366 -0
- package/src/commands/status.ts +4 -1
- package/src/commands/upgrade.ts +429 -0
- package/src/csrf.ts +101 -0
- package/src/grants.ts +142 -0
- package/src/help.ts +133 -19
- package/src/hub-control.ts +12 -0
- package/src/hub-db.ts +164 -0
- package/src/hub-server.ts +643 -22
- package/src/hub.ts +97 -390
- package/src/jwks.ts +41 -0
- package/src/jwt-audience.ts +40 -0
- package/src/jwt-sign.ts +275 -0
- package/src/module-manifest.ts +435 -0
- package/src/oauth-handlers.ts +1175 -0
- package/src/oauth-ui.ts +582 -0
- package/src/operator-token.ts +129 -0
- package/src/providers/detect.ts +97 -0
- package/src/scope-explanations.ts +137 -0
- package/src/scope-registry.ts +158 -0
- package/src/service-spec.ts +270 -97
- package/src/services-manifest.ts +57 -1
- package/src/sessions.ts +115 -0
- package/src/signing-keys.ts +120 -0
- package/src/users.ts +144 -0
- package/src/well-known.ts +62 -26
- package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
- package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
- package/web/ui/dist/index.html +14 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "../config.ts";
|
|
3
|
+
import {
|
|
4
|
+
SCRIBE_PROVIDERS,
|
|
5
|
+
type ScribeProviderKey,
|
|
6
|
+
apiKeyEnvFor,
|
|
7
|
+
isKnownScribeProvider,
|
|
8
|
+
} from "../scribe-config.ts";
|
|
9
|
+
import {
|
|
10
|
+
FIRST_PARTY_FALLBACKS,
|
|
11
|
+
type ServiceSpec,
|
|
12
|
+
composeServiceSpec,
|
|
13
|
+
knownServices,
|
|
14
|
+
} from "../service-spec.ts";
|
|
15
|
+
import { findService } from "../services-manifest.ts";
|
|
16
|
+
import { type InstallOpts, install } from "./install.ts";
|
|
17
|
+
import type { InteractiveAvailability } from "./scribe-provider-interactive.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `parachute setup` — unified, prompt-up-front walk-through that orchestrates
|
|
21
|
+
* the existing per-service install flows behind a single command.
|
|
22
|
+
*
|
|
23
|
+
* Shape (closes #45):
|
|
24
|
+
* 1. Detect what's already in services.json.
|
|
25
|
+
* 2. Multi-select prompt: which uninstalled services to install (default
|
|
26
|
+
* all). Already-installed services aren't offered — re-running setup is
|
|
27
|
+
* idempotent.
|
|
28
|
+
* 3. Per-service follow-ups asked **before** any install runs, so the
|
|
29
|
+
* operator answers everything in one sitting instead of stopping mid-
|
|
30
|
+
* stream:
|
|
31
|
+
* - vault → vault name (default: "default")
|
|
32
|
+
* - scribe → transcription provider + (cloud-only) API key
|
|
33
|
+
* - notes → nothing extra
|
|
34
|
+
* 4. Iterate `install(short, opts)` per pick with the pre-collected
|
|
35
|
+
* answers threaded through. Reuses every existing seam: bun add, init,
|
|
36
|
+
* port assignment, services.json seed, auto-wire, footer.
|
|
37
|
+
* 5. Final summary banner with running URLs + a "try Claude Code" hint.
|
|
38
|
+
*
|
|
39
|
+
* Errors in one service don't roll back earlier ones — partial setup beats
|
|
40
|
+
* losing already-working installs. The caller sees a non-zero exit code if
|
|
41
|
+
* any step failed; the rest of the work still landed.
|
|
42
|
+
*
|
|
43
|
+
* Existing `parachute install <svc>` keeps working — setup is additive.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export interface SetupOpts {
|
|
47
|
+
manifestPath?: string;
|
|
48
|
+
configDir?: string;
|
|
49
|
+
log?: (line: string) => void;
|
|
50
|
+
/** Test seam: drives every prompt. Defaults to a real readline against the TTY. */
|
|
51
|
+
availability?: InteractiveAvailability;
|
|
52
|
+
/** Test seam: replaces the per-service `install()` call. */
|
|
53
|
+
installFn?: (input: string, installOpts: InstallOpts) => Promise<number>;
|
|
54
|
+
/** Test seam: extra opts merged into every `install()` call (runner, port probe, …). */
|
|
55
|
+
baseInstallOpts?: Partial<InstallOpts>;
|
|
56
|
+
/** Forwarded to install(): npm dist-tag for `bun add -g <pkg>@<tag>`. */
|
|
57
|
+
tag?: string;
|
|
58
|
+
/** Forwarded to install(): skip auto-start. */
|
|
59
|
+
noStart?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ServiceChoice {
|
|
63
|
+
short: string;
|
|
64
|
+
installed: boolean;
|
|
65
|
+
manifestName: string;
|
|
66
|
+
spec: ServiceSpec;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface VaultAnswer {
|
|
70
|
+
vaultName: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ScribeAnswer {
|
|
74
|
+
provider: ScribeProviderKey;
|
|
75
|
+
apiKey: string | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reject leading and trailing hyphens. The previous form `[a-z0-9][a-z0-9-]*`
|
|
79
|
+
// permitted `my-vault-` which round-trips poorly through path segments and
|
|
80
|
+
// some shells. Single-char names (`a`, `7`) stay legal.
|
|
81
|
+
const VAULT_NAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
82
|
+
|
|
83
|
+
function defaultAvailability(): InteractiveAvailability {
|
|
84
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return { kind: "not-tty" };
|
|
85
|
+
return {
|
|
86
|
+
kind: "available",
|
|
87
|
+
prompt: async (question: string) => {
|
|
88
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
+
try {
|
|
90
|
+
return await rl.question(question);
|
|
91
|
+
} finally {
|
|
92
|
+
rl.close();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Survey the eligible services. We include the four first-party shortnames
|
|
100
|
+
* (vault / notes / scribe / channel) but flag channel as exploratory in the
|
|
101
|
+
* blurb so operators don't grab it by reflex. `installed` is true when the
|
|
102
|
+
* service has a row in services.json.
|
|
103
|
+
*/
|
|
104
|
+
function surveyServices(manifestPath: string): ServiceChoice[] {
|
|
105
|
+
return knownServices().map((short) => {
|
|
106
|
+
const fb = FIRST_PARTY_FALLBACKS[short];
|
|
107
|
+
if (!fb) throw new Error(`setup: unexpected first-party shortname ${short}`);
|
|
108
|
+
const spec = composeServiceSpec({
|
|
109
|
+
packageName: fb.package,
|
|
110
|
+
manifest: fb.manifest,
|
|
111
|
+
extras: fb.extras,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
short,
|
|
115
|
+
manifestName: spec.manifestName,
|
|
116
|
+
spec,
|
|
117
|
+
installed: !!findService(spec.manifestName, manifestPath),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const BLURBS: Record<string, string> = {
|
|
123
|
+
vault: "knowledge graph (MCP) — your owner-authenticated note + tag store",
|
|
124
|
+
notes: "Notes PWA — web/mobile UI on top of vault",
|
|
125
|
+
scribe: "audio transcription for dictation + recordings",
|
|
126
|
+
channel: "(exploratory — may retire) notification fan-out across modules",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function blurbFor(choice: ServiceChoice): string {
|
|
130
|
+
return BLURBS[choice.short] ?? choice.spec.manifestName;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse the user's pick string into a list of indices into `offered`.
|
|
135
|
+
* Accepts:
|
|
136
|
+
* - empty / whitespace → every offered service (the "Enter for all" path)
|
|
137
|
+
* - "all" → every offered service
|
|
138
|
+
* - "1,3" or "1 3" or "1, 3" → those specific 1-based indices
|
|
139
|
+
* - "vault,scribe" → those specific shortnames (matched against `offered`)
|
|
140
|
+
*
|
|
141
|
+
* Unknown tokens raise an error string the caller surfaces — no silent
|
|
142
|
+
* dropouts.
|
|
143
|
+
*/
|
|
144
|
+
export function parseServicePicks(
|
|
145
|
+
raw: string,
|
|
146
|
+
offered: ServiceChoice[],
|
|
147
|
+
): { picks: ServiceChoice[] } | { error: string } {
|
|
148
|
+
const trimmed = raw.trim();
|
|
149
|
+
if (trimmed.length === 0 || trimmed.toLowerCase() === "all") {
|
|
150
|
+
return { picks: [...offered] };
|
|
151
|
+
}
|
|
152
|
+
const tokens = trimmed
|
|
153
|
+
.split(/[\s,]+/)
|
|
154
|
+
.map((t) => t.trim())
|
|
155
|
+
.filter((t) => t.length > 0);
|
|
156
|
+
const picks: ServiceChoice[] = [];
|
|
157
|
+
const seen = new Set<string>();
|
|
158
|
+
for (const tok of tokens) {
|
|
159
|
+
let match: ServiceChoice | undefined;
|
|
160
|
+
if (/^\d+$/.test(tok)) {
|
|
161
|
+
const idx = Number.parseInt(tok, 10) - 1;
|
|
162
|
+
if (idx < 0 || idx >= offered.length) {
|
|
163
|
+
return { error: `out-of-range index "${tok}" (offered 1..${offered.length})` };
|
|
164
|
+
}
|
|
165
|
+
match = offered[idx];
|
|
166
|
+
} else {
|
|
167
|
+
match = offered.find((c) => c.short === tok.toLowerCase());
|
|
168
|
+
if (!match) return { error: `unknown service "${tok}"` };
|
|
169
|
+
}
|
|
170
|
+
if (match && !seen.has(match.short)) {
|
|
171
|
+
picks.push(match);
|
|
172
|
+
seen.add(match.short);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { picks };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function askVaultName(
|
|
179
|
+
prompt: (q: string) => Promise<string>,
|
|
180
|
+
log: (line: string) => void,
|
|
181
|
+
): Promise<VaultAnswer> {
|
|
182
|
+
for (;;) {
|
|
183
|
+
const raw = (await prompt("vault — name (default: default): ")).trim();
|
|
184
|
+
const candidate = raw.length === 0 ? "default" : raw;
|
|
185
|
+
if (VAULT_NAME_RE.test(candidate)) return { vaultName: candidate };
|
|
186
|
+
log(
|
|
187
|
+
` invalid name "${candidate}" — must start with [a-z0-9] and contain only [a-z0-9-]. Try again.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function askScribeProvider(
|
|
193
|
+
prompt: (q: string) => Promise<string>,
|
|
194
|
+
log: (line: string) => void,
|
|
195
|
+
): Promise<ScribeAnswer> {
|
|
196
|
+
log("");
|
|
197
|
+
log("scribe — transcription provider:");
|
|
198
|
+
for (let i = 0; i < SCRIBE_PROVIDERS.length; i++) {
|
|
199
|
+
const p = SCRIBE_PROVIDERS[i];
|
|
200
|
+
if (!p) continue;
|
|
201
|
+
log(` [${i + 1}] ${p.label} — ${p.blurb}`);
|
|
202
|
+
}
|
|
203
|
+
let provider: ScribeProviderKey | undefined;
|
|
204
|
+
while (!provider) {
|
|
205
|
+
const raw = (await prompt("Pick a provider (Enter for parakeet-mlx): ")).trim();
|
|
206
|
+
if (raw.length === 0) {
|
|
207
|
+
provider = "parakeet-mlx";
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (/^\d+$/.test(raw)) {
|
|
211
|
+
const idx = Number.parseInt(raw, 10) - 1;
|
|
212
|
+
const hit = SCRIBE_PROVIDERS[idx];
|
|
213
|
+
if (hit) {
|
|
214
|
+
provider = hit.key;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
log(` out of range — pick 1..${SCRIBE_PROVIDERS.length}`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (isKnownScribeProvider(raw)) {
|
|
221
|
+
provider = raw;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
log(` unknown provider "${raw}" — try a number from the list above`);
|
|
225
|
+
}
|
|
226
|
+
const apiKeyEnv = apiKeyEnvFor(provider);
|
|
227
|
+
let apiKey: string | undefined;
|
|
228
|
+
if (apiKeyEnv) {
|
|
229
|
+
const raw = (await prompt(`scribe — ${apiKeyEnv} (or Enter to skip): `)).trim();
|
|
230
|
+
if (raw.length > 0) apiKey = raw;
|
|
231
|
+
}
|
|
232
|
+
return { provider, apiKey };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function summarizeUrls(
|
|
236
|
+
manifestPath: string,
|
|
237
|
+
picks: ServiceChoice[],
|
|
238
|
+
log: (line: string) => void,
|
|
239
|
+
): void {
|
|
240
|
+
log("");
|
|
241
|
+
log("Setup complete.");
|
|
242
|
+
log("");
|
|
243
|
+
for (const choice of picks) {
|
|
244
|
+
const entry = findService(choice.manifestName, manifestPath);
|
|
245
|
+
if (!entry) {
|
|
246
|
+
log(` ⚠ ${choice.manifestName} not in services.json — re-run install if expected`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const url = choice.spec.urlForEntry?.(entry);
|
|
250
|
+
log(` ✓ ${entry.name}${url ? ` — ${url}` : ""}`);
|
|
251
|
+
}
|
|
252
|
+
log("");
|
|
253
|
+
log(
|
|
254
|
+
"Discovery: ~/.parachute/services.json (CLI) and the hub's /.well-known/parachute.json (HTTP).",
|
|
255
|
+
);
|
|
256
|
+
log("");
|
|
257
|
+
log("Next: open Claude Code and try");
|
|
258
|
+
log(' claude "Hello, can you help me set up my Parachute vault?"');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function setup(opts: SetupOpts = {}): Promise<number> {
|
|
262
|
+
const manifestPath = opts.manifestPath ?? SERVICES_MANIFEST_PATH;
|
|
263
|
+
const configDir = opts.configDir ?? CONFIG_DIR;
|
|
264
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
265
|
+
const availability = opts.availability ?? defaultAvailability();
|
|
266
|
+
const installFn = opts.installFn ?? install;
|
|
267
|
+
|
|
268
|
+
log("Welcome to Parachute setup.");
|
|
269
|
+
log("");
|
|
270
|
+
log("This walks you through installing the Parachute services and configuring");
|
|
271
|
+
log("them for first use. Existing installs are detected and skipped.");
|
|
272
|
+
log("");
|
|
273
|
+
|
|
274
|
+
const survey = surveyServices(manifestPath);
|
|
275
|
+
const installed = survey.filter((s) => s.installed);
|
|
276
|
+
const offered = survey.filter((s) => !s.installed);
|
|
277
|
+
|
|
278
|
+
if (installed.length > 0) {
|
|
279
|
+
log("Already installed:");
|
|
280
|
+
for (const s of installed) log(` ✓ ${s.short}`);
|
|
281
|
+
log("");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (offered.length === 0) {
|
|
285
|
+
log("All known services are already installed. Nothing to do.");
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (availability.kind !== "available") {
|
|
290
|
+
log(
|
|
291
|
+
"Non-interactive shell — `parachute setup` needs a TTY. Run interactively, or use `parachute install <svc>` directly.",
|
|
292
|
+
);
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
const prompt = availability.prompt;
|
|
296
|
+
|
|
297
|
+
log("Available to install:");
|
|
298
|
+
for (let i = 0; i < offered.length; i++) {
|
|
299
|
+
const c = offered[i];
|
|
300
|
+
if (!c) continue;
|
|
301
|
+
log(` [${i + 1}] ${c.short.padEnd(8)} — ${blurbFor(c)}`);
|
|
302
|
+
}
|
|
303
|
+
log("");
|
|
304
|
+
|
|
305
|
+
let picks: ServiceChoice[] | undefined;
|
|
306
|
+
while (!picks) {
|
|
307
|
+
const raw = await prompt("Which to install? (numbers/names, comma-separated; Enter for all): ");
|
|
308
|
+
const result = parseServicePicks(raw, offered);
|
|
309
|
+
if ("error" in result) {
|
|
310
|
+
log(` ${result.error}`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (result.picks.length === 0) {
|
|
314
|
+
log(" no services picked — try again");
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
picks = result.picks;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Pre-collect per-service answers so the operator sits through one batch
|
|
321
|
+
// of questions instead of mid-stream interruptions.
|
|
322
|
+
const vaultPick = picks.find((p) => p.short === "vault");
|
|
323
|
+
const scribePick = picks.find((p) => p.short === "scribe");
|
|
324
|
+
|
|
325
|
+
let vaultAnswer: VaultAnswer | undefined;
|
|
326
|
+
if (vaultPick) {
|
|
327
|
+
log("");
|
|
328
|
+
vaultAnswer = await askVaultName(prompt, log);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let scribeAnswer: ScribeAnswer | undefined;
|
|
332
|
+
if (scribePick) {
|
|
333
|
+
scribeAnswer = await askScribeProvider(prompt, log);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
log("");
|
|
337
|
+
log("Configuring…");
|
|
338
|
+
log("");
|
|
339
|
+
|
|
340
|
+
let firstFailure = 0;
|
|
341
|
+
for (const choice of picks) {
|
|
342
|
+
log(`— ${choice.short} —`);
|
|
343
|
+
const installOpts: InstallOpts = { manifestPath, configDir, log, ...opts.baseInstallOpts };
|
|
344
|
+
if (opts.tag !== undefined) installOpts.tag = opts.tag;
|
|
345
|
+
if (opts.noStart) installOpts.noStart = true;
|
|
346
|
+
if (choice.short === "vault" && vaultAnswer) {
|
|
347
|
+
installOpts.vaultName = vaultAnswer.vaultName;
|
|
348
|
+
}
|
|
349
|
+
if (choice.short === "scribe" && scribeAnswer) {
|
|
350
|
+
installOpts.scribeProvider = scribeAnswer.provider;
|
|
351
|
+
if (scribeAnswer.apiKey) installOpts.scribeKey = scribeAnswer.apiKey;
|
|
352
|
+
}
|
|
353
|
+
const code = await installFn(choice.short, installOpts);
|
|
354
|
+
if (code !== 0 && firstFailure === 0) firstFailure = code;
|
|
355
|
+
log("");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (firstFailure !== 0) {
|
|
359
|
+
log(`⚠ One or more installs returned a non-zero exit code (${firstFailure}).`);
|
|
360
|
+
log(" Re-run `parachute install <svc>` for the failing service to retry.");
|
|
361
|
+
return firstFailure;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
summarizeUrls(manifestPath, picks, log);
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -142,7 +142,10 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
142
142
|
*/
|
|
143
143
|
const rows: StatusRow[] = await Promise.all(
|
|
144
144
|
manifest.services.map(async (entry) => {
|
|
145
|
-
|
|
145
|
+
// Third-party rows (with `installDir`) live under `~/.parachute/<entry.name>/`,
|
|
146
|
+
// matching what `parachute start` uses as the short. First-party rows still
|
|
147
|
+
// map manifestName → short via the canonical fallback.
|
|
148
|
+
const short = shortNameForManifest(entry.name) ?? (entry.installDir ? entry.name : undefined);
|
|
146
149
|
const proc = short ? processState(short, configDir, alive) : undefined;
|
|
147
150
|
|
|
148
151
|
const processLabel =
|