@bigking67/pi-67 0.10.6 → 0.10.8
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/CHANGELOG.md +16 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/schemas/pi67-state.schema.json +26 -0
- package/scripts/check.mjs +87 -2
- package/src/commands/doctor.mjs +1 -1
- package/src/commands/install.mjs +20 -1
- package/src/commands/skills.mjs +11 -6
- package/src/commands/status.mjs +5 -1
- package/src/commands/update.mjs +30 -3
- package/src/commands/version.mjs +79 -2
- package/src/lib/settings-runtime-clean.mjs +20 -0
- package/src/lib/settings-runtime-state.mjs +167 -0
- package/src/lib/skill-policy.mjs +5 -3
- package/src/lib/state-store.mjs +14 -2
- package/src/lib/update-plan.mjs +27 -6
- package/src/tools/settings-runtime-state-filter.mjs +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.8]
|
|
4
|
+
|
|
5
|
+
- Adds actionable `pi-67 version` recommendations when the npm manager has
|
|
6
|
+
been updated but the local distro checkout is still old or `settings.json`
|
|
7
|
+
still has runtime-marker dirty state.
|
|
8
|
+
|
|
9
|
+
## [0.10.7]
|
|
10
|
+
|
|
11
|
+
- Migrates `settings.json.lastChangelogVersion` into ignored manager state at
|
|
12
|
+
`~/.pi/pi67/state.json`, normalizes the runtime-only marker out of
|
|
13
|
+
`settings.json`, and installs a local Git clean filter so future marker
|
|
14
|
+
writes cannot be carried into normal diffs or commits.
|
|
15
|
+
- Renames shared-skill drift in human-facing output from "conflicts" to
|
|
16
|
+
"preserved user-modified" while keeping the JSON `conflicts` compatibility
|
|
17
|
+
field.
|
|
18
|
+
|
|
3
19
|
## [0.10.6]
|
|
4
20
|
|
|
5
21
|
- Adds runtime request retry to the canonical `xtalpi-pi-tools` provider path,
|
package/README.md
CHANGED
|
@@ -129,8 +129,9 @@ The explicit theme setter also writes a runtime backup before changing
|
|
|
129
129
|
`settings.json`; normal update never changes the selected theme.
|
|
130
130
|
|
|
131
131
|
The manager writes lightweight state outside the repo at `~/.pi/pi67/state.json`.
|
|
132
|
-
It records versions, paths, theme, provider/model,
|
|
133
|
-
|
|
132
|
+
It records versions, paths, theme, provider/model, commit information, and
|
|
133
|
+
runtime-only UI markers such as `settings.json.lastChangelogVersion` after
|
|
134
|
+
migrating them out of tracked config; it does not store API keys.
|
|
134
135
|
|
|
135
136
|
## Main commands
|
|
136
137
|
|
package/package.json
CHANGED
|
@@ -25,6 +25,32 @@
|
|
|
25
25
|
},
|
|
26
26
|
"lastTheme": {
|
|
27
27
|
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
"runtimeMarkers": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"description": "Runtime-only UI markers migrated out of tracked runtime config files.",
|
|
32
|
+
"properties": {
|
|
33
|
+
"lastChangelogVersion": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["value", "storage"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"value": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"source": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"storage": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"updatedAt": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"additionalProperties": true
|
|
28
54
|
}
|
|
29
55
|
},
|
|
30
56
|
"additionalProperties": true
|
package/scripts/check.mjs
CHANGED
|
@@ -8,6 +8,11 @@ import { commandCandidatesForPlatform } from "../src/lib/shell-runner.mjs";
|
|
|
8
8
|
import { parseCommandOptions, splitGlobalArgs } from "../src/lib/args.mjs";
|
|
9
9
|
import { readExtensionRegistry, validateExtensionRegistry } from "../src/lib/extension-registry.mjs";
|
|
10
10
|
import { buildPlanDecisions, classifyGitShort } from "../src/lib/update-plan.mjs";
|
|
11
|
+
import {
|
|
12
|
+
mergeSettingsRuntimeMarkerIntoState,
|
|
13
|
+
settingsRuntimeMarkerFromObject,
|
|
14
|
+
stripSettingsRuntimeMarkerText,
|
|
15
|
+
} from "../src/lib/settings-runtime-state.mjs";
|
|
11
16
|
import {
|
|
12
17
|
beginUpdateLifecycle,
|
|
13
18
|
inspectLegacyConflictBackup,
|
|
@@ -32,9 +37,11 @@ for (const file of files.filter((item) => item.endsWith(".json"))) {
|
|
|
32
37
|
await runNpmRegistrySelfTests();
|
|
33
38
|
runArgsSelfTests();
|
|
34
39
|
runCliHelpContractSelfTests();
|
|
40
|
+
runVersionRecommendationSelfTests();
|
|
35
41
|
runPublishTargetSelfTests();
|
|
36
42
|
runShellRunnerSelfTests();
|
|
37
43
|
runExtensionRegistrySelfTests();
|
|
44
|
+
runSettingsRuntimeStateSelfTests();
|
|
38
45
|
runUpdatePlanSelfTests();
|
|
39
46
|
runUpdateSafetySelfTests();
|
|
40
47
|
|
|
@@ -116,6 +123,68 @@ function runCliHelpContractSelfTests() {
|
|
|
116
123
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
function runVersionRecommendationSelfTests() {
|
|
127
|
+
if (spawnSync("git", ["--version"], { encoding: "utf8" }).status !== 0) return;
|
|
128
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-version-recommendation-"));
|
|
129
|
+
const home = path.join(tmpRoot, "home");
|
|
130
|
+
const repo = path.join(tmpRoot, "agent");
|
|
131
|
+
fs.mkdirSync(home, { recursive: true });
|
|
132
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
133
|
+
fs.writeFileSync(path.join(repo, "VERSION"), "0.10.0\n");
|
|
134
|
+
fs.writeFileSync(path.join(repo, "settings.json"), "{\n \"lastChangelogVersion\": \"0.80.3\",\n \"theme\": \"gruvbox-dark\"\n}\n");
|
|
135
|
+
for (const args of [
|
|
136
|
+
["-C", repo, "init", "-q"],
|
|
137
|
+
["-C", repo, "config", "user.email", "pi67-check@example.invalid"],
|
|
138
|
+
["-C", repo, "config", "user.name", "pi67-check"],
|
|
139
|
+
["-C", repo, "add", "VERSION", "settings.json"],
|
|
140
|
+
["-C", repo, "commit", "-q", "-m", "init"],
|
|
141
|
+
]) {
|
|
142
|
+
const result = spawnSync("git", args, { encoding: "utf8" });
|
|
143
|
+
assert(result.status === 0, `git setup failed for version recommendation self-test: git ${args.join(" ")}\n${result.stderr}`);
|
|
144
|
+
}
|
|
145
|
+
fs.writeFileSync(path.join(repo, "settings.json"), "{\n \"lastChangelogVersion\": \"0.80.4\",\n \"theme\": \"gruvbox-dark\"\n}\n");
|
|
146
|
+
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
|
147
|
+
const result = spawnSync(process.execPath, [
|
|
148
|
+
path.join(root, "bin", "pi-67.mjs"),
|
|
149
|
+
"--agent-dir",
|
|
150
|
+
repo,
|
|
151
|
+
"--repo-root",
|
|
152
|
+
repo,
|
|
153
|
+
"version",
|
|
154
|
+
], {
|
|
155
|
+
cwd: root,
|
|
156
|
+
env,
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
});
|
|
159
|
+
assert(result.status === 0, `version recommendation command failed\n${result.stderr || result.stdout}`);
|
|
160
|
+
assert(
|
|
161
|
+
result.stdout.includes("npm install updated only the manager package") &&
|
|
162
|
+
result.stdout.includes("update --repair") &&
|
|
163
|
+
result.stdout.includes("settings.json has Pi runtime changelog marker state"),
|
|
164
|
+
`version output must explain manager/distro mismatch and runtime marker repair\n${result.stdout}`,
|
|
165
|
+
);
|
|
166
|
+
const json = spawnSync(process.execPath, [
|
|
167
|
+
path.join(root, "bin", "pi-67.mjs"),
|
|
168
|
+
"--agent-dir",
|
|
169
|
+
repo,
|
|
170
|
+
"--repo-root",
|
|
171
|
+
repo,
|
|
172
|
+
"version",
|
|
173
|
+
"--json",
|
|
174
|
+
], {
|
|
175
|
+
cwd: root,
|
|
176
|
+
env,
|
|
177
|
+
encoding: "utf8",
|
|
178
|
+
});
|
|
179
|
+
assert(json.status === 0, `version --json recommendation command failed\n${json.stderr || json.stdout}`);
|
|
180
|
+
const parsed = JSON.parse(json.stdout);
|
|
181
|
+
assert(
|
|
182
|
+
parsed.recommendations?.some((item) => item.message.includes("update --repair")),
|
|
183
|
+
"version --json must include actionable recommendations",
|
|
184
|
+
);
|
|
185
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
119
188
|
function runPublishTargetSelfTests() {
|
|
120
189
|
const unpublished = {
|
|
121
190
|
skipped: false,
|
|
@@ -322,6 +391,22 @@ function runExtensionRegistrySelfTests() {
|
|
|
322
391
|
);
|
|
323
392
|
}
|
|
324
393
|
|
|
394
|
+
function runSettingsRuntimeStateSelfTests() {
|
|
395
|
+
const input = "{\n \"lastChangelogVersion\": \"0.80.3\",\n \"theme\": \"gruvbox-dark\"\n}\n";
|
|
396
|
+
const stripped = JSON.parse(stripSettingsRuntimeMarkerText(input));
|
|
397
|
+
assert(
|
|
398
|
+
stripped.lastChangelogVersion === undefined && stripped.theme === "gruvbox-dark",
|
|
399
|
+
"settings runtime clean filter must remove only lastChangelogVersion",
|
|
400
|
+
);
|
|
401
|
+
const marker = settingsRuntimeMarkerFromObject({ lastChangelogVersion: "0.80.3" });
|
|
402
|
+
const state = mergeSettingsRuntimeMarkerIntoState({ schema: "pi67.state.v1" }, marker, "2026-07-08T00:00:00.000Z");
|
|
403
|
+
assert(
|
|
404
|
+
state.runtimeMarkers?.lastChangelogVersion?.value === "0.80.3" &&
|
|
405
|
+
state.runtimeMarkers.lastChangelogVersion.storage === "state.json",
|
|
406
|
+
"settings runtime marker must migrate into state runtimeMarkers",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
325
410
|
function runUpdatePlanSelfTests() {
|
|
326
411
|
assert(
|
|
327
412
|
classifyGitShort(" M settings.json\n?? tmp.txt").preservedRuntime.includes("settings.json"),
|
|
@@ -396,8 +481,8 @@ function runUpdatePlanSelfTests() {
|
|
|
396
481
|
skills: { summary: { missing: 0, conflicts: 2 } },
|
|
397
482
|
});
|
|
398
483
|
assert(
|
|
399
|
-
buildPlanDecisions(sharedSkillConflict).warnings.some((item) => item.includes("
|
|
400
|
-
"shared
|
|
484
|
+
buildPlanDecisions(sharedSkillConflict).warnings.some((item) => item.includes("preserved user-modified global skills")),
|
|
485
|
+
"preserved user-modified shared skills must warn and preserve by default",
|
|
401
486
|
);
|
|
402
487
|
const strictSharedSkillConflict = decisionsFixture({
|
|
403
488
|
strictSharedSkills: true,
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -42,7 +42,7 @@ Options:
|
|
|
42
42
|
--no-skill-list Skip pi skill list on POSIX platforms; accepted as a no-op on Windows.
|
|
43
43
|
--skill-list-timeout-seconds N
|
|
44
44
|
Timeout for pi skill list where enabled.
|
|
45
|
-
--strict-shared-skills Treat
|
|
45
|
+
--strict-shared-skills Treat preserved user-modified shared skills as blocking.
|
|
46
46
|
--dry-run Print the script invocation without running it.
|
|
47
47
|
|
|
48
48
|
Examples:
|
package/src/commands/install.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { runDistroScript } from "../lib/distro-scripts.mjs";
|
|
|
7
7
|
import { isWindows } from "../lib/platform.mjs";
|
|
8
8
|
import { CliError, info, warn } from "../lib/output.mjs";
|
|
9
9
|
import { writeState } from "../lib/state-store.mjs";
|
|
10
|
+
import { migrateSettingsRuntimeState } from "../lib/settings-runtime-state.mjs";
|
|
10
11
|
|
|
11
12
|
export async function installCommand(ctx, argv) {
|
|
12
13
|
const { options } = parseCommandOptions(argv, {
|
|
@@ -47,7 +48,25 @@ export async function installCommand(ctx, argv) {
|
|
|
47
48
|
if (dryRun) args.push("--dry-run");
|
|
48
49
|
runCommand("bash", [path.join(ctx.repoRoot, "install.sh"), ...args], { cwd: ctx.repoRoot, dryRun: false });
|
|
49
50
|
}
|
|
50
|
-
if (!dryRun)
|
|
51
|
+
if (!dryRun) {
|
|
52
|
+
writeState(ctx, "install");
|
|
53
|
+
const runtimeState = migrateSettingsRuntimeState(ctx, {
|
|
54
|
+
normalizeSettingsJson: true,
|
|
55
|
+
installGitFilter: true,
|
|
56
|
+
});
|
|
57
|
+
if (runtimeState.markerFound && runtimeState.stateWritten) {
|
|
58
|
+
info("Migrated settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json");
|
|
59
|
+
}
|
|
60
|
+
if (runtimeState.settingsNormalized) {
|
|
61
|
+
info("Normalized settings.json by removing runtime-only lastChangelogVersion.");
|
|
62
|
+
}
|
|
63
|
+
if (runtimeState.gitFilterInstalled) {
|
|
64
|
+
info("Installed local git clean filter for future settings.json runtime markers.");
|
|
65
|
+
}
|
|
66
|
+
for (const error of runtimeState.errors) {
|
|
67
|
+
warn(`settings runtime marker migration skipped: ${error}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
51
70
|
info("Install finished. Run `pi-67 doctor` next.");
|
|
52
71
|
}
|
|
53
72
|
|
package/src/commands/skills.mjs
CHANGED
|
@@ -26,9 +26,9 @@ function inventory(ctx, argv) {
|
|
|
26
26
|
keyValue("Target", data.skillsDir);
|
|
27
27
|
keyValue("Identical", data.summary.identical);
|
|
28
28
|
keyValue("Missing", data.summary.missing);
|
|
29
|
-
keyValue("
|
|
29
|
+
keyValue("Preserved user-modified", preservedUserModified(data.summary));
|
|
30
30
|
for (const entry of data.entries.filter((item) => !item.identical)) {
|
|
31
|
-
const status = entry.conflict ? "
|
|
31
|
+
const status = entry.conflict ? "preserved user-modified" : "missing";
|
|
32
32
|
info(`${status}: ${entry.name}`);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -43,7 +43,7 @@ function plan(ctx, argv) {
|
|
|
43
43
|
keyValue("Target", data.skillsDir);
|
|
44
44
|
keyValue("Selected", data.selected.length === 0 ? "all changed skills" : data.selected.join(", "));
|
|
45
45
|
keyValue("Missing", data.actions.filter((item) => item.action === "copy-missing").length);
|
|
46
|
-
keyValue("
|
|
46
|
+
keyValue("Preserved user-modified", data.actions.filter((item) => item.action === "preserve-conflict").length);
|
|
47
47
|
for (const action of data.actions) {
|
|
48
48
|
const command = action.conflict
|
|
49
49
|
? `inspect: pi-67 skills diff ${action.name}; explicit sync: pi-67 skills sync ${action.name} --dry-run`
|
|
@@ -100,7 +100,11 @@ function migrate(ctx, argv) {
|
|
|
100
100
|
section("pi-67 skills migrate preview");
|
|
101
101
|
warn("migrate is intentionally dry-run in the npm manager; use `pi-67 skills sync` to copy missing skills.");
|
|
102
102
|
keyValue("Missing", data.summary.missing);
|
|
103
|
-
keyValue("
|
|
103
|
+
keyValue("Preserved user-modified", preservedUserModified(data.summary));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function preservedUserModified(summary) {
|
|
107
|
+
return summary?.preservedUserModified ?? summary?.conflicts ?? 0;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
function printSkillsHelp() {
|
|
@@ -115,8 +119,9 @@ Usage:
|
|
|
115
119
|
|
|
116
120
|
Safety:
|
|
117
121
|
Missing skills are copied by default. Existing different global skills are
|
|
118
|
-
preserved unless you name the skill explicitly and pass
|
|
119
|
-
overwrite is intentionally
|
|
122
|
+
preserved as user-modified unless you name the skill explicitly and pass
|
|
123
|
+
--yes. Bulk overwrite of preserved user-modified skills is intentionally
|
|
124
|
+
blocked.
|
|
120
125
|
|
|
121
126
|
Examples:
|
|
122
127
|
pi-67 skills inventory
|
package/src/commands/status.mjs
CHANGED
|
@@ -21,7 +21,11 @@ export async function statusCommand(ctx, argv) {
|
|
|
21
21
|
keyValue("Provider", plan.settings.defaultProvider || "unset");
|
|
22
22
|
keyValue("Model", plan.settings.defaultModel || "unset");
|
|
23
23
|
keyValue("Theme", plan.settings.theme || "unset");
|
|
24
|
-
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${plan.skills
|
|
24
|
+
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${preservedUserModified(plan.skills)} preserved user-modified`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function preservedUserModified(skills) {
|
|
28
|
+
return skills?.preservedUserModified ?? skills?.conflicts ?? 0;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
function printStatusHelp() {
|
package/src/commands/update.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { isWindows } from "../lib/platform.mjs";
|
|
|
6
6
|
import { runCommand } from "../lib/shell-runner.mjs";
|
|
7
7
|
import { writeState } from "../lib/state-store.mjs";
|
|
8
8
|
import { beginUpdateLifecycle } from "../lib/update-safety.mjs";
|
|
9
|
+
import { migrateSettingsRuntimeState } from "../lib/settings-runtime-state.mjs";
|
|
9
10
|
|
|
10
11
|
export async function updateCommand(ctx, argv) {
|
|
11
12
|
const { options } = parseCommandOptions(argv, {
|
|
@@ -65,7 +66,10 @@ export async function updateCommand(ctx, argv) {
|
|
|
65
66
|
: buildBashUpdateArgs(ctx, options, dryRun);
|
|
66
67
|
try {
|
|
67
68
|
runDistroScript(ctx, { sh: "pi67-update.sh", ps1: "pi67-update.ps1" }, args, { dryRun: false });
|
|
68
|
-
if (!dryRun)
|
|
69
|
+
if (!dryRun) {
|
|
70
|
+
writeState(ctx, options.repair ? "repair" : "update");
|
|
71
|
+
reportSettingsRuntimeStateMigration(ctx);
|
|
72
|
+
}
|
|
69
73
|
} finally {
|
|
70
74
|
lifecycle.release();
|
|
71
75
|
}
|
|
@@ -82,6 +86,25 @@ export async function updateCommand(ctx, argv) {
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
function reportSettingsRuntimeStateMigration(ctx) {
|
|
90
|
+
const result = migrateSettingsRuntimeState(ctx, {
|
|
91
|
+
normalizeSettingsJson: true,
|
|
92
|
+
installGitFilter: true,
|
|
93
|
+
});
|
|
94
|
+
if (result.markerFound && result.stateWritten) {
|
|
95
|
+
info("Migrated settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json");
|
|
96
|
+
}
|
|
97
|
+
if (result.settingsNormalized) {
|
|
98
|
+
info("Normalized settings.json by removing runtime-only lastChangelogVersion.");
|
|
99
|
+
}
|
|
100
|
+
if (result.gitFilterInstalled) {
|
|
101
|
+
info("Installed local git clean filter for future settings.json runtime markers.");
|
|
102
|
+
}
|
|
103
|
+
for (const error of result.errors) {
|
|
104
|
+
warn(`settings runtime marker migration skipped: ${error}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
function buildBashUpdateArgs(ctx, options, dryRun) {
|
|
86
109
|
const args = ["--agent-dir", ctx.agentDir, "--repo-root", ctx.repoRoot, "--skills-dir", ctx.skillsDir];
|
|
87
110
|
if (dryRun) args.push("--dry-run");
|
|
@@ -128,7 +151,7 @@ function printPlan(plan) {
|
|
|
128
151
|
keyValue("Model", plan.settings.defaultModel || "unset");
|
|
129
152
|
keyValue("Theme", plan.settings.theme || "unset");
|
|
130
153
|
keyValue("Theme installed", plan.settings.themeInstalled ? "yes" : "no");
|
|
131
|
-
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${plan.skills
|
|
154
|
+
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${preservedUserModified(plan.skills)} preserved user-modified`);
|
|
132
155
|
for (const repo of plan.external) {
|
|
133
156
|
keyValue(`External ${repo.name}`, repo.exists ? `${repo.git?.dirty ? "dirty" : "clean"} ${repo.git?.commit || ""}` : "missing");
|
|
134
157
|
}
|
|
@@ -153,6 +176,10 @@ function printPlan(plan) {
|
|
|
153
176
|
if (!plan.git?.dirty) pass("update check completed without local dirty blocker");
|
|
154
177
|
}
|
|
155
178
|
|
|
179
|
+
function preservedUserModified(skills) {
|
|
180
|
+
return skills?.preservedUserModified ?? skills?.conflicts ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
156
183
|
function printUpdateHelp() {
|
|
157
184
|
process.stdout.write(`pi-67 update - update pi-67 safely
|
|
158
185
|
|
|
@@ -167,7 +194,7 @@ Options:
|
|
|
167
194
|
--no-remote Skip remote git/npm registry checks where supported.
|
|
168
195
|
--no-npm Skip npm package sync in the distro updater.
|
|
169
196
|
--allow-dirty Let the script-level updater handle non-runtime dirty files.
|
|
170
|
-
--strict-shared-skills Treat
|
|
197
|
+
--strict-shared-skills Treat preserved user-modified shared skills as blocking.
|
|
171
198
|
--include-pi Also run upstream \`pi update --all\` when paired with --yes.
|
|
172
199
|
--include-external Report explicit external repo update commands.
|
|
173
200
|
--all Alias for --include-pi --include-external.
|
package/src/commands/version.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { captureCommand } from "../lib/shell-runner.mjs";
|
|
2
2
|
import { gitStatus } from "../lib/git.mjs";
|
|
3
3
|
import { currentTheme } from "../lib/theme-policy.mjs";
|
|
4
|
-
import { keyValue, printJson } from "../lib/output.mjs";
|
|
4
|
+
import { info, keyValue, printJson, section, warn } from "../lib/output.mjs";
|
|
5
5
|
import { platformName } from "../lib/platform.mjs";
|
|
6
6
|
import { readCliPackageJson, readTextIfExists } from "../lib/paths.mjs";
|
|
7
|
+
import { readJsonFileIfExists } from "../lib/config-json.mjs";
|
|
7
8
|
import { parseCommandOptions } from "../lib/args.mjs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
|
|
@@ -17,6 +18,8 @@ export async function versionCommand(ctx, argv) {
|
|
|
17
18
|
const pkg = readCliPackageJson();
|
|
18
19
|
const git = gitStatus(ctx.repoRoot);
|
|
19
20
|
const pi = captureCommand("pi", ["--version"]);
|
|
21
|
+
const distroVersion = readTextIfExists(path.join(ctx.repoRoot, "VERSION")).trim();
|
|
22
|
+
const settings = readJsonFileIfExists(path.join(ctx.agentDir, "settings.json")) || {};
|
|
20
23
|
const data = {
|
|
21
24
|
schema: "pi67.version.v1",
|
|
22
25
|
manager: {
|
|
@@ -24,7 +27,7 @@ export async function versionCommand(ctx, argv) {
|
|
|
24
27
|
version: pkg.version,
|
|
25
28
|
},
|
|
26
29
|
distro: {
|
|
27
|
-
version:
|
|
30
|
+
version: distroVersion,
|
|
28
31
|
commit: git.commit || "",
|
|
29
32
|
branch: git.branch || "",
|
|
30
33
|
dirty: Boolean(git.dirty),
|
|
@@ -42,6 +45,7 @@ export async function versionCommand(ctx, argv) {
|
|
|
42
45
|
},
|
|
43
46
|
theme: currentTheme(ctx),
|
|
44
47
|
};
|
|
48
|
+
data.recommendations = buildVersionRecommendations(ctx, data, git, settings);
|
|
45
49
|
if (json) {
|
|
46
50
|
printJson(data);
|
|
47
51
|
return;
|
|
@@ -54,6 +58,79 @@ export async function versionCommand(ctx, argv) {
|
|
|
54
58
|
keyValue("platform", data.runtime.platform);
|
|
55
59
|
keyValue("agentDir", data.paths.agentDir);
|
|
56
60
|
keyValue("theme", data.theme || "unset");
|
|
61
|
+
printRecommendations(data.recommendations);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildVersionRecommendations(ctx, data, git, settings) {
|
|
65
|
+
const recommendations = [];
|
|
66
|
+
const managerVsDistro = compareSemver(data.manager.version, data.distro.version);
|
|
67
|
+
const settingsDirty = git.short
|
|
68
|
+
.split(/\r?\n/)
|
|
69
|
+
.some((line) => line.trim().endsWith("settings.json"));
|
|
70
|
+
const hasRuntimeMarker = settings && Object.prototype.hasOwnProperty.call(settings, "lastChangelogVersion");
|
|
71
|
+
const updateCommand = `pi-67 --agent-dir "${ctx.agentDir}" --repo-root "${ctx.repoRoot}" update --repair`;
|
|
72
|
+
|
|
73
|
+
if (managerVsDistro > 0) {
|
|
74
|
+
recommendations.push({
|
|
75
|
+
level: "WARN",
|
|
76
|
+
message: `manager is ${data.manager.version} but local distro is ${data.distro.version || "unknown"}; npm install updated only the manager package.`,
|
|
77
|
+
});
|
|
78
|
+
recommendations.push({
|
|
79
|
+
level: "INFO",
|
|
80
|
+
message: `Run: ${updateCommand}`,
|
|
81
|
+
});
|
|
82
|
+
} else if (managerVsDistro < 0) {
|
|
83
|
+
recommendations.push({
|
|
84
|
+
level: "WARN",
|
|
85
|
+
message: `local distro ${data.distro.version} is newer than manager ${data.manager.version}; update the manager with npm install -g ${data.manager.package}@latest.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (settingsDirty && hasRuntimeMarker) {
|
|
90
|
+
recommendations.push({
|
|
91
|
+
level: "INFO",
|
|
92
|
+
message: "settings.json has Pi runtime changelog marker state; update --repair migrates it into ~/.pi/pi67/state.json and normalizes settings.json.",
|
|
93
|
+
});
|
|
94
|
+
} else if (settingsDirty) {
|
|
95
|
+
recommendations.push({
|
|
96
|
+
level: "INFO",
|
|
97
|
+
message: "settings.json is dirty; inspect with git diff -- settings.json before updating if it contains personal provider/model/theme edits.",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!data.runtime.pi) {
|
|
102
|
+
recommendations.push({
|
|
103
|
+
level: "INFO",
|
|
104
|
+
message: "`pi` is not on PATH in this shell; this does not block pi-67 manager updates.",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return recommendations;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printRecommendations(recommendations) {
|
|
111
|
+
if (!recommendations?.length) return;
|
|
112
|
+
section("Next steps");
|
|
113
|
+
for (const item of recommendations) {
|
|
114
|
+
if (item.level === "WARN") warn(item.message);
|
|
115
|
+
else info(item.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function compareSemver(left, right) {
|
|
120
|
+
const a = parseSemver(left);
|
|
121
|
+
const b = parseSemver(right);
|
|
122
|
+
if (!a || !b) return 0;
|
|
123
|
+
for (let index = 0; index < 3; index += 1) {
|
|
124
|
+
if (a[index] > b[index]) return 1;
|
|
125
|
+
if (a[index] < b[index]) return -1;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseSemver(value) {
|
|
131
|
+
const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
132
|
+
if (!match) return null;
|
|
133
|
+
return match.slice(1, 4).map((item) => Number(item));
|
|
57
134
|
}
|
|
58
135
|
|
|
59
136
|
function printVersionHelp() {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const SETTINGS_RUNTIME_MARKER_KEY = "lastChangelogVersion";
|
|
2
|
+
|
|
3
|
+
export function stripSettingsRuntimeMarker(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
5
|
+
const next = { ...value };
|
|
6
|
+
delete next[SETTINGS_RUNTIME_MARKER_KEY];
|
|
7
|
+
return next;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function stripSettingsRuntimeMarkerText(text) {
|
|
11
|
+
try {
|
|
12
|
+
return stableSettingsJson(stripSettingsRuntimeMarker(JSON.parse(String(text || "").replace(/^\uFEFF/, ""))));
|
|
13
|
+
} catch {
|
|
14
|
+
return String(text || "");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stableSettingsJson(value) {
|
|
19
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { captureCommand } from "./shell-runner.mjs";
|
|
4
|
+
import { isGitRepo } from "./git.mjs";
|
|
5
|
+
import { readJsonFileIfExists, writeJsonAtomic } from "./config-json.mjs";
|
|
6
|
+
import {
|
|
7
|
+
SETTINGS_RUNTIME_MARKER_KEY,
|
|
8
|
+
stableSettingsJson,
|
|
9
|
+
stripSettingsRuntimeMarker,
|
|
10
|
+
stripSettingsRuntimeMarkerText,
|
|
11
|
+
} from "./settings-runtime-clean.mjs";
|
|
12
|
+
|
|
13
|
+
export const SETTINGS_RUNTIME_FILTER_NAME = "pi67-settings-runtime-state";
|
|
14
|
+
export const SETTINGS_RUNTIME_FILTER_SCRIPT = "packages/pi67-cli/src/tools/settings-runtime-state-filter.mjs";
|
|
15
|
+
|
|
16
|
+
export { SETTINGS_RUNTIME_MARKER_KEY, stripSettingsRuntimeMarker, stripSettingsRuntimeMarkerText };
|
|
17
|
+
|
|
18
|
+
export function settingsRuntimeMarkerFromObject(settings) {
|
|
19
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return null;
|
|
20
|
+
const value = settings[SETTINGS_RUNTIME_MARKER_KEY];
|
|
21
|
+
if (value === undefined || value === null || String(value).trim() === "") return null;
|
|
22
|
+
return {
|
|
23
|
+
key: SETTINGS_RUNTIME_MARKER_KEY,
|
|
24
|
+
value: String(value),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function settingsRuntimeMarkerFromState(state) {
|
|
29
|
+
const marker = state?.runtimeMarkers?.[SETTINGS_RUNTIME_MARKER_KEY];
|
|
30
|
+
if (!marker || marker.value === undefined || marker.value === null || String(marker.value).trim() === "") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
key: SETTINGS_RUNTIME_MARKER_KEY,
|
|
35
|
+
value: String(marker.value),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function mergeSettingsRuntimeMarkerIntoState(state, marker, now = new Date().toISOString()) {
|
|
40
|
+
const next = { ...(state || {}) };
|
|
41
|
+
const runtimeMarkers = { ...(next.runtimeMarkers || {}) };
|
|
42
|
+
if (marker?.value) {
|
|
43
|
+
runtimeMarkers[SETTINGS_RUNTIME_MARKER_KEY] = {
|
|
44
|
+
value: marker.value,
|
|
45
|
+
source: "settings.json",
|
|
46
|
+
storage: "state.json",
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(runtimeMarkers).length > 0) {
|
|
51
|
+
next.runtimeMarkers = runtimeMarkers;
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function migrateSettingsRuntimeState(ctx, options = {}) {
|
|
57
|
+
const normalizeSettingsJson = Boolean(options.normalizeSettingsJson);
|
|
58
|
+
const installGitFilter = Boolean(options.installGitFilter);
|
|
59
|
+
const dryRun = Boolean(options.dryRun);
|
|
60
|
+
const settingsPath = path.join(ctx.agentDir, "settings.json");
|
|
61
|
+
const statePath = path.join(ctx.stateDir, "state.json");
|
|
62
|
+
const result = {
|
|
63
|
+
schema: "pi67.settings-runtime-state.v1",
|
|
64
|
+
settingsPath,
|
|
65
|
+
statePath,
|
|
66
|
+
markerFound: false,
|
|
67
|
+
markerValue: "",
|
|
68
|
+
stateWritten: false,
|
|
69
|
+
settingsNormalized: false,
|
|
70
|
+
gitFilterInstalled: false,
|
|
71
|
+
skipped: [],
|
|
72
|
+
errors: [],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(settingsPath)) {
|
|
76
|
+
result.skipped.push("settings.json missing");
|
|
77
|
+
} else {
|
|
78
|
+
let settingsText = "";
|
|
79
|
+
let settings = null;
|
|
80
|
+
try {
|
|
81
|
+
settingsText = fs.readFileSync(settingsPath, "utf8").replace(/^\uFEFF/, "");
|
|
82
|
+
settings = JSON.parse(settingsText);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
result.errors.push(`settings.json is not valid JSON: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const marker = settingsRuntimeMarkerFromObject(settings);
|
|
88
|
+
if (marker) {
|
|
89
|
+
result.markerFound = true;
|
|
90
|
+
result.markerValue = marker.value;
|
|
91
|
+
if (!dryRun) {
|
|
92
|
+
const previous = readJsonFileIfExists(statePath) || {};
|
|
93
|
+
writeJsonAtomic(statePath, mergeSettingsRuntimeMarkerIntoState(previous, marker));
|
|
94
|
+
result.stateWritten = true;
|
|
95
|
+
}
|
|
96
|
+
if (normalizeSettingsJson) {
|
|
97
|
+
const normalizedText = stableSettingsJson(stripSettingsRuntimeMarker(settings));
|
|
98
|
+
if (normalizedText !== settingsText) {
|
|
99
|
+
if (!dryRun) {
|
|
100
|
+
fs.writeFileSync(settingsPath, normalizedText, "utf8");
|
|
101
|
+
}
|
|
102
|
+
result.settingsNormalized = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
result.skipped.push("settings.json runtime marker absent");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (installGitFilter) {
|
|
111
|
+
const filterResult = installSettingsRuntimeGitFilter(ctx, { dryRun });
|
|
112
|
+
result.gitFilterInstalled = filterResult.installed;
|
|
113
|
+
if (filterResult.skipped) result.skipped.push(filterResult.skipped);
|
|
114
|
+
if (filterResult.error) result.errors.push(filterResult.error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function installSettingsRuntimeGitFilter(ctx, options = {}) {
|
|
121
|
+
const result = {
|
|
122
|
+
installed: false,
|
|
123
|
+
skipped: "",
|
|
124
|
+
error: "",
|
|
125
|
+
};
|
|
126
|
+
if (options.dryRun) {
|
|
127
|
+
result.installed = true;
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
if (!isGitRepo(ctx.repoRoot)) {
|
|
131
|
+
result.skipped = "repo root is not a git checkout";
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
const scriptPath = path.join(ctx.repoRoot, SETTINGS_RUNTIME_FILTER_SCRIPT);
|
|
135
|
+
if (!fs.existsSync(scriptPath)) {
|
|
136
|
+
result.skipped = `settings runtime filter script missing: ${SETTINGS_RUNTIME_FILTER_SCRIPT}`;
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const cleanCommand = `node ${SETTINGS_RUNTIME_FILTER_SCRIPT} --clean`;
|
|
141
|
+
const clean = captureCommand("git", [
|
|
142
|
+
"-C",
|
|
143
|
+
ctx.repoRoot,
|
|
144
|
+
"config",
|
|
145
|
+
"--local",
|
|
146
|
+
`filter.${SETTINGS_RUNTIME_FILTER_NAME}.clean`,
|
|
147
|
+
cleanCommand,
|
|
148
|
+
]);
|
|
149
|
+
if (!clean.ok) {
|
|
150
|
+
result.error = clean.stderr || clean.error || "failed to configure settings runtime clean filter";
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
const required = captureCommand("git", [
|
|
154
|
+
"-C",
|
|
155
|
+
ctx.repoRoot,
|
|
156
|
+
"config",
|
|
157
|
+
"--local",
|
|
158
|
+
`filter.${SETTINGS_RUNTIME_FILTER_NAME}.required`,
|
|
159
|
+
"false",
|
|
160
|
+
]);
|
|
161
|
+
if (!required.ok) {
|
|
162
|
+
result.error = required.stderr || required.error || "failed to configure settings runtime filter required=false";
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
result.installed = true;
|
|
166
|
+
return result;
|
|
167
|
+
}
|
package/src/lib/skill-policy.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export function inventorySkills(ctx) {
|
|
|
16
16
|
const conflict = targetExists && !identical;
|
|
17
17
|
return { name, source, target, sourceHash, targetExists, targetHash, identical, conflict };
|
|
18
18
|
});
|
|
19
|
+
const conflicts = entries.filter((entry) => entry.conflict).length;
|
|
19
20
|
return {
|
|
20
21
|
schema: "pi67.skills-inventory.v1",
|
|
21
22
|
sourceRoot,
|
|
@@ -24,7 +25,8 @@ export function inventorySkills(ctx) {
|
|
|
24
25
|
source: entries.length,
|
|
25
26
|
missing: entries.filter((entry) => !entry.targetExists).length,
|
|
26
27
|
identical: entries.filter((entry) => entry.identical).length,
|
|
27
|
-
conflicts
|
|
28
|
+
conflicts,
|
|
29
|
+
preservedUserModified: conflicts,
|
|
28
30
|
},
|
|
29
31
|
entries,
|
|
30
32
|
};
|
|
@@ -79,7 +81,7 @@ export function planSkills(ctx, { names = [] } = {}) {
|
|
|
79
81
|
conflict: entry.conflict,
|
|
80
82
|
action: entry.conflict ? "preserve-conflict" : "copy-missing",
|
|
81
83
|
reason: entry.conflict
|
|
82
|
-
? "target differs; default update preserves
|
|
84
|
+
? "target differs; default update preserves this user-modified global skill"
|
|
83
85
|
: "target is missing and can be copied safely",
|
|
84
86
|
}));
|
|
85
87
|
return {
|
|
@@ -112,7 +114,7 @@ export function syncSkills(ctx, { dryRun = false, names = [], yes = false } = {}
|
|
|
112
114
|
action: "warn",
|
|
113
115
|
reason: targeted
|
|
114
116
|
? "target differs; rerun with --yes to replace this explicitly named skill after backup"
|
|
115
|
-
: "target differs; bulk
|
|
117
|
+
: "target differs; bulk overwrite of preserved user-modified skills is intentionally blocked",
|
|
116
118
|
});
|
|
117
119
|
continue;
|
|
118
120
|
}
|
package/src/lib/state-store.mjs
CHANGED
|
@@ -4,11 +4,18 @@ import { gitStatus } from "./git.mjs";
|
|
|
4
4
|
import { currentTheme } from "./theme-policy.mjs";
|
|
5
5
|
import { readJsonFileIfExists, writeJsonAtomic } from "./config-json.mjs";
|
|
6
6
|
import { readCliPackageJson, readTextIfExists } from "./paths.mjs";
|
|
7
|
+
import {
|
|
8
|
+
mergeSettingsRuntimeMarkerIntoState,
|
|
9
|
+
settingsRuntimeMarkerFromObject,
|
|
10
|
+
settingsRuntimeMarkerFromState,
|
|
11
|
+
} from "./settings-runtime-state.mjs";
|
|
7
12
|
|
|
8
13
|
export function writeState(ctx, operation) {
|
|
9
14
|
const pkg = readCliPackageJson();
|
|
10
15
|
const git = gitStatus(ctx.repoRoot);
|
|
11
16
|
const settings = readJsonFileIfExists(path.join(ctx.agentDir, "settings.json")) || {};
|
|
17
|
+
const statePath = path.join(ctx.stateDir, "state.json");
|
|
18
|
+
const previous = readJsonFileIfExists(statePath) || {};
|
|
12
19
|
const state = {
|
|
13
20
|
schema: "pi67.state.v1",
|
|
14
21
|
updatedAt: new Date().toISOString(),
|
|
@@ -25,7 +32,12 @@ export function writeState(ctx, operation) {
|
|
|
25
32
|
lastProvider: settings.defaultProvider || "",
|
|
26
33
|
lastModel: settings.defaultModel || "",
|
|
27
34
|
};
|
|
35
|
+
const mergedState = mergeSettingsRuntimeMarkerIntoState(
|
|
36
|
+
state,
|
|
37
|
+
settingsRuntimeMarkerFromObject(settings) || settingsRuntimeMarkerFromState(previous),
|
|
38
|
+
state.updatedAt,
|
|
39
|
+
);
|
|
28
40
|
fs.mkdirSync(ctx.stateDir, { recursive: true });
|
|
29
|
-
writeJsonAtomic(
|
|
30
|
-
return
|
|
41
|
+
writeJsonAtomic(statePath, mergedState);
|
|
42
|
+
return mergedState;
|
|
31
43
|
}
|
package/src/lib/update-plan.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { readCliPackageJson, readTextIfExists } from "./paths.mjs";
|
|
|
9
9
|
import { npmLatestVersion } from "./npm-registry.mjs";
|
|
10
10
|
import { buildDistroManifest } from "./distro-manifest.mjs";
|
|
11
11
|
import { PRESERVED_RUNTIME_FILES } from "./update-safety.mjs";
|
|
12
|
+
import { settingsRuntimeMarkerFromObject } from "./settings-runtime-state.mjs";
|
|
12
13
|
|
|
13
14
|
export async function buildUpdatePlan(ctx, options = {}) {
|
|
14
15
|
const versionFile = path.join(ctx.repoRoot, "VERSION");
|
|
@@ -39,6 +40,7 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
39
40
|
currentVersion: pkg.version,
|
|
40
41
|
noRemote: options.noRemote,
|
|
41
42
|
});
|
|
43
|
+
const settingsRuntimeMarker = settingsRuntimeMarkerFromObject(settings);
|
|
42
44
|
const dirtyClass = classifyGitShort(git?.short || "");
|
|
43
45
|
const benignRuntime = classifyBenignRuntimeDiff(ctx, dirtyClass.preservedRuntime);
|
|
44
46
|
|
|
@@ -54,15 +56,14 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
54
56
|
} else if (git.dirty && dirtyClass.unsafeTracked.length > 0) {
|
|
55
57
|
recommendations.push("Resolve or commit local changes before pi-67 update.");
|
|
56
58
|
} else if (git.dirty && benignRuntime.benign) {
|
|
57
|
-
recommendations.push("No manual action required for benign settings runtime markers; pi-67
|
|
58
|
-
recommendations.push("Optional: normalize local settings runtime marker if you want a clean git status.");
|
|
59
|
+
recommendations.push("No manual action required for benign settings runtime markers; pi-67 migrates lastChangelogVersion to ignored state during update/repair.");
|
|
59
60
|
} else if (git.dirty) {
|
|
60
61
|
recommendations.push("No manual action required for user runtime config; pi-67 backs up/restores it during update.");
|
|
61
62
|
} else {
|
|
62
63
|
recommendations.push("Run: pi-67 update");
|
|
63
64
|
}
|
|
64
65
|
if (skills.summary.conflicts > 0) {
|
|
65
|
-
recommendations.push("Run: pi-67 skills inventory
|
|
66
|
+
recommendations.push("Run: pi-67 skills inventory to inspect preserved user-modified global skills.");
|
|
66
67
|
}
|
|
67
68
|
if (theme && !hasTheme(ctx, theme)) {
|
|
68
69
|
recommendations.push(`Current theme is missing: ${theme}. Run: pi-67 themes list`);
|
|
@@ -74,6 +75,7 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
74
75
|
ctx,
|
|
75
76
|
git,
|
|
76
77
|
benignRuntime,
|
|
78
|
+
settingsRuntimeMarker,
|
|
77
79
|
managerRegistry,
|
|
78
80
|
remote,
|
|
79
81
|
manifest,
|
|
@@ -122,6 +124,11 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
122
124
|
scripts: scriptStatus,
|
|
123
125
|
manifest: manifest.summary,
|
|
124
126
|
skills: skills.summary,
|
|
127
|
+
runtimeState: {
|
|
128
|
+
settingsLastChangelogVersion: settingsRuntimeMarker?.value || "",
|
|
129
|
+
storage: "~/.pi/pi67/state.json",
|
|
130
|
+
policy: "settings.json lastChangelogVersion is runtime-only and is normalized into ignored state on update/repair",
|
|
131
|
+
},
|
|
125
132
|
external,
|
|
126
133
|
actions: decisions.actions,
|
|
127
134
|
blocked: decisions.blocked,
|
|
@@ -139,6 +146,7 @@ export function buildPlanDecisions(context) {
|
|
|
139
146
|
];
|
|
140
147
|
const dirty = classifyGitShort(context.git?.short || "");
|
|
141
148
|
const benignRuntime = context.benignRuntime || { benign: false, reasons: [] };
|
|
149
|
+
const settingsRuntimeMarker = context.settingsRuntimeMarker || null;
|
|
142
150
|
|
|
143
151
|
if (context.managerRegistry.outdated) {
|
|
144
152
|
actions.push({
|
|
@@ -153,6 +161,19 @@ export function buildPlanDecisions(context) {
|
|
|
153
161
|
});
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
if (settingsRuntimeMarker?.value || benignRuntime.benign) {
|
|
165
|
+
actions.push({
|
|
166
|
+
id: "settings-runtime-state",
|
|
167
|
+
kind: "runtime-state",
|
|
168
|
+
operation: "migrate-lastChangelogVersion-to-ignored-state",
|
|
169
|
+
writes: ["~/.pi/pi67/state.json", "settings.json only when removing runtime-only lastChangelogVersion"],
|
|
170
|
+
preserves: ["settings.json.theme", "settings.json.defaultProvider", "settings.json.defaultModel", "settings.json.packages"],
|
|
171
|
+
risk: "low",
|
|
172
|
+
reason: "lastChangelogVersion is Pi runtime UI state, not pi-67 source configuration",
|
|
173
|
+
benign: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
156
177
|
if (!context.git?.isRepo) {
|
|
157
178
|
blocked.push({
|
|
158
179
|
id: "repo-root",
|
|
@@ -257,16 +278,16 @@ export function buildPlanDecisions(context) {
|
|
|
257
278
|
});
|
|
258
279
|
}
|
|
259
280
|
if (context.skills.summary.conflicts > 0) {
|
|
260
|
-
const conflictMessage = `${context.skills.summary.conflicts}
|
|
281
|
+
const conflictMessage = `${context.skills.summary.conflicts} preserved user-modified global skills differ from the bundled baseline`;
|
|
261
282
|
if (context.strictSharedSkills) {
|
|
262
283
|
blocked.push({
|
|
263
284
|
id: "shared-skills",
|
|
264
285
|
kind: "skill-pack",
|
|
265
|
-
reason: `${conflictMessage}; strict mode blocks instead of overwriting`,
|
|
286
|
+
reason: `${conflictMessage}; strict mode blocks instead of overwriting user-modified skills`,
|
|
266
287
|
recovery: "inspect with pi-67 skills inventory, then sync or resolve manually",
|
|
267
288
|
});
|
|
268
289
|
} else {
|
|
269
|
-
warnings.push(`${conflictMessage}; default update preserves
|
|
290
|
+
warnings.push(`${conflictMessage}; default update preserves them`);
|
|
270
291
|
}
|
|
271
292
|
}
|
|
272
293
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
if (args.includes("--clean")) {
|
|
9
|
+
const { stripSettingsRuntimeMarkerText } = await import("../lib/settings-runtime-clean.mjs");
|
|
10
|
+
const input = fs.readFileSync(0, "utf8");
|
|
11
|
+
process.stdout.write(stripSettingsRuntimeMarkerText(input));
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (args.includes("--migrate")) {
|
|
16
|
+
const { migrateSettingsRuntimeState } = await import("../lib/settings-runtime-state.mjs");
|
|
17
|
+
const ctx = {
|
|
18
|
+
agentDir: path.resolve(valueAfter("--agent-dir") || process.cwd()),
|
|
19
|
+
repoRoot: path.resolve(valueAfter("--repo-root") || process.cwd()),
|
|
20
|
+
stateDir: path.resolve(valueAfter("--state-dir") || path.join(os.homedir(), ".pi", "pi67")),
|
|
21
|
+
};
|
|
22
|
+
const result = migrateSettingsRuntimeState(ctx, {
|
|
23
|
+
normalizeSettingsJson: args.includes("--normalize"),
|
|
24
|
+
installGitFilter: args.includes("--install-git-filter"),
|
|
25
|
+
dryRun: args.includes("--dry-run"),
|
|
26
|
+
});
|
|
27
|
+
if (args.includes("--json")) {
|
|
28
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
29
|
+
} else {
|
|
30
|
+
printHuman(result);
|
|
31
|
+
}
|
|
32
|
+
process.exit(result.errors.length > 0 ? 1 : 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.stderr.write(`Usage:
|
|
36
|
+
settings-runtime-state-filter.mjs --clean
|
|
37
|
+
settings-runtime-state-filter.mjs --migrate --agent-dir DIR --repo-root DIR [--state-dir DIR] [--normalize] [--install-git-filter] [--json] [--dry-run]
|
|
38
|
+
`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
|
|
41
|
+
function valueAfter(name) {
|
|
42
|
+
const index = args.indexOf(name);
|
|
43
|
+
if (index === -1) return "";
|
|
44
|
+
return args[index + 1] || "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printHuman(result) {
|
|
48
|
+
if (result.markerFound) {
|
|
49
|
+
console.log(`PASS migrated settings.json lastChangelogVersion to ${result.statePath}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log("PASS settings.json runtime marker is already absent");
|
|
52
|
+
}
|
|
53
|
+
if (result.settingsNormalized) {
|
|
54
|
+
console.log("PASS normalized settings.json by removing runtime-only lastChangelogVersion");
|
|
55
|
+
}
|
|
56
|
+
if (result.gitFilterInstalled) {
|
|
57
|
+
console.log("PASS installed local git clean filter for settings.json runtime marker");
|
|
58
|
+
}
|
|
59
|
+
for (const item of result.skipped) {
|
|
60
|
+
console.log(`INFO ${item}`);
|
|
61
|
+
}
|
|
62
|
+
for (const item of result.errors) {
|
|
63
|
+
console.error(`FAIL ${item}`);
|
|
64
|
+
}
|
|
65
|
+
}
|