portable-agent-layer 0.85.14 → 0.85.16
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/environment.ts +31 -11
- package/src/cli/index.ts +32 -68
- package/src/cli/leftovers.ts +118 -0
- package/src/cli/migrate.ts +38 -6
- package/src/hooks/lib/log.ts +1 -13
- package/src/targets/copilot/uninstall.ts +1 -17
- package/src/targets/lib.ts +16 -37
- package/src/targets/opencode/install.ts +2 -13
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
@@ -67,22 +68,47 @@ function playwrightPlatformOverride(host: BrowserHost): string {
|
|
|
67
68
|
return `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-${host.arch === "arm64" ? "arm64" : "x64"} `;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function chromiumInstallCommand(host: BrowserHost): string {
|
|
72
|
+
const pinned = host.playwright ? `playwright@${host.playwright.version}` : "playwright";
|
|
73
|
+
return `${playwrightPlatformOverride(host)}bun x ${pinned} install chromium`;
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
export function playwrightFinding(host: BrowserHost): Finding {
|
|
71
77
|
if (hasChromium(host)) return passed("playwright", "Playwright Chromium installed");
|
|
72
|
-
const pinned = host.playwright ? `playwright@${host.playwright.version}` : "playwright";
|
|
73
|
-
const override = playwrightPlatformOverride(host);
|
|
74
78
|
return warning(
|
|
75
79
|
"playwright.missing",
|
|
76
80
|
"Playwright's Chromium is not installed — create-pdf, consulting-report and screenshots fail",
|
|
77
81
|
{
|
|
78
|
-
say:
|
|
82
|
+
say: playwrightPlatformOverride(host)
|
|
79
83
|
? "Install it as Ubuntu 24.04, which Playwright supports"
|
|
80
84
|
: "Install it",
|
|
81
|
-
command:
|
|
85
|
+
command: chromiumInstallCommand(host),
|
|
82
86
|
}
|
|
83
87
|
);
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
function thisBrowserHost(): BrowserHost {
|
|
91
|
+
return {
|
|
92
|
+
browsersPath: playwrightBrowsersPath(),
|
|
93
|
+
playwright: installedPlaywright(),
|
|
94
|
+
platform: process.platform,
|
|
95
|
+
arch: process.arch,
|
|
96
|
+
osRelease: readOsRelease(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runInShell(command: string): boolean {
|
|
101
|
+
return spawnSync(command, { cwd: palPkg(), shell: true, stdio: "ignore" }).status === 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A failure is left to the doctor's report that follows, which names the fix. */
|
|
105
|
+
export function installChromium(
|
|
106
|
+
host: BrowserHost = thisBrowserHost(),
|
|
107
|
+
run: (command: string) => boolean = runInShell
|
|
108
|
+
): void {
|
|
109
|
+
if (!hasChromium(host)) run(chromiumInstallCommand(host));
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
interface PalInstall {
|
|
87
113
|
pal: string | null;
|
|
88
114
|
pkg: string;
|
|
@@ -136,13 +162,7 @@ export function environmentFindings(rtk: ToolCheck): Finding[] {
|
|
|
136
162
|
pkg: palPkg(),
|
|
137
163
|
repoMode: isRepoMode(),
|
|
138
164
|
}),
|
|
139
|
-
playwrightFinding(
|
|
140
|
-
browsersPath: playwrightBrowsersPath(),
|
|
141
|
-
playwright: installedPlaywright(),
|
|
142
|
-
platform: process.platform,
|
|
143
|
-
arch: process.arch,
|
|
144
|
-
osRelease: readOsRelease(),
|
|
145
|
-
}),
|
|
165
|
+
playwrightFinding(thisBrowserHost()),
|
|
146
166
|
rtkFinding(rtk),
|
|
147
167
|
...versionControlFindings(),
|
|
148
168
|
];
|
package/src/cli/index.ts
CHANGED
|
@@ -50,10 +50,10 @@ import { inference, previewInferenceRoute } from "../hooks/lib/inference";
|
|
|
50
50
|
import { logDebug } from "../hooks/lib/log";
|
|
51
51
|
import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine";
|
|
52
52
|
import { palHome, palPkg, paths, platform, toPath } from "../hooks/lib/paths";
|
|
53
|
-
import {
|
|
54
|
-
import { log } from "../targets/lib";
|
|
53
|
+
import { log, narrateSteps } from "../targets/lib";
|
|
55
54
|
import { builtinToolVerbs, runBuiltinTool } from "./builtin-tools";
|
|
56
55
|
import { type DoctorResult, detectAgents } from "./doctor/agents";
|
|
56
|
+
import { installChromium } from "./doctor/environment";
|
|
57
57
|
import { runDoctor } from "./doctor/run";
|
|
58
58
|
import { findSessionAgent, NO_SESSION_AGENT_MESSAGE } from "./session-agent";
|
|
59
59
|
|
|
@@ -155,7 +155,7 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
155
155
|
break;
|
|
156
156
|
case "install":
|
|
157
157
|
banner();
|
|
158
|
-
await install(resolveTargets(args));
|
|
158
|
+
process.exit(await install(resolveTargets(args), args));
|
|
159
159
|
break;
|
|
160
160
|
case "uninstall":
|
|
161
161
|
await uninstall(args);
|
|
@@ -265,24 +265,10 @@ function banner() {
|
|
|
265
265
|
console.log("");
|
|
266
266
|
console.log(" ╔═══════════════════════════════════╗");
|
|
267
267
|
console.log(" ║ PAL — Portable Agent Layer ║");
|
|
268
|
-
console.log(" ║ Non-destructive · Modular ║");
|
|
269
268
|
console.log(" ╚═══════════════════════════════════╝");
|
|
270
269
|
console.log("");
|
|
271
270
|
}
|
|
272
271
|
|
|
273
|
-
/**
|
|
274
|
-
* Install asks for identity and stops. TELOS is a conversation, not a form: a
|
|
275
|
-
* user who has just installed a tool cannot yet say what they want from it, and
|
|
276
|
-
* answers given under that pressure are worse than none.
|
|
277
|
-
*/
|
|
278
|
-
function pointAtOnboarding(): void {
|
|
279
|
-
const unanswered = telosStatus().filter((topic) => topic.priority && !topic.answered);
|
|
280
|
-
if (unanswered.length === 0) return;
|
|
281
|
-
log.info(
|
|
282
|
-
`PAL knows nothing about you yet (${unanswered.length} topics open). When you have half an hour, ask your agent to onboard you.`
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
272
|
function showHelp() {
|
|
287
273
|
console.log(`
|
|
288
274
|
Usage:
|
|
@@ -290,8 +276,9 @@ function showHelp() {
|
|
|
290
276
|
pal cli <command> [options] Admin commands
|
|
291
277
|
|
|
292
278
|
Admin commands:
|
|
293
|
-
pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all)
|
|
294
|
-
pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets
|
|
279
|
+
pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all), then run the doctor
|
|
280
|
+
pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets, then run the doctor
|
|
281
|
+
Add --verbose to either for the step-by-step log
|
|
295
282
|
pal cli uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks for targets
|
|
296
283
|
pal cli update Update PAL (git pull or npm update)
|
|
297
284
|
pal cli export [path] [--dry-run] Export state to zip
|
|
@@ -486,12 +473,10 @@ async function init(args: string[]) {
|
|
|
486
473
|
const { scaffoldTelos } = await import("../targets/lib");
|
|
487
474
|
|
|
488
475
|
banner();
|
|
476
|
+
narrateSteps(args.includes("--verbose"));
|
|
489
477
|
|
|
490
478
|
const health = detectAgents();
|
|
491
|
-
runDoctor([], health);
|
|
492
|
-
if (!health.hasAgent) {
|
|
493
|
-
process.exit(1);
|
|
494
|
-
}
|
|
479
|
+
if (!health.hasAgent) process.exit(runDoctor([], health));
|
|
495
480
|
|
|
496
481
|
const home = palHome();
|
|
497
482
|
log.info(`Creating PAL home at ${home}`);
|
|
@@ -506,7 +491,7 @@ async function init(args: string[]) {
|
|
|
506
491
|
|
|
507
492
|
// Auto-detect available targets
|
|
508
493
|
const targets = resolveTargets(args, health);
|
|
509
|
-
await install(targets);
|
|
494
|
+
process.exit(await install(targets, args));
|
|
510
495
|
}
|
|
511
496
|
|
|
512
497
|
/**
|
|
@@ -522,25 +507,28 @@ function runQuietly(cmd: string, args: string[], cwd: string): number | null {
|
|
|
522
507
|
return r.status;
|
|
523
508
|
}
|
|
524
509
|
|
|
525
|
-
|
|
526
|
-
|
|
510
|
+
function targetInstallers(): [keyof Targets, string, () => Promise<unknown>][] {
|
|
511
|
+
return [
|
|
512
|
+
["claude", "Claude Code", () => import("../targets/claude/install")],
|
|
513
|
+
["opencode", "opencode", () => import("../targets/opencode/install")],
|
|
514
|
+
["cursor", "Cursor", () => import("../targets/cursor/install")],
|
|
515
|
+
["copilot", "Copilot", () => import("../targets/copilot/install")],
|
|
516
|
+
["codex", "Codex", () => import("../targets/codex/install")],
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function install(targets: Targets, args: string[]): Promise<number> {
|
|
521
|
+
narrateSteps(args.includes("--verbose"));
|
|
527
522
|
const pkg = palPkg();
|
|
528
523
|
if (runQuietly("bun", ["install", "--frozen-lockfile"], pkg) !== 0) {
|
|
529
524
|
log.warn("bun install failed — continuing anyway, but hooks may not work");
|
|
530
525
|
}
|
|
531
526
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
527
|
+
const { applyPendingMigrations } = await import("./migrate");
|
|
528
|
+
for (const line of applyPendingMigrations()) log.info(line);
|
|
529
|
+
|
|
535
530
|
// Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
|
|
536
|
-
if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1")
|
|
537
|
-
const pw = runQuietly("bun", ["x", "playwright", "install", "chromium"], pkg);
|
|
538
|
-
if (pw !== 0) {
|
|
539
|
-
log.warn(
|
|
540
|
-
`playwright install chromium failed (exit ${pw}) — create-pdf and consulting-report skills won't work. Retry manually: bun x playwright install chromium`
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
531
|
+
if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") installChromium();
|
|
544
532
|
|
|
545
533
|
// Scaffold TELOS + PAL settings, then prompt for missing identity
|
|
546
534
|
const { scaffoldTelos, scaffoldPalSettings, copyPalDocs, generateSkillIndex } =
|
|
@@ -553,7 +541,6 @@ async function install(targets: Targets) {
|
|
|
553
541
|
await promptIdentity();
|
|
554
542
|
await promptAttribution();
|
|
555
543
|
await promptAutoUpdate();
|
|
556
|
-
pointAtOnboarding();
|
|
557
544
|
|
|
558
545
|
// Registers the label loadActor derives, so it travels on the next export.
|
|
559
546
|
const { ensureActorRegistered } = await import("../hooks/lib/actor");
|
|
@@ -566,34 +553,10 @@ async function install(targets: Targets) {
|
|
|
566
553
|
const palDocsCount = copyPalDocs();
|
|
567
554
|
regenerateIfNeeded();
|
|
568
555
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (targets.opencode) {
|
|
576
|
-
console.log("━━━ opencode ━━━");
|
|
577
|
-
await import("../targets/opencode/install");
|
|
578
|
-
console.log("");
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (targets.cursor) {
|
|
582
|
-
console.log("━━━ Cursor ━━━");
|
|
583
|
-
await import("../targets/cursor/install");
|
|
584
|
-
console.log("");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (targets.copilot) {
|
|
588
|
-
console.log("━━━ Copilot ━━━");
|
|
589
|
-
await import("../targets/copilot/install");
|
|
590
|
-
console.log("");
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (targets.codex) {
|
|
594
|
-
console.log("━━━ Codex ━━━");
|
|
595
|
-
await import("../targets/codex/install");
|
|
596
|
-
console.log("");
|
|
556
|
+
for (const [target, label, installTarget] of targetInstallers()) {
|
|
557
|
+
if (!targets[target]) continue;
|
|
558
|
+
log.heading(label);
|
|
559
|
+
await installTarget();
|
|
597
560
|
}
|
|
598
561
|
|
|
599
562
|
// The rest of the shared work reads what the installers just wrote: the index
|
|
@@ -608,7 +571,8 @@ async function install(targets: Targets) {
|
|
|
608
571
|
|
|
609
572
|
await refreshControlRoom();
|
|
610
573
|
|
|
611
|
-
|
|
574
|
+
if (args.includes("--verbose")) console.log("");
|
|
575
|
+
return runDoctor(args);
|
|
612
576
|
}
|
|
613
577
|
|
|
614
578
|
/**
|
|
@@ -912,7 +876,7 @@ async function update() {
|
|
|
912
876
|
clearUpdateCache();
|
|
913
877
|
|
|
914
878
|
log.info("Reinstalling...");
|
|
915
|
-
await install(resolveTargets([]));
|
|
879
|
+
process.exit(await install(resolveTargets([]), []));
|
|
916
880
|
}
|
|
917
881
|
|
|
918
882
|
function cliDebug(args: string[]) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readlinkSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { paths, platform } from "../hooks/lib/paths";
|
|
4
|
+
import { readJson, writeJson } from "../targets/lib";
|
|
5
|
+
|
|
6
|
+
/** Something an older PAL wrote that the current one neither writes nor reads. */
|
|
7
|
+
export interface Leftover {
|
|
8
|
+
id: string;
|
|
9
|
+
description: string;
|
|
10
|
+
find(): string[];
|
|
11
|
+
remove(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Json = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const claudeSettings = () => resolve(platform.claudeDir(), "settings.json");
|
|
17
|
+
const palSettings = () => resolve(paths.memory(), "pal-settings.json");
|
|
18
|
+
|
|
19
|
+
function isPathScopedFileToolRule(rule: string): boolean {
|
|
20
|
+
return /^(?:Grep|Glob)\(/.test(rule);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function claudeAllowRules(): string[] {
|
|
24
|
+
const settings = readJson<Json>(claudeSettings(), {});
|
|
25
|
+
const allow = (settings.permissions as { allow?: unknown } | undefined)?.allow;
|
|
26
|
+
return Array.isArray(allow)
|
|
27
|
+
? allow.filter((r): r is string => typeof r === "string")
|
|
28
|
+
: [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const claudeFileToolRules: Leftover = {
|
|
32
|
+
id: "claude-file-tool-rules",
|
|
33
|
+
description:
|
|
34
|
+
"Remove Grep()/Glob() allow rules older templates wrote — Claude Code ignores them and warns on every prompt",
|
|
35
|
+
find: () => claudeAllowRules().filter(isPathScopedFileToolRule),
|
|
36
|
+
remove() {
|
|
37
|
+
const settings = readJson<Json>(claudeSettings(), {});
|
|
38
|
+
const permissions = settings.permissions as { allow: string[] };
|
|
39
|
+
permissions.allow = permissions.allow.filter(
|
|
40
|
+
(rule) => !isPathScopedFileToolRule(rule)
|
|
41
|
+
);
|
|
42
|
+
writeJson(claudeSettings(), settings);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function startupFiles(): string[] {
|
|
47
|
+
const settings = readJson<Json>(palSettings(), {});
|
|
48
|
+
const files = (settings.loadAtStartup as { files?: unknown } | undefined)?.files;
|
|
49
|
+
return Array.isArray(files)
|
|
50
|
+
? files.filter((f): f is string => typeof f === "string")
|
|
51
|
+
: [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isProjectsFile = (file: string) => file.endsWith("PROJECTS.md");
|
|
55
|
+
|
|
56
|
+
const startupProjectsFile: Leftover = {
|
|
57
|
+
id: "startup-projects-md",
|
|
58
|
+
description:
|
|
59
|
+
"Stop loading PROJECTS.md at startup — projects moved to the project store",
|
|
60
|
+
find: () => startupFiles().filter(isProjectsFile),
|
|
61
|
+
remove() {
|
|
62
|
+
const settings = readJson<Json>(palSettings(), {});
|
|
63
|
+
const startup = settings.loadAtStartup as { files: string[] };
|
|
64
|
+
startup.files = startup.files.filter((file) => !isProjectsFile(file));
|
|
65
|
+
writeJson(palSettings(), settings);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const oldOpencodePlugin = () =>
|
|
70
|
+
resolve(platform.opencodeDir(), "plugins", "pai-plugin.ts");
|
|
71
|
+
|
|
72
|
+
const opencodePluginOldName: Leftover = {
|
|
73
|
+
id: "opencode-pai-plugin",
|
|
74
|
+
description: "Remove the opencode plugin under its old name, pai-plugin.ts",
|
|
75
|
+
find: () => (existsSync(oldOpencodePlugin()) ? [oldOpencodePlugin()] : []),
|
|
76
|
+
remove: () => unlinkSync(oldOpencodePlugin()),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const oldCopilotInstructions = () =>
|
|
80
|
+
resolve(platform.copilotDir(), "copilot-instructions.md");
|
|
81
|
+
|
|
82
|
+
function linksToAgentsMd(path: string): boolean {
|
|
83
|
+
try {
|
|
84
|
+
return lstatSync(path).isSymbolicLink() && readlinkSync(path).includes("AGENTS.md");
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const copilotInstructionsLink: Leftover = {
|
|
91
|
+
id: "copilot-instructions-link",
|
|
92
|
+
description:
|
|
93
|
+
"Remove the copilot-instructions.md link to AGENTS.md — Copilot now reads PAL's own instruction files",
|
|
94
|
+
find: () =>
|
|
95
|
+
linksToAgentsMd(oldCopilotInstructions()) ? [oldCopilotInstructions()] : [],
|
|
96
|
+
remove: () => unlinkSync(oldCopilotInstructions()),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const debugLog = () => resolve(paths.debug(), "debug.log");
|
|
100
|
+
|
|
101
|
+
const debugLogPrev: Leftover = {
|
|
102
|
+
id: "debug-log-prev",
|
|
103
|
+
description: "Rename the old single rotated debug log, debug.log.prev, to debug.log.1",
|
|
104
|
+
find: () => (existsSync(`${debugLog()}.prev`) ? [`${debugLog()}.prev`] : []),
|
|
105
|
+
remove() {
|
|
106
|
+
const prev = `${debugLog()}.prev`;
|
|
107
|
+
if (existsSync(`${debugLog()}.1`)) unlinkSync(prev);
|
|
108
|
+
else renameSync(prev, `${debugLog()}.1`);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const LEFTOVERS: Leftover[] = [
|
|
113
|
+
claudeFileToolRules,
|
|
114
|
+
startupProjectsFile,
|
|
115
|
+
opencodePluginOldName,
|
|
116
|
+
copilotInstructionsLink,
|
|
117
|
+
debugLogPrev,
|
|
118
|
+
];
|
package/src/cli/migrate.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pal cli migrate —
|
|
2
|
+
* pal cli migrate — the only place PAL knows about its own past.
|
|
3
3
|
*
|
|
4
4
|
* Each Migration has:
|
|
5
|
-
* check() —
|
|
6
|
-
* run() — applies
|
|
5
|
+
* check() — reads what is on disk, so it is safe to call on every install
|
|
6
|
+
* run() — applies it; user data is moved, never deleted, while leftovers
|
|
7
|
+
* PAL itself wrote and no longer reads are removed
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* pending work without running anything.
|
|
9
|
+
* Retiring a setting, hook, file or name means adding a migration that removes
|
|
10
|
+
* its leftovers. Install runs pending migrations first; the doctor lists them.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
slugify,
|
|
41
41
|
} from "../tools/knowledge/lib";
|
|
42
42
|
import { readThreads, type Thread, writeThreads } from "../tools/lib/thread";
|
|
43
|
+
import { LEFTOVERS, type Leftover } from "./leftovers";
|
|
43
44
|
|
|
44
45
|
// ── Types ─────────────────────────────────────────────────────────
|
|
45
46
|
|
|
@@ -715,6 +716,29 @@ const v7RetiredSettingsKeys: Migration = {
|
|
|
715
716
|
},
|
|
716
717
|
};
|
|
717
718
|
|
|
719
|
+
function leftoverMigration(leftover: Leftover): Migration {
|
|
720
|
+
return {
|
|
721
|
+
id: leftover.id,
|
|
722
|
+
description: leftover.description,
|
|
723
|
+
movesSources: true,
|
|
724
|
+
check() {
|
|
725
|
+
const found = leftover.find();
|
|
726
|
+
return { pending: found.length > 0, detail: found.join(", ") || undefined };
|
|
727
|
+
},
|
|
728
|
+
run(dryRun = false): MigrationResult {
|
|
729
|
+
const found = leftover.find();
|
|
730
|
+
if (found.length === 0) return { migrated: 0, skipped: 0, results: [] };
|
|
731
|
+
if (!dryRun) leftover.remove();
|
|
732
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
733
|
+
return {
|
|
734
|
+
migrated: found.length,
|
|
735
|
+
skipped: 0,
|
|
736
|
+
results: [`${verb} ${found.join(", ")}`],
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
718
742
|
const MIGRATIONS: Migration[] = [
|
|
719
743
|
v1Projects,
|
|
720
744
|
v2ThreadsToIsc,
|
|
@@ -723,6 +747,7 @@ const MIGRATIONS: Migration[] = [
|
|
|
723
747
|
v5AttributionKeys,
|
|
724
748
|
v6HistorySlugs,
|
|
725
749
|
v7RetiredSettingsKeys,
|
|
750
|
+
...LEFTOVERS.map(leftoverMigration),
|
|
726
751
|
];
|
|
727
752
|
|
|
728
753
|
// ── Public API ────────────────────────────────────────────────────
|
|
@@ -733,6 +758,13 @@ interface PendingMigration {
|
|
|
733
758
|
detail?: string;
|
|
734
759
|
}
|
|
735
760
|
|
|
761
|
+
/** Install runs these first, so one install leaves nothing an older PAL wrote. */
|
|
762
|
+
export function applyPendingMigrations(): string[] {
|
|
763
|
+
return MIGRATIONS.filter((m) => m.check().pending).flatMap((m) =>
|
|
764
|
+
m.run(false).results.map((line) => `${m.id}: ${line}`)
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
736
768
|
/** Returns migrations that have pending work. Used by doctor. */
|
|
737
769
|
export function checkPendingMigrations(): PendingMigration[] {
|
|
738
770
|
return MIGRATIONS.flatMap((m) => {
|
package/src/hooks/lib/log.ts
CHANGED
|
@@ -36,22 +36,10 @@ function timestamp(): string {
|
|
|
36
36
|
* .MAX_ROTATED (oldest). Each rotation shifts .N-1 → .N, drops the oldest,
|
|
37
37
|
* and renames the current log to .1. Total disk footprint bounded at
|
|
38
38
|
* (MAX_ROTATED + 1) * MAX_LOG_SIZE ≈ 300KB.
|
|
39
|
-
*
|
|
40
|
-
* Migrates legacy .prev → .1 on first new rotation so existing history
|
|
41
|
-
* survives the format change.
|
|
42
39
|
*/
|
|
43
40
|
function rotateIfNeeded(path: string): void {
|
|
44
41
|
try {
|
|
45
42
|
if (!existsSync(path) || statSync(path).size <= MAX_LOG_SIZE) return;
|
|
46
|
-
// Backward-compat migration: legacy .prev was the single old rotation file.
|
|
47
|
-
const legacyPrev = `${path}.prev`;
|
|
48
|
-
if (existsSync(legacyPrev) && !existsSync(`${path}.1`)) {
|
|
49
|
-
try {
|
|
50
|
-
renameSync(legacyPrev, `${path}.1`);
|
|
51
|
-
} catch {
|
|
52
|
-
/* ignore */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
43
|
// Drop the oldest, then shift .N-1 → .N for N from MAX_ROTATED down to 2.
|
|
56
44
|
const oldest = `${path}.${MAX_ROTATED}`;
|
|
57
45
|
if (existsSync(oldest)) {
|
|
@@ -99,7 +87,7 @@ function logFilesOldestFirst(): string[] {
|
|
|
99
87
|
{ length: MAX_ROTATED },
|
|
100
88
|
(_, i) => `${path}.${MAX_ROTATED - i}`
|
|
101
89
|
);
|
|
102
|
-
return [...rotated,
|
|
90
|
+
return [...rotated, path];
|
|
103
91
|
}
|
|
104
92
|
|
|
105
93
|
function readAllLogs(): string {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and the VS Code chat.instructionsFilesLocations entry.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { copyFileSync, existsSync,
|
|
7
|
+
import { copyFileSync, existsSync, unlinkSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { platform } from "../../hooks/lib/paths";
|
|
10
10
|
import {
|
|
@@ -63,22 +63,6 @@ log.success(
|
|
|
63
63
|
`Removed ${removedInstructions.length} ~/.copilot/instructions/pal-*.instructions.md`
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
// --- Backward compat: remove old copilot-instructions.md symlink if present ---
|
|
67
|
-
const legacyPath = resolve(COPILOT_DIR, "copilot-instructions.md");
|
|
68
|
-
if (existsSync(legacyPath)) {
|
|
69
|
-
try {
|
|
70
|
-
if (
|
|
71
|
-
lstatSync(legacyPath).isSymbolicLink() &&
|
|
72
|
-
readlinkSync(legacyPath).includes("AGENTS.md")
|
|
73
|
-
) {
|
|
74
|
-
unlinkSync(legacyPath);
|
|
75
|
-
log.success("Removed legacy copilot-instructions.md symlink");
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
/* ignore */
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
66
|
// --- Remove ~/.copilot/instructions entry from VS Code settings ---
|
|
83
67
|
const vsSettingsPath = vscodeSettingsFile();
|
|
84
68
|
if (vsSettingsPath && existsSync(vsSettingsPath)) {
|
package/src/targets/lib.ts
CHANGED
|
@@ -27,9 +27,21 @@ function runningUnderTest(): boolean {
|
|
|
27
27
|
return process.env.PAL_TEST_SANDBOX === "1";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
let narrating = true;
|
|
31
|
+
|
|
32
|
+
/** Install's step log is for --verbose; the doctor report that follows carries the result. */
|
|
33
|
+
export function narrateSteps(on: boolean): void {
|
|
34
|
+
narrating = on;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function narrate(line: string): void {
|
|
38
|
+
if (narrating) console.log(line);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
export const log = {
|
|
31
|
-
info: (msg: string) =>
|
|
32
|
-
success: (msg: string) =>
|
|
42
|
+
info: (msg: string) => narrate(`\x1b[34m[pal]\x1b[0m ${msg}`),
|
|
43
|
+
success: (msg: string) => narrate(`\x1b[32m[pal]\x1b[0m ${msg}`),
|
|
44
|
+
heading: (label: string) => narrate(`\n━━━ ${label} ━━━`),
|
|
33
45
|
warn: (msg: string) => console.log(`\x1b[33m[pal]\x1b[0m ${msg}`),
|
|
34
46
|
error: (msg: string) => console.error(`\x1b[31m[pal]\x1b[0m ${msg}`),
|
|
35
47
|
|
|
@@ -42,7 +54,7 @@ export const log = {
|
|
|
42
54
|
*/
|
|
43
55
|
detail: (msg: string) => {
|
|
44
56
|
if (runningUnderTest()) return;
|
|
45
|
-
|
|
57
|
+
narrate(`\x1b[34m[pal]\x1b[0m ${msg}`);
|
|
46
58
|
},
|
|
47
59
|
};
|
|
48
60
|
|
|
@@ -199,7 +211,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
// Merge permissions.allow (deduplicate)
|
|
214
|
+
// Merge permissions.allow (deduplicate)
|
|
203
215
|
if (template.permissions?.allow) {
|
|
204
216
|
result.permissions ??= {};
|
|
205
217
|
result.permissions.allow ??= [];
|
|
@@ -209,15 +221,6 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
209
221
|
}
|
|
210
222
|
}
|
|
211
223
|
}
|
|
212
|
-
// Strip ineffective Grep(...)/Glob(...) rules left by older templates. Claude Code
|
|
213
|
-
// governs Grep/Glob via Read(...) rules, so a path-scoped Grep()/Glob() entry never
|
|
214
|
-
// matches and Claude Code warns about it on every prompt. Read(//*) already covers them.
|
|
215
|
-
if (result.permissions?.allow) {
|
|
216
|
-
result.permissions.allow = result.permissions.allow.filter(
|
|
217
|
-
(perm) => !isIneffectiveFileToolRule(perm)
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
224
|
// Merge skillOverrides (object with skill name keys, add if not present)
|
|
222
225
|
if (template.skillOverrides && typeof template.skillOverrides === "object") {
|
|
223
226
|
result.skillOverrides ??= {};
|
|
@@ -476,15 +479,6 @@ function isOutdatedPalHook(cmd: string, palCanonical: Set<string>): boolean {
|
|
|
476
479
|
return palCanonical.has(canonicalPalCmd(cmd)) || runsRemovedPalHook(cmd);
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
/**
|
|
480
|
-
* True for path-scoped Grep()/Glob() allow rules, which Claude Code cannot honor —
|
|
481
|
-
* it resolves Grep/Glob permission through Read(...) rules, so these never match and
|
|
482
|
-
* trigger a warning on every prompt. Bare "Grep"/"Glob" tool allows are left intact.
|
|
483
|
-
*/
|
|
484
|
-
function isIneffectiveFileToolRule(perm: string): boolean {
|
|
485
|
-
return /^(?:Grep|Glob)\(/.test(perm);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
482
|
export function loadCodexHooksTemplate(
|
|
489
483
|
templatePath: string,
|
|
490
484
|
pkgRoot: string
|
|
@@ -685,21 +679,6 @@ export function scaffoldPalSettings(): void {
|
|
|
685
679
|
copyFileSync(src, dst);
|
|
686
680
|
log.info("Created pal-settings.json from template");
|
|
687
681
|
}
|
|
688
|
-
|
|
689
|
-
// Strip deprecated loadAtStartup.files entries from existing installs.
|
|
690
|
-
// mergeSettings only adds, never removes — deprecated entries persist indefinitely otherwise.
|
|
691
|
-
try {
|
|
692
|
-
const raw = JSON.parse(readFileSync(dst, "utf-8"));
|
|
693
|
-
const files: string[] = raw?.loadAtStartup?.files ?? [];
|
|
694
|
-
const cleaned = files.filter((f: string) => !f.endsWith("PROJECTS.md"));
|
|
695
|
-
if (cleaned.length !== files.length) {
|
|
696
|
-
raw.loadAtStartup.files = cleaned;
|
|
697
|
-
writeFileSync(dst, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
698
|
-
log.info("Removed deprecated PROJECTS.md from loadAtStartup.files");
|
|
699
|
-
}
|
|
700
|
-
} catch {
|
|
701
|
-
/* non-fatal — malformed settings left as-is */
|
|
702
|
-
}
|
|
703
682
|
}
|
|
704
683
|
|
|
705
684
|
// --- PAL docs (modular context routing files) ---
|
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
* Deploys plugin, installs skills, generates AGENTS.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readFileSync,
|
|
10
|
-
statSync,
|
|
11
|
-
unlinkSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
14
7
|
import { resolve } from "node:path";
|
|
15
8
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
16
9
|
import { getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
@@ -22,11 +15,7 @@ const OC_PLUGINS_DIR = resolve(OC_GLOBAL_DIR, "plugins");
|
|
|
22
15
|
|
|
23
16
|
mkdirSync(OC_PLUGINS_DIR, { recursive: true });
|
|
24
17
|
|
|
25
|
-
// --- 1. Deploy plugin
|
|
26
|
-
const legacyPlugin = resolve(OC_PLUGINS_DIR, "pai-plugin.ts");
|
|
27
|
-
if (existsSync(legacyPlugin)) {
|
|
28
|
-
unlinkSync(legacyPlugin);
|
|
29
|
-
}
|
|
18
|
+
// --- 1. Deploy plugin ---
|
|
30
19
|
const pluginSrc = resolve(PKG_ROOT, "src", "targets", "opencode", "plugin.ts");
|
|
31
20
|
const pluginDst = resolve(OC_PLUGINS_DIR, "pal-plugin.ts");
|
|
32
21
|
// Embed PKG_ROOT as a hardcoded constant so no env config is needed
|