portable-agent-layer 0.85.15 → 0.85.17
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/dependencies.ts +11 -0
- package/src/cli/doctor/agents.ts +5 -2
- package/src/cli/doctor/environment.ts +25 -2
- package/src/cli/doctor/finding.ts +4 -4
- package/src/cli/doctor/health.ts +4 -1
- package/src/cli/doctor/inference.ts +1 -0
- package/src/cli/doctor/state.ts +5 -0
- package/src/cli/doctor/version-control.ts +9 -6
- package/src/cli/index.ts +7 -1
- 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 +1 -34
- package/src/targets/opencode/install.ts +2 -13
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { unresolvedDependencies } from "./doctor/state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `bun install` arguments that make PAL's dependencies resolve, or null when
|
|
5
|
+
* they already do. A published package ships no lockfile, and `bun add -g` has
|
|
6
|
+
* already installed its dependencies next to it.
|
|
7
|
+
*/
|
|
8
|
+
export function dependencyInstall(pkg: string, repoMode: boolean): string[] | null {
|
|
9
|
+
if (repoMode) return ["install", "--frozen-lockfile"];
|
|
10
|
+
return unresolvedDependencies(pkg).length > 0 ? ["install", "--production"] : null;
|
|
11
|
+
}
|
package/src/cli/doctor/agents.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
5
5
|
import { NO_SESSION_AGENT_MESSAGE } from "../session-agent";
|
|
6
|
-
import { type Finding, failing, optional, passed, warning } from "./finding";
|
|
6
|
+
import { type Finding, type Fix, failing, optional, passed, warning } from "./finding";
|
|
7
7
|
|
|
8
8
|
export type AgentName = "claude" | "opencode" | "cursor" | "copilot" | "codex";
|
|
9
9
|
|
|
@@ -139,9 +139,10 @@ const LAYOUT: Record<AgentName, AgentLayout> = {
|
|
|
139
139
|
},
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
-
const reinstall = (agent: AgentName) => ({
|
|
142
|
+
const reinstall = (agent: AgentName): Fix => ({
|
|
143
143
|
say: `Reinstall PAL for ${LAYOUT[agent].label}`,
|
|
144
144
|
command: `pal cli install --${agent}`,
|
|
145
|
+
external: false,
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
/** Hook-config keys that carry a shell command: cross-platform and per-shell variants. */
|
|
@@ -215,6 +216,7 @@ function hookFindings(agent: AgentName, file: string): Finding[] {
|
|
|
215
216
|
{
|
|
216
217
|
say: `Fix the JSON in ${file}, then reinstall`,
|
|
217
218
|
command: `pal cli install --${agent}`,
|
|
219
|
+
external: false,
|
|
218
220
|
}
|
|
219
221
|
),
|
|
220
222
|
];
|
|
@@ -310,6 +312,7 @@ export function agentFindings(agents: AgentName[]): Finding[] {
|
|
|
310
312
|
: failing("agents-md", "AGENTS.md is missing", {
|
|
311
313
|
say: "Reinstall PAL",
|
|
312
314
|
command: "pal cli install",
|
|
315
|
+
external: false,
|
|
313
316
|
}),
|
|
314
317
|
...agents.flatMap(oneAgentFindings),
|
|
315
318
|
];
|
|
@@ -83,6 +83,7 @@ export function playwrightFinding(host: BrowserHost): Finding {
|
|
|
83
83
|
? "Install it as Ubuntu 24.04, which Playwright supports"
|
|
84
84
|
: "Install it",
|
|
85
85
|
command: chromiumInstallCommand(host),
|
|
86
|
+
external: false,
|
|
86
87
|
}
|
|
87
88
|
);
|
|
88
89
|
}
|
|
@@ -113,15 +114,34 @@ interface PalInstall {
|
|
|
113
114
|
pal: string | null;
|
|
114
115
|
pkg: string;
|
|
115
116
|
repoMode: boolean;
|
|
117
|
+
bunBin: string;
|
|
118
|
+
inBunBin: boolean;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
function putPalOnPath(install: PalInstall): Fix {
|
|
122
|
+
if (install.inBunBin)
|
|
123
|
+
return {
|
|
124
|
+
say: `Add ${install.bunBin} to PATH in your shell profile — Bun put pal there`,
|
|
125
|
+
};
|
|
119
126
|
return install.repoMode
|
|
120
127
|
? {
|
|
121
128
|
say: "Link the checkout — a shell alias is invisible to agents",
|
|
122
129
|
command: `cd ${install.pkg} && bun link`,
|
|
130
|
+
external: false,
|
|
123
131
|
}
|
|
124
|
-
: {
|
|
132
|
+
: {
|
|
133
|
+
say: "Install PAL globally",
|
|
134
|
+
command: "bun add -g portable-agent-layer",
|
|
135
|
+
external: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function bunBin(): string {
|
|
140
|
+
return resolve(process.env.BUN_INSTALL ?? resolve(homedir(), ".bun"), "bin");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isInBunBin(): boolean {
|
|
144
|
+
return ["pal", "pal.exe"].some((name) => existsSync(resolve(bunBin(), name)));
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
export function palOnPathFinding(install: PalInstall): Finding {
|
|
@@ -140,11 +160,12 @@ function rtkInstall(): Fix {
|
|
|
140
160
|
say: "Download rtk.exe from https://github.com/rtk-ai/rtk/releases and add it to PATH",
|
|
141
161
|
};
|
|
142
162
|
if (process.platform === "darwin")
|
|
143
|
-
return { say: "Install rtk", command: "brew install rtk" };
|
|
163
|
+
return { say: "Install rtk", command: "brew install rtk", external: true };
|
|
144
164
|
return {
|
|
145
165
|
say: "Install rtk",
|
|
146
166
|
command:
|
|
147
167
|
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh",
|
|
168
|
+
external: true,
|
|
148
169
|
};
|
|
149
170
|
}
|
|
150
171
|
|
|
@@ -161,6 +182,8 @@ export function environmentFindings(rtk: ToolCheck): Finding[] {
|
|
|
161
182
|
pal: findBinaryOnPath("pal"),
|
|
162
183
|
pkg: palPkg(),
|
|
163
184
|
repoMode: isRepoMode(),
|
|
185
|
+
bunBin: bunBin(),
|
|
186
|
+
inBunBin: isInBunBin(),
|
|
164
187
|
}),
|
|
165
188
|
playwrightFinding(thisBrowserHost()),
|
|
166
189
|
rtkFinding(rtk),
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
export type Severity = "fail" | "warn" | "optional" | "ok";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
command?:
|
|
12
|
-
}
|
|
9
|
+
/** `external`: the command belongs to another tool, so PAL leaves running it to you. */
|
|
10
|
+
export type Fix =
|
|
11
|
+
| { say: string; command?: undefined }
|
|
12
|
+
| { say: string; command: string; external: boolean };
|
|
13
13
|
|
|
14
14
|
export interface Finding {
|
|
15
15
|
id: string;
|
package/src/cli/doctor/health.ts
CHANGED
|
@@ -33,10 +33,12 @@ function claudeLoginExpiredFinding(
|
|
|
33
33
|
return warning(`hook-errors.${group.source}`, title, {
|
|
34
34
|
say: "CLAUDE_CODE_OAUTH_TOKEN is set — if these failures predate it, they clear 24h after the last one; if not, the token is invalid, so create a new one",
|
|
35
35
|
command: "claude setup-token",
|
|
36
|
+
external: true,
|
|
36
37
|
});
|
|
37
38
|
return failing(`hook-errors.${group.source}`, title, {
|
|
38
39
|
say: "Create a year-long token, then export it as CLAUDE_CODE_OAUTH_TOKEN in your shell profile",
|
|
39
40
|
command: "claude setup-token",
|
|
41
|
+
external: true,
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -66,7 +68,7 @@ export function migrationFindings(pending: PendingMigration[]): Finding[] {
|
|
|
66
68
|
return warning(
|
|
67
69
|
`migration.${migration.id}`,
|
|
68
70
|
`Migration pending: ${migration.description}${detail}`,
|
|
69
|
-
{ say: "Run it", command: "pal cli migrate" }
|
|
71
|
+
{ say: "Run it", command: "pal cli migrate", external: false }
|
|
70
72
|
);
|
|
71
73
|
});
|
|
72
74
|
}
|
|
@@ -76,6 +78,7 @@ export function updateFinding(cache: UpdateCache | null): Finding {
|
|
|
76
78
|
return warning("update", `PAL ${cache.latest} is out (${cache.current} installed)`, {
|
|
77
79
|
say: "Update",
|
|
78
80
|
command: "pal cli update",
|
|
81
|
+
external: false,
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -34,6 +34,7 @@ export function leakedEnvFindings(env: Env, os: NodeJS.Platform): Finding[] {
|
|
|
34
34
|
{
|
|
35
35
|
say: "Clear it, and remove it from your shell profile if it is set there",
|
|
36
36
|
command: unsetCommand(name, os),
|
|
37
|
+
external: true,
|
|
37
38
|
}
|
|
38
39
|
)
|
|
39
40
|
: passed(`env.${name}`, `${name} not set`)
|
package/src/cli/doctor/state.ts
CHANGED
|
@@ -95,6 +95,7 @@ function identityFinding(settings: Record<string, unknown>): Finding {
|
|
|
95
95
|
return warning("identity", "Identity is incomplete — PAL does not know both names", {
|
|
96
96
|
say: "Answer the identity questions",
|
|
97
97
|
command: "pal cli install",
|
|
98
|
+
external: false,
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -120,6 +121,7 @@ function settingsFindings(): Finding[] {
|
|
|
120
121
|
warning("settings.missing", "pal-settings.json is missing", {
|
|
121
122
|
say: "Recreate it",
|
|
122
123
|
command: "pal cli install",
|
|
124
|
+
external: false,
|
|
123
125
|
}),
|
|
124
126
|
];
|
|
125
127
|
let settings: unknown;
|
|
@@ -165,6 +167,7 @@ function telosFindings(home: string): Finding[] {
|
|
|
165
167
|
failing("telos.missing", "TELOS is not scaffolded", {
|
|
166
168
|
say: "Scaffold it",
|
|
167
169
|
command: "pal cli init",
|
|
170
|
+
external: false,
|
|
168
171
|
}),
|
|
169
172
|
];
|
|
170
173
|
const unanswered = telosStatus(home)
|
|
@@ -205,6 +208,7 @@ export function bindingFinding(issue: BindingIssue): Finding {
|
|
|
205
208
|
return warning(`binding.${project}`, title, {
|
|
206
209
|
say: "Point it at its checkout",
|
|
207
210
|
command: `pal cli project set-path ${project} <path>`,
|
|
211
|
+
external: false,
|
|
208
212
|
});
|
|
209
213
|
}
|
|
210
214
|
|
|
@@ -250,6 +254,7 @@ function dependencyFinding(): Finding {
|
|
|
250
254
|
{
|
|
251
255
|
say: "Install them",
|
|
252
256
|
command: "pal cli install",
|
|
257
|
+
external: false,
|
|
253
258
|
}
|
|
254
259
|
);
|
|
255
260
|
}
|
|
@@ -30,22 +30,24 @@ const GH_ON_LINUX: Partial<Record<LinuxFamily, string>> = {
|
|
|
30
30
|
|
|
31
31
|
export function gitInstallHint(platform: NodeJS.Platform, osRelease: string): Fix {
|
|
32
32
|
if (platform === "win32")
|
|
33
|
-
return { say: "Install git", command: "winget install Git.Git" };
|
|
34
|
-
if (platform === "darwin")
|
|
33
|
+
return { say: "Install git", command: "winget install Git.Git", external: true };
|
|
34
|
+
if (platform === "darwin")
|
|
35
|
+
return { say: "Install git", command: "brew install git", external: true };
|
|
35
36
|
const family = linuxFamily(osRelease);
|
|
36
37
|
return family
|
|
37
|
-
? { say: "Install git", command: GIT_ON_LINUX[family] }
|
|
38
|
+
? { say: "Install git", command: GIT_ON_LINUX[family], external: true }
|
|
38
39
|
: { say: "Install git with your package manager" };
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export function ghInstallHint(platform: NodeJS.Platform, osRelease: string): Fix {
|
|
42
43
|
if (platform === "win32")
|
|
43
|
-
return { say: "Install gh", command: "winget install GitHub.cli" };
|
|
44
|
-
if (platform === "darwin")
|
|
44
|
+
return { say: "Install gh", command: "winget install GitHub.cli", external: true };
|
|
45
|
+
if (platform === "darwin")
|
|
46
|
+
return { say: "Install gh", command: "brew install gh", external: true };
|
|
45
47
|
const family = linuxFamily(osRelease);
|
|
46
48
|
const command = family ? GH_ON_LINUX[family] : undefined;
|
|
47
49
|
return command
|
|
48
|
-
? { say: "Install gh", command }
|
|
50
|
+
? { say: "Install gh", command, external: true }
|
|
49
51
|
: { say: `Install gh as described at ${GH_LINUX_DOCS}` };
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -94,6 +96,7 @@ function ghFinding(host: Host): Finding {
|
|
|
94
96
|
return warning("gh.logged-out", `${gh.version} is not logged in`, {
|
|
95
97
|
say: "Log in",
|
|
96
98
|
command: "gh auth login",
|
|
99
|
+
external: true,
|
|
97
100
|
});
|
|
98
101
|
return passed("gh", `${gh.version}, logged in`);
|
|
99
102
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -520,10 +520,16 @@ function targetInstallers(): [keyof Targets, string, () => Promise<unknown>][] {
|
|
|
520
520
|
async function install(targets: Targets, args: string[]): Promise<number> {
|
|
521
521
|
narrateSteps(args.includes("--verbose"));
|
|
522
522
|
const pkg = palPkg();
|
|
523
|
-
|
|
523
|
+
const { dependencyInstall } = await import("./dependencies");
|
|
524
|
+
const { isRepoMode } = await import("../hooks/handlers/update-check");
|
|
525
|
+
const bunInstall = dependencyInstall(pkg, isRepoMode());
|
|
526
|
+
if (bunInstall && runQuietly("bun", bunInstall, pkg) !== 0) {
|
|
524
527
|
log.warn("bun install failed — continuing anyway, but hooks may not work");
|
|
525
528
|
}
|
|
526
529
|
|
|
530
|
+
const { applyPendingMigrations } = await import("./migrate");
|
|
531
|
+
for (const line of applyPendingMigrations()) log.info(line);
|
|
532
|
+
|
|
527
533
|
// Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
|
|
528
534
|
if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") installChromium();
|
|
529
535
|
|
|
@@ -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
|
@@ -211,7 +211,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// Merge permissions.allow (deduplicate)
|
|
214
|
+
// Merge permissions.allow (deduplicate)
|
|
215
215
|
if (template.permissions?.allow) {
|
|
216
216
|
result.permissions ??= {};
|
|
217
217
|
result.permissions.allow ??= [];
|
|
@@ -221,15 +221,6 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
// Strip ineffective Grep(...)/Glob(...) rules left by older templates. Claude Code
|
|
225
|
-
// governs Grep/Glob via Read(...) rules, so a path-scoped Grep()/Glob() entry never
|
|
226
|
-
// matches and Claude Code warns about it on every prompt. Read(//*) already covers them.
|
|
227
|
-
if (result.permissions?.allow) {
|
|
228
|
-
result.permissions.allow = result.permissions.allow.filter(
|
|
229
|
-
(perm) => !isIneffectiveFileToolRule(perm)
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
224
|
// Merge skillOverrides (object with skill name keys, add if not present)
|
|
234
225
|
if (template.skillOverrides && typeof template.skillOverrides === "object") {
|
|
235
226
|
result.skillOverrides ??= {};
|
|
@@ -488,15 +479,6 @@ function isOutdatedPalHook(cmd: string, palCanonical: Set<string>): boolean {
|
|
|
488
479
|
return palCanonical.has(canonicalPalCmd(cmd)) || runsRemovedPalHook(cmd);
|
|
489
480
|
}
|
|
490
481
|
|
|
491
|
-
/**
|
|
492
|
-
* True for path-scoped Grep()/Glob() allow rules, which Claude Code cannot honor —
|
|
493
|
-
* it resolves Grep/Glob permission through Read(...) rules, so these never match and
|
|
494
|
-
* trigger a warning on every prompt. Bare "Grep"/"Glob" tool allows are left intact.
|
|
495
|
-
*/
|
|
496
|
-
function isIneffectiveFileToolRule(perm: string): boolean {
|
|
497
|
-
return /^(?:Grep|Glob)\(/.test(perm);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
482
|
export function loadCodexHooksTemplate(
|
|
501
483
|
templatePath: string,
|
|
502
484
|
pkgRoot: string
|
|
@@ -697,21 +679,6 @@ export function scaffoldPalSettings(): void {
|
|
|
697
679
|
copyFileSync(src, dst);
|
|
698
680
|
log.info("Created pal-settings.json from template");
|
|
699
681
|
}
|
|
700
|
-
|
|
701
|
-
// Strip deprecated loadAtStartup.files entries from existing installs.
|
|
702
|
-
// mergeSettings only adds, never removes — deprecated entries persist indefinitely otherwise.
|
|
703
|
-
try {
|
|
704
|
-
const raw = JSON.parse(readFileSync(dst, "utf-8"));
|
|
705
|
-
const files: string[] = raw?.loadAtStartup?.files ?? [];
|
|
706
|
-
const cleaned = files.filter((f: string) => !f.endsWith("PROJECTS.md"));
|
|
707
|
-
if (cleaned.length !== files.length) {
|
|
708
|
-
raw.loadAtStartup.files = cleaned;
|
|
709
|
-
writeFileSync(dst, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
710
|
-
log.info("Removed deprecated PROJECTS.md from loadAtStartup.files");
|
|
711
|
-
}
|
|
712
|
-
} catch {
|
|
713
|
-
/* non-fatal — malformed settings left as-is */
|
|
714
|
-
}
|
|
715
682
|
}
|
|
716
683
|
|
|
717
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
|