portable-agent-layer 0.90.0 → 0.91.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/package.json +1 -1
- package/src/cli/doctor/agents.ts +41 -7
- package/src/cli/doctor/environment.ts +16 -8
- package/src/cli/doctor/finding.ts +7 -2
- package/src/cli/doctor/inference.ts +5 -1
- package/src/cli/doctor/render.ts +231 -50
- package/src/cli/doctor/run.ts +33 -15
- package/src/cli/index.ts +46 -165
- package/src/cli/install-flow.ts +331 -0
- package/src/cli/reinstall.ts +5 -1
- package/src/cli/setup-attribution.ts +9 -9
- package/src/cli/setup-auto-update.ts +8 -7
- package/src/cli/setup-identity.ts +10 -15
- package/src/cli/ui/banner.ts +50 -0
- package/src/cli/ui/live.ts +112 -0
- package/src/cli/ui/rail.ts +119 -0
- package/src/cli/ui/style.ts +132 -0
- package/src/cli/ui/terminal.ts +81 -0
- package/src/cli/ui/wrap.ts +19 -0
- package/src/cli/update-flow.ts +98 -0
- package/src/cli/whats-new.ts +108 -0
- package/src/targets/lib.ts +49 -6
package/src/cli/index.ts
CHANGED
|
@@ -35,8 +35,8 @@ import { palHome, palPkg, paths, platform, toPath } from "../hooks/lib/paths";
|
|
|
35
35
|
import { log, narrateSteps } from "../targets/lib";
|
|
36
36
|
import { helpText, runCommand } from "../tools/lib/command";
|
|
37
37
|
import { type DoctorResult, detectAgents } from "./doctor/agents";
|
|
38
|
-
import { installChromium } from "./doctor/environment";
|
|
39
38
|
import { runDoctor } from "./doctor/run";
|
|
39
|
+
import type { InstallPlan } from "./install-flow";
|
|
40
40
|
import { findSessionAgent, NO_SESSION_AGENT_MESSAGE } from "./session-agent";
|
|
41
41
|
import { cliTree, type DebugState } from "./tree";
|
|
42
42
|
|
|
@@ -45,7 +45,7 @@ const CLI_PATH = ["pal", "cli"];
|
|
|
45
45
|
const tree = cliTree({
|
|
46
46
|
init: ({ argv }) => init(argv),
|
|
47
47
|
install: async ({ argv }) => {
|
|
48
|
-
|
|
48
|
+
narrateSteps(argv.includes("--verbose"));
|
|
49
49
|
process.exit(await install(resolveTargets(argv), argv));
|
|
50
50
|
},
|
|
51
51
|
uninstall: ({ argv }) => uninstall(argv),
|
|
@@ -153,14 +153,6 @@ async function doctor(args: string[]): Promise<never> {
|
|
|
153
153
|
|
|
154
154
|
// ── Helpers ──
|
|
155
155
|
|
|
156
|
-
function banner() {
|
|
157
|
-
console.log("");
|
|
158
|
-
console.log(" ╔═══════════════════════════════════╗");
|
|
159
|
-
console.log(" ║ PAL — Portable Agent Layer ║");
|
|
160
|
-
console.log(" ╚═══════════════════════════════════╝");
|
|
161
|
-
console.log("");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
156
|
function showHelp() {
|
|
165
157
|
console.log(
|
|
166
158
|
`Usage: pal [agent-args...]\n\n Start the first installed agent, passing the arguments on\n\n${helpText(tree, CLI_PATH)}`
|
|
@@ -268,42 +260,27 @@ async function probeInference(): Promise<void> {
|
|
|
268
260
|
|
|
269
261
|
// ── Commands ──
|
|
270
262
|
|
|
271
|
-
|
|
263
|
+
/**
|
|
264
|
+
* memory/state is scaffolded here, not left to generateSkillIndex: that returns
|
|
265
|
+
* early when ~/.pal/skills is absent, so an init that installs no skills would
|
|
266
|
+
* leave every writer of memory/state with nowhere to write.
|
|
267
|
+
*/
|
|
268
|
+
async function createPalHome(): Promise<void> {
|
|
272
269
|
const { scaffoldTelos } = await import("../targets/lib");
|
|
273
|
-
|
|
274
|
-
banner();
|
|
275
|
-
narrateSteps(args.includes("--verbose"));
|
|
276
|
-
|
|
277
|
-
const health = detectAgents();
|
|
278
|
-
if (!health.hasAgent) process.exit(runDoctor([], health));
|
|
279
|
-
|
|
280
270
|
const home = palHome();
|
|
281
|
-
log.info(`Creating PAL home at ${home}`);
|
|
282
271
|
mkdirSync(resolve(home, "telos"), { recursive: true });
|
|
283
|
-
mkdirSync(resolve(home, "memory"), { recursive: true });
|
|
284
|
-
// Scaffolded here, not left to generateSkillIndex: that returns early when
|
|
285
|
-
// ~/.pal/skills is absent, so an init that installs no skills would leave
|
|
286
|
-
// every writer of memory/state with nowhere to write.
|
|
287
272
|
mkdirSync(resolve(home, "memory", "state"), { recursive: true });
|
|
288
|
-
|
|
289
273
|
scaffoldTelos();
|
|
290
|
-
|
|
291
|
-
// Auto-detect available targets
|
|
292
|
-
const targets = resolveTargets(args, health);
|
|
293
|
-
process.exit(await install(targets, args));
|
|
294
274
|
}
|
|
295
275
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const r = spawnSync(cmd, args, { cwd, encoding: "utf-8", shell: true });
|
|
305
|
-
if (r.status !== 0) process.stderr.write((r.stdout ?? "") + (r.stderr ?? ""));
|
|
306
|
-
return r.status;
|
|
276
|
+
async function init(args: string[]) {
|
|
277
|
+
narrateSteps(args.includes("--verbose"));
|
|
278
|
+
const health = detectAgents();
|
|
279
|
+
if (!health.hasAgent) process.exit(runDoctor([], health));
|
|
280
|
+
const targets = resolveTargets(args, health);
|
|
281
|
+
process.exit(
|
|
282
|
+
await install(targets, args, { kind: "init", prepareHome: createPalHome })
|
|
283
|
+
);
|
|
307
284
|
}
|
|
308
285
|
|
|
309
286
|
function targetScripts(): Record<
|
|
@@ -338,91 +315,28 @@ function targetScripts(): Record<
|
|
|
338
315
|
};
|
|
339
316
|
}
|
|
340
317
|
|
|
341
|
-
function targetInstallers():
|
|
342
|
-
return
|
|
343
|
-
agent,
|
|
344
|
-
|
|
345
|
-
targetScripts()[agent].install,
|
|
346
|
-
]);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function install(targets: Targets, args: string[]): Promise<number> {
|
|
350
|
-
narrateSteps(args.includes("--verbose"));
|
|
351
|
-
const pkg = palPkg();
|
|
352
|
-
const { dependencyInstall } = await import("./dependencies");
|
|
353
|
-
const { isRepoMode } = await import("../hooks/handlers/update-check");
|
|
354
|
-
const bunInstall = dependencyInstall(pkg, isRepoMode());
|
|
355
|
-
if (bunInstall && runQuietly("bun", bunInstall, pkg) !== 0) {
|
|
356
|
-
log.warn("bun install failed — continuing anyway, but hooks may not work");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const { applyPendingMigrations } = await import("./migrate");
|
|
360
|
-
for (const line of applyPendingMigrations()) log.info(line);
|
|
361
|
-
|
|
362
|
-
// Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
|
|
363
|
-
if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") installChromium();
|
|
364
|
-
|
|
365
|
-
// Scaffold TELOS + PAL settings, then prompt for missing identity
|
|
366
|
-
const { scaffoldTelos, scaffoldPalSettings, copyPalDocs, generateSkillIndex } =
|
|
367
|
-
await import("../targets/lib");
|
|
368
|
-
const { promptIdentity } = await import("./setup-identity");
|
|
369
|
-
const { promptAttribution } = await import("./setup-attribution");
|
|
370
|
-
const { promptAutoUpdate } = await import("./setup-auto-update");
|
|
371
|
-
scaffoldTelos();
|
|
372
|
-
scaffoldPalSettings();
|
|
373
|
-
await promptIdentity();
|
|
374
|
-
await promptAttribution();
|
|
375
|
-
await promptAutoUpdate();
|
|
376
|
-
|
|
377
|
-
// Registers the label loadActor derives, so it travels on the next export.
|
|
378
|
-
const { ensureActorRegistered } = await import("../hooks/lib/actor");
|
|
379
|
-
ensureActorRegistered();
|
|
380
|
-
|
|
381
|
-
// Shared, target-independent state. Every target installer used to repeat these
|
|
382
|
-
// identical calls; AGENTS.md in particular must exist before any target symlinks
|
|
383
|
-
// to it, so it runs once here rather than once per target.
|
|
384
|
-
const { regenerateIfNeeded } = await import("../hooks/lib/claude-md");
|
|
385
|
-
const palDocsCount = copyPalDocs();
|
|
386
|
-
regenerateIfNeeded();
|
|
387
|
-
|
|
388
|
-
for (const [target, label, installTarget] of targetInstallers()) {
|
|
389
|
-
if (!targets[target]) continue;
|
|
390
|
-
log.heading(label);
|
|
391
|
-
await installTarget();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// The rest of the shared work reads what the installers just wrote: the index
|
|
395
|
-
// walks ~/.pal/skills, and the digests land in ~/.cursor/rules and
|
|
396
|
-
// ~/.copilot/instructions and the Antigravity plugin's rules/, which are skipped
|
|
397
|
-
// when the agent's home is absent.
|
|
398
|
-
const { writeContextDigests } = await import("../hooks/handlers/context-digests");
|
|
399
|
-
const indexedSkills = generateSkillIndex();
|
|
400
|
-
writeContextDigests();
|
|
401
|
-
log.success(
|
|
402
|
-
`Shared: ${indexedSkills} skills indexed · ${palDocsCount} docs → ~/.pal/docs/ · AGENTS.md + context digests written`
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
await refreshControlRoom();
|
|
406
|
-
|
|
407
|
-
if (args.includes("--verbose")) console.log("");
|
|
408
|
-
return runDoctor(args);
|
|
318
|
+
function targetInstallers(): Record<AgentName, () => Promise<unknown>> {
|
|
319
|
+
return Object.fromEntries(
|
|
320
|
+
AGENT_NAMES.map((agent) => [agent, targetScripts()[agent].install])
|
|
321
|
+
) as Record<AgentName, () => Promise<unknown>>;
|
|
409
322
|
}
|
|
410
323
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
324
|
+
async function install(
|
|
325
|
+
targets: Targets,
|
|
326
|
+
args: string[],
|
|
327
|
+
plan: Pick<InstallPlan, "kind" | "prepareHome"> = { kind: "install" }
|
|
328
|
+
): Promise<number> {
|
|
329
|
+
const { runInstall } = await import("./install-flow");
|
|
330
|
+
const updatedFrom = process.env.PAL_UPDATED_FROM;
|
|
331
|
+
return runInstall({
|
|
332
|
+
...plan,
|
|
333
|
+
kind: updatedFrom ? "update" : plan.kind,
|
|
334
|
+
agents: AGENT_NAMES.filter((agent) => targets[agent]),
|
|
335
|
+
installers: targetInstallers(),
|
|
336
|
+
verbose: args.includes("--verbose"),
|
|
337
|
+
version: packageVersion(),
|
|
338
|
+
updatedFrom,
|
|
339
|
+
});
|
|
426
340
|
}
|
|
427
341
|
|
|
428
342
|
async function uninstall(args: string[]) {
|
|
@@ -643,50 +557,17 @@ async function update() {
|
|
|
643
557
|
const { checkForUpdate, clearUpdateCache } = await import(
|
|
644
558
|
"../hooks/handlers/update-check"
|
|
645
559
|
);
|
|
646
|
-
const result = await checkForUpdate(true);
|
|
647
|
-
|
|
648
|
-
log.info(`Current: ${result.current} (${result.mode} mode)`);
|
|
649
|
-
|
|
650
|
-
if (!result.available) {
|
|
651
|
-
log.success("Already up to date.");
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
log.info(`Available: ${result.latest}`);
|
|
656
|
-
|
|
657
|
-
const pkg = palPkg();
|
|
658
|
-
if (result.mode === "repo") {
|
|
659
|
-
log.info("Pulling updates...");
|
|
660
|
-
const pull = spawnSync("git", ["pull", "--ff-only"], { cwd: pkg, stdio: "inherit" });
|
|
661
|
-
if (pull.status !== 0) {
|
|
662
|
-
log.error("git pull failed. You may have local changes — try pulling manually.");
|
|
663
|
-
process.exit(1);
|
|
664
|
-
}
|
|
665
|
-
} else {
|
|
666
|
-
log.info("Updating via bun...");
|
|
667
|
-
const up = spawnSync("bun", ["add", "-g", `portable-agent-layer@${result.latest}`], {
|
|
668
|
-
stdio: "inherit",
|
|
669
|
-
});
|
|
670
|
-
if (up.status !== 0) {
|
|
671
|
-
log.error(`Update failed. Try: bun add -g portable-agent-layer@${result.latest}`);
|
|
672
|
-
process.exit(1);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
let newPkg: { version: string };
|
|
677
|
-
try {
|
|
678
|
-
newPkg = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
|
|
679
|
-
version: string;
|
|
680
|
-
};
|
|
681
|
-
} catch (e) {
|
|
682
|
-
throw new Error(`Failed to read updated package.json: ${e}`);
|
|
683
|
-
}
|
|
684
|
-
log.success(`Updated: ${result.current} → ${newPkg.version}`);
|
|
685
|
-
clearUpdateCache();
|
|
686
|
-
|
|
687
|
-
log.info("Reinstalling...");
|
|
688
560
|
const { reinstallInFreshProcess } = await import("./reinstall");
|
|
689
|
-
|
|
561
|
+
const { runUpdate } = await import("./update-flow");
|
|
562
|
+
process.exit(
|
|
563
|
+
await runUpdate({
|
|
564
|
+
check: () => checkForUpdate(true),
|
|
565
|
+
reinstall: (env) => {
|
|
566
|
+
clearUpdateCache();
|
|
567
|
+
return reinstallInFreshProcess(process.argv[1], env);
|
|
568
|
+
},
|
|
569
|
+
})
|
|
570
|
+
);
|
|
690
571
|
}
|
|
691
572
|
|
|
692
573
|
function cliDebug(state: DebugState) {
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pal cli init`, `install` and the reinstall that ends `update`: one rail of
|
|
3
|
+
* steps, each settling into a line that says what it did, then the doctor's verdict.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { AGENT_REGISTRY, type AgentName } from "../hooks/lib/agent-registry";
|
|
8
|
+
import { palHome, palPkg } from "../hooks/lib/paths";
|
|
9
|
+
import { raw as readSettings } from "../hooks/lib/settings";
|
|
10
|
+
import { capturingLog, type LogLevel, narrateSteps } from "../targets/lib";
|
|
11
|
+
import { agentInventory, detectAgents } from "./doctor/agents";
|
|
12
|
+
import {
|
|
13
|
+
type DoctorReport,
|
|
14
|
+
doctorExitCode,
|
|
15
|
+
plainProblemLines,
|
|
16
|
+
plainSummary,
|
|
17
|
+
problemLines,
|
|
18
|
+
tally,
|
|
19
|
+
} from "./doctor/render";
|
|
20
|
+
import { doctorReport } from "./doctor/run";
|
|
21
|
+
import { banner } from "./ui/banner";
|
|
22
|
+
import { type Done, LiveRail, type Note } from "./ui/live";
|
|
23
|
+
import { box, healthBadge, mark, railEnd, railGap, railNote, railTop } from "./ui/rail";
|
|
24
|
+
import { createStyle, homeShortened, PALETTE, type Style } from "./ui/style";
|
|
25
|
+
import { whatsNew } from "./whats-new";
|
|
26
|
+
|
|
27
|
+
type InstallKind = "init" | "install" | "update";
|
|
28
|
+
|
|
29
|
+
export interface InstallPlan {
|
|
30
|
+
kind: InstallKind;
|
|
31
|
+
agents: AgentName[];
|
|
32
|
+
installers: Record<AgentName, () => Promise<unknown>>;
|
|
33
|
+
verbose: boolean;
|
|
34
|
+
version: string;
|
|
35
|
+
updatedFrom?: string;
|
|
36
|
+
prepareHome?: () => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Captured {
|
|
40
|
+
code: number;
|
|
41
|
+
output: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runCaptured(
|
|
45
|
+
cmd: string,
|
|
46
|
+
args: string[],
|
|
47
|
+
cwd?: string
|
|
48
|
+
): Promise<Captured> {
|
|
49
|
+
return new Promise((done) => {
|
|
50
|
+
const child = spawn(cmd, args, { cwd, shell: true });
|
|
51
|
+
let output = "";
|
|
52
|
+
const collect = (chunk: Buffer) => {
|
|
53
|
+
output += chunk.toString();
|
|
54
|
+
};
|
|
55
|
+
child.stdout?.on("data", collect);
|
|
56
|
+
child.stderr?.on("data", collect);
|
|
57
|
+
child.on("error", (error) => done({ code: 1, output: error.message }));
|
|
58
|
+
child.on("close", (code) => done({ code: code ?? 1, output }));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lastLines(output: string, count = 4): Note[] {
|
|
63
|
+
return output
|
|
64
|
+
.trim()
|
|
65
|
+
.split("\n")
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.slice(-count)
|
|
68
|
+
.map((text) => ({ level: "info", text }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const labelOf = (agent: AgentName) => AGENT_REGISTRY[agent].label;
|
|
72
|
+
|
|
73
|
+
function plural(count: number, noun: string): string {
|
|
74
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function openRail(rail: LiveRail, style: Style, plan: InstallPlan): void {
|
|
78
|
+
if (plan.kind === "update") return;
|
|
79
|
+
if (!style.term.rich) {
|
|
80
|
+
rail.line(`PAL ${plan.version} ${plan.kind}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const line of banner(style, plan.version)) rail.line(line);
|
|
84
|
+
const title =
|
|
85
|
+
plan.kind === "init"
|
|
86
|
+
? "Setting up PAL"
|
|
87
|
+
: `Installing into ${plural(plan.agents.length, "agent")}`;
|
|
88
|
+
rail.line(railTop(style, title));
|
|
89
|
+
rail.line(railGap(style));
|
|
90
|
+
if (plan.kind !== "init") return;
|
|
91
|
+
const found = plan.agents.map(labelOf).join(` ${style.glyph.dot} `);
|
|
92
|
+
rail.line(
|
|
93
|
+
`${mark(style, "info")} Found ${plural(plan.agents.length, "agent")} ${style.soft(found)}`
|
|
94
|
+
);
|
|
95
|
+
rail.line(railGap(style));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function installDependencies(): Promise<Done | null> {
|
|
99
|
+
const { dependencyInstall } = await import("./dependencies");
|
|
100
|
+
const { isRepoMode } = await import("../hooks/handlers/update-check");
|
|
101
|
+
const args = dependencyInstall(palPkg(), isRepoMode());
|
|
102
|
+
if (!args) return null;
|
|
103
|
+
const run = await runCaptured("bun", args, palPkg());
|
|
104
|
+
if (run.code === 0)
|
|
105
|
+
return /no changes/.test(run.output) ? null : { detail: ["installed"] };
|
|
106
|
+
return {
|
|
107
|
+
detail: ["bun install failed"],
|
|
108
|
+
notes: [
|
|
109
|
+
{ level: "warn", text: "continuing anyway, but hooks may not work" },
|
|
110
|
+
...lastLines(run.output),
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function applyMigrations(): Promise<Done | null> {
|
|
116
|
+
const { applyPendingMigrations } = await import("./migrate");
|
|
117
|
+
const lines = applyPendingMigrations();
|
|
118
|
+
if (lines.length === 0) return null;
|
|
119
|
+
return {
|
|
120
|
+
detail: [`${plural(lines.length, "change")} applied`],
|
|
121
|
+
notes: lines.map((text) => ({ level: "info", text })),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function downloadChromium(): Promise<Done | null> {
|
|
126
|
+
if (process.env.PAL_SKIP_BROWSER_INSTALL === "1") return null;
|
|
127
|
+
const { installChromium } = await import("./doctor/environment");
|
|
128
|
+
const installed = await installChromium();
|
|
129
|
+
if (installed === null) return null;
|
|
130
|
+
return { detail: [installed ? "installed for screenshots" : "download failed"] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function askSetupQuestions(): Promise<boolean> {
|
|
134
|
+
const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
|
|
135
|
+
const { promptIdentity } = await import("./setup-identity");
|
|
136
|
+
const { promptAttribution } = await import("./setup-attribution");
|
|
137
|
+
const { promptAutoUpdate } = await import("./setup-auto-update");
|
|
138
|
+
scaffoldTelos();
|
|
139
|
+
scaffoldPalSettings();
|
|
140
|
+
const asked = [
|
|
141
|
+
await promptIdentity(),
|
|
142
|
+
await promptAttribution(),
|
|
143
|
+
await promptAutoUpdate(),
|
|
144
|
+
];
|
|
145
|
+
const { ensureActorRegistered } = await import("../hooks/lib/actor");
|
|
146
|
+
ensureActorRegistered();
|
|
147
|
+
return asked.some(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function noteFor(level: LogLevel, message: string, verbose: boolean): Note | null {
|
|
151
|
+
const text = homeShortened(message);
|
|
152
|
+
if (level === "warn") return { level: "warn", text };
|
|
153
|
+
if (level === "error") return { level: "fail", text };
|
|
154
|
+
return verbose ? { level: "info", text } : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runInstaller(plan: InstallPlan, agent: AgentName, notes: Note[]) {
|
|
158
|
+
await capturingLog((level, message) => {
|
|
159
|
+
const note = noteFor(level, message, plan.verbose);
|
|
160
|
+
if (note) notes.push(note);
|
|
161
|
+
}, plan.installers[agent]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function installOne(plan: InstallPlan, agent: AgentName): Promise<Done> {
|
|
165
|
+
const notes: Note[] = [];
|
|
166
|
+
await runInstaller(plan, agent, notes);
|
|
167
|
+
return { detail: agentInventory(agent), notes };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function installAll(plan: InstallPlan): Promise<Done> {
|
|
171
|
+
const notes: Note[] = [];
|
|
172
|
+
for (const agent of plan.agents) await runInstaller(plan, agent, notes);
|
|
173
|
+
return { detail: plan.agents, notes };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function prepareShared(): Promise<number> {
|
|
177
|
+
const { copyPalDocs } = await import("../targets/lib");
|
|
178
|
+
const { regenerateIfNeeded } = await import("../hooks/lib/claude-md");
|
|
179
|
+
const docs = copyPalDocs();
|
|
180
|
+
regenerateIfNeeded();
|
|
181
|
+
return docs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function finishShared(docs: number): Promise<Done> {
|
|
185
|
+
const { generateSkillIndex } = await import("../targets/lib");
|
|
186
|
+
const { writeContextDigests } = await import("../hooks/handlers/context-digests");
|
|
187
|
+
const skills = generateSkillIndex();
|
|
188
|
+
writeContextDigests();
|
|
189
|
+
return {
|
|
190
|
+
detail: [`${plural(skills, "skill")} indexed`, plural(docs, "doc"), "digests"],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A published install carries the page in its tarball; a checkout does not —
|
|
196
|
+
* ui/dist is gitignored, so a pull leaves whatever was built last. Rebuild
|
|
197
|
+
* there, then replace the process, because the API is the running code.
|
|
198
|
+
*/
|
|
199
|
+
async function refreshControlRoom(): Promise<Done | null> {
|
|
200
|
+
const { isRepoMode } = await import("../hooks/handlers/update-check");
|
|
201
|
+
const { buildPage } = await import("../tools/control-room/static");
|
|
202
|
+
const rebuilt = isRepoMode() ? buildPage() : null;
|
|
203
|
+
if (rebuilt === false)
|
|
204
|
+
return {
|
|
205
|
+
detail: ["page not rebuilt"],
|
|
206
|
+
notes: [{ level: "warn", text: "run: bun run build:ui" }],
|
|
207
|
+
};
|
|
208
|
+
const { restartIfRunning } = await import("./server");
|
|
209
|
+
const restarted = await restartIfRunning();
|
|
210
|
+
const detail = [rebuilt ? "page rebuilt" : "", restarted ? "restarted" : ""].filter(
|
|
211
|
+
Boolean
|
|
212
|
+
);
|
|
213
|
+
return detail.length > 0 ? { detail } : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function truncated(text: string, width: number): string {
|
|
217
|
+
return [...text].length <= width ? text : `${[...text].slice(0, width - 1).join("")}…`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function notesLink(url: string, more: number, width: number): [string, string] {
|
|
221
|
+
const label = more > 0 ? `${more} more:` : "all notes:";
|
|
222
|
+
const room = width - label.length - 3;
|
|
223
|
+
const short =
|
|
224
|
+
url.length <= room ? url : url.replace(/github\.com\/[^/]+\/[^/]+/, "github.com/…");
|
|
225
|
+
return [label, short];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function showWhatsNew(rail: LiveRail, style: Style, from: string, to: string) {
|
|
229
|
+
const news = await whatsNew(from, to);
|
|
230
|
+
if (!news) return;
|
|
231
|
+
const width = Math.max(30, style.term.width - 6);
|
|
232
|
+
if (!style.term.rich) {
|
|
233
|
+
for (const item of news.items) rail.line(`new ${item}`);
|
|
234
|
+
rail.line(`notes ${news.url}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const [label, url] = notesLink(news.url, news.more, width);
|
|
238
|
+
rail.line(railGap(style));
|
|
239
|
+
const heading = `What's new in ${to.replace(/\.0$/, "")}`;
|
|
240
|
+
rail.line(`${mark(style, "info")} ${style.bold(heading)}`);
|
|
241
|
+
for (const item of news.items)
|
|
242
|
+
rail.line(
|
|
243
|
+
railNote(
|
|
244
|
+
style,
|
|
245
|
+
`${style.paint(PALETTE.violet, style.glyph.sparkle)} ${truncated(item, width)}`
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
const lead = style.dim(`${style.glyph.arrow} ${label}`);
|
|
249
|
+
rail.line(railNote(style, `${lead} ${style.cmd(url)}`));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function closeRail(rail: LiveRail, style: Style, report: DoctorReport | null): void {
|
|
253
|
+
if (!report) {
|
|
254
|
+
rail.line(style.term.rich ? railEnd(style, style.soft("Installed")) : "installed");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const t = tally(report.findings);
|
|
258
|
+
if (!style.term.rich) {
|
|
259
|
+
for (const line of [...plainProblemLines(t), plainSummary(t)]) rail.line(line);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const count = { fails: t.fails.length, warns: t.warns.length };
|
|
263
|
+
rail.line(railGap(style));
|
|
264
|
+
const passedCount = style.soft(`${plural(t.passes.length, "check")} passed`);
|
|
265
|
+
rail.line(railEnd(style, `${healthBadge(style, count)} ${passedCount}`));
|
|
266
|
+
const problems = problemLines(style, t, " ");
|
|
267
|
+
rail.line();
|
|
268
|
+
for (const line of problems) rail.line(line);
|
|
269
|
+
if (problems.length > 0) rail.line();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function showNextSteps(rail: LiveRail, style: Style): void {
|
|
273
|
+
const who = readSettings().identity;
|
|
274
|
+
const name = who?.principal?.name ? `, ${who.principal.name}` : "";
|
|
275
|
+
const ai = who?.ai?.name || "PAL";
|
|
276
|
+
if (!style.term.rich) {
|
|
277
|
+
rail.line(
|
|
278
|
+
`Next: run pal to start your agent, ask it to "onboard me", run pal cli doctor anytime`
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const step = (n: string, command: string, what: string) =>
|
|
283
|
+
`${style.dim(n)} ${style.cmd(command.padEnd(17))}${style.soft(what)}`;
|
|
284
|
+
const lines = box(style, [
|
|
285
|
+
"",
|
|
286
|
+
style.bold(style.gradient(`You're set${name}. ${ai} is ready.`)),
|
|
287
|
+
"",
|
|
288
|
+
step("1", "pal", "start your agent"),
|
|
289
|
+
step("2", '"onboard me"', "teach it your goals"),
|
|
290
|
+
step("3", "pal cli doctor", "check health anytime"),
|
|
291
|
+
"",
|
|
292
|
+
]);
|
|
293
|
+
for (const line of lines) rail.line(line);
|
|
294
|
+
rail.line();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function installAgents(rail: LiveRail, plan: InstallPlan): Promise<void> {
|
|
298
|
+
if (plan.kind === "update") {
|
|
299
|
+
await rail.step("Reinstall", "reinstalling…", () => installAll(plan));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
for (const agent of plan.agents)
|
|
303
|
+
await rail.step(labelOf(agent), "installing…", () => installOne(plan, agent));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function runInstall(plan: InstallPlan): Promise<number> {
|
|
307
|
+
narrateSteps(plan.verbose);
|
|
308
|
+
const style = createStyle();
|
|
309
|
+
const rail = new LiveRail(style);
|
|
310
|
+
openRail(rail, style, plan);
|
|
311
|
+
const prepareHome = plan.prepareHome;
|
|
312
|
+
if (prepareHome)
|
|
313
|
+
await rail.step("PAL home", "creating…", async () => {
|
|
314
|
+
await prepareHome();
|
|
315
|
+
return { detail: [homeShortened(palHome()), "TELOS scaffolded"] };
|
|
316
|
+
});
|
|
317
|
+
await rail.step("Dependencies", "installing…", installDependencies);
|
|
318
|
+
await rail.step("Migrations", "applying…", applyMigrations);
|
|
319
|
+
await rail.step("Chromium", "downloading the browser…", downloadChromium);
|
|
320
|
+
if ((await askSetupQuestions()) && style.term.rich) rail.line(railGap(style));
|
|
321
|
+
const docs = await prepareShared();
|
|
322
|
+
await installAgents(rail, plan);
|
|
323
|
+
await rail.step("Shared", "indexing…", () => finishShared(docs));
|
|
324
|
+
await rail.step("Control room", "refreshing…", refreshControlRoom);
|
|
325
|
+
const report =
|
|
326
|
+
process.env.PAL_SKIP_DOCTOR === "1" ? null : doctorReport(detectAgents());
|
|
327
|
+
if (plan.updatedFrom) await showWhatsNew(rail, style, plan.updatedFrom, plan.version);
|
|
328
|
+
closeRail(rail, style, report);
|
|
329
|
+
if (plan.kind === "init") showNextSteps(rail, style);
|
|
330
|
+
return report ? doctorExitCode(report.findings) : 0;
|
|
331
|
+
}
|
package/src/cli/reinstall.ts
CHANGED
|
@@ -5,9 +5,13 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
* modules in Bun's cache, so a new installer importing a new export would bind
|
|
6
6
|
* against the stale module and throw. A fresh process loads everything from disk.
|
|
7
7
|
*/
|
|
8
|
-
export function reinstallInFreshProcess(
|
|
8
|
+
export function reinstallInFreshProcess(
|
|
9
|
+
entry: string = process.argv[1],
|
|
10
|
+
env: Record<string, string> = {}
|
|
11
|
+
): number {
|
|
9
12
|
const run = spawnSync(process.execPath, [entry, "cli", "install"], {
|
|
10
13
|
stdio: "inherit",
|
|
14
|
+
env: { ...process.env, ...env },
|
|
11
15
|
});
|
|
12
16
|
return run.status ?? 1;
|
|
13
17
|
}
|
|
@@ -15,17 +15,16 @@ import {
|
|
|
15
15
|
} from "../hooks/lib/settings";
|
|
16
16
|
import { buildAttributionText } from "../targets/lib";
|
|
17
17
|
|
|
18
|
-
/** Prompt once for git attribution opt-in
|
|
19
|
-
export async function promptAttribution(): Promise<
|
|
20
|
-
if (!process.stdin.isTTY) return;
|
|
18
|
+
/** Prompt once for git attribution opt-in; true when it asked. */
|
|
19
|
+
export async function promptAttribution(): Promise<boolean> {
|
|
20
|
+
if (!process.stdin.isTTY) return false;
|
|
21
21
|
|
|
22
22
|
const settings = { ...readSettings() };
|
|
23
|
-
if (settings.attribution?.decided) return;
|
|
23
|
+
if (settings.attribution?.decided) return false;
|
|
24
24
|
|
|
25
25
|
const name = identity().ai.name;
|
|
26
26
|
const { commit, pr } = buildAttributionText(name);
|
|
27
27
|
|
|
28
|
-
clack.intro("Git attribution");
|
|
29
28
|
clack.note(
|
|
30
29
|
`commit: ${commit}\npr: ${pr}`,
|
|
31
30
|
`Credit ${name} on the commits & PRs it makes?`
|
|
@@ -36,14 +35,15 @@ export async function promptAttribution(): Promise<void> {
|
|
|
36
35
|
initialValue: true,
|
|
37
36
|
});
|
|
38
37
|
if (clack.isCancel(enabled)) {
|
|
39
|
-
clack.
|
|
40
|
-
return;
|
|
38
|
+
clack.log.warn("Skipped — will ask again next time");
|
|
39
|
+
return true;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
settings.attribution = { enabled: enabled === true, decided: true };
|
|
44
43
|
writeSettings(settings);
|
|
45
44
|
const state = enabled ? `${name} attribution on` : "Attribution off";
|
|
46
|
-
clack.
|
|
47
|
-
`${state}
|
|
45
|
+
clack.log.success(
|
|
46
|
+
`${state} · change later: edit pal-settings.json, then run 'pal install'`
|
|
48
47
|
);
|
|
48
|
+
return true;
|
|
49
49
|
}
|
|
@@ -21,13 +21,13 @@ function whatItDoes(): string {
|
|
|
21
21
|
: onClose;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** True when it asked. */
|
|
25
|
+
export async function promptAutoUpdate(): Promise<boolean> {
|
|
26
|
+
if (!process.stdin.isTTY) return false;
|
|
26
27
|
|
|
27
28
|
const settings = { ...readSettings() };
|
|
28
|
-
if (settings.autoUpdate?.decided) return;
|
|
29
|
+
if (settings.autoUpdate?.decided) return false;
|
|
29
30
|
|
|
30
|
-
clack.intro("Automatic updates");
|
|
31
31
|
clack.note(whatItDoes(), "Keep PAL up to date on its own?");
|
|
32
32
|
|
|
33
33
|
const enabled = await clack.confirm({
|
|
@@ -35,12 +35,13 @@ export async function promptAutoUpdate(): Promise<void> {
|
|
|
35
35
|
initialValue: false,
|
|
36
36
|
});
|
|
37
37
|
if (clack.isCancel(enabled)) {
|
|
38
|
-
clack.
|
|
39
|
-
return;
|
|
38
|
+
clack.log.warn("Skipped — will ask again next time");
|
|
39
|
+
return true;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
settings.autoUpdate = { enabled: enabled === true, decided: true };
|
|
43
43
|
writeSettings(settings);
|
|
44
44
|
const state = enabled ? "Automatic updates on" : "Automatic updates off";
|
|
45
|
-
clack.
|
|
45
|
+
clack.log.success(`${state} · change later: control room → Settings → Updates`);
|
|
46
|
+
return true;
|
|
46
47
|
}
|