@bigking67/pi-67 0.10.7 → 0.10.9

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.9]
4
+
5
+ - Runs settings runtime-marker migration before the local distro updater script
6
+ as well as after it, so latest npm manager repair can clean `M settings.json`
7
+ even when the local checkout still contains an older updater script.
8
+
9
+ ## [0.10.8]
10
+
11
+ - Adds actionable `pi-67 version` recommendations when the npm manager has
12
+ been updated but the local distro checkout is still old or `settings.json`
13
+ still has runtime-marker dirty state.
14
+
3
15
  ## [0.10.7]
4
16
 
5
17
  - Migrates `settings.json.lastChangelogVersion` into ignored manager state at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigking67/pi-67",
3
- "version": "0.10.7",
3
+ "version": "0.10.9",
4
4
  "description": "Cross-platform manager CLI for the pi-67 Pi Coding Agent distribution.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/check.mjs CHANGED
@@ -37,10 +37,12 @@ for (const file of files.filter((item) => item.endsWith(".json"))) {
37
37
  await runNpmRegistrySelfTests();
38
38
  runArgsSelfTests();
39
39
  runCliHelpContractSelfTests();
40
+ runVersionRecommendationSelfTests();
40
41
  runPublishTargetSelfTests();
41
42
  runShellRunnerSelfTests();
42
43
  runExtensionRegistrySelfTests();
43
44
  runSettingsRuntimeStateSelfTests();
45
+ runUpdatePreflightMigrationSelfTests();
44
46
  runUpdatePlanSelfTests();
45
47
  runUpdateSafetySelfTests();
46
48
 
@@ -122,6 +124,68 @@ function runCliHelpContractSelfTests() {
122
124
  fs.rmSync(tmpRoot, { recursive: true, force: true });
123
125
  }
124
126
 
127
+ function runVersionRecommendationSelfTests() {
128
+ if (spawnSync("git", ["--version"], { encoding: "utf8" }).status !== 0) return;
129
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-version-recommendation-"));
130
+ const home = path.join(tmpRoot, "home");
131
+ const repo = path.join(tmpRoot, "agent");
132
+ fs.mkdirSync(home, { recursive: true });
133
+ fs.mkdirSync(repo, { recursive: true });
134
+ fs.writeFileSync(path.join(repo, "VERSION"), "0.10.0\n");
135
+ fs.writeFileSync(path.join(repo, "settings.json"), "{\n \"lastChangelogVersion\": \"0.80.3\",\n \"theme\": \"gruvbox-dark\"\n}\n");
136
+ for (const args of [
137
+ ["-C", repo, "init", "-q"],
138
+ ["-C", repo, "config", "user.email", "pi67-check@example.invalid"],
139
+ ["-C", repo, "config", "user.name", "pi67-check"],
140
+ ["-C", repo, "add", "VERSION", "settings.json"],
141
+ ["-C", repo, "commit", "-q", "-m", "init"],
142
+ ]) {
143
+ const result = spawnSync("git", args, { encoding: "utf8" });
144
+ assert(result.status === 0, `git setup failed for version recommendation self-test: git ${args.join(" ")}\n${result.stderr}`);
145
+ }
146
+ fs.writeFileSync(path.join(repo, "settings.json"), "{\n \"lastChangelogVersion\": \"0.80.4\",\n \"theme\": \"gruvbox-dark\"\n}\n");
147
+ const env = { ...process.env, HOME: home, USERPROFILE: home };
148
+ const result = spawnSync(process.execPath, [
149
+ path.join(root, "bin", "pi-67.mjs"),
150
+ "--agent-dir",
151
+ repo,
152
+ "--repo-root",
153
+ repo,
154
+ "version",
155
+ ], {
156
+ cwd: root,
157
+ env,
158
+ encoding: "utf8",
159
+ });
160
+ assert(result.status === 0, `version recommendation command failed\n${result.stderr || result.stdout}`);
161
+ assert(
162
+ result.stdout.includes("npm install updated only the manager package") &&
163
+ result.stdout.includes("update --repair") &&
164
+ result.stdout.includes("settings.json has Pi runtime changelog marker state"),
165
+ `version output must explain manager/distro mismatch and runtime marker repair\n${result.stdout}`,
166
+ );
167
+ const json = spawnSync(process.execPath, [
168
+ path.join(root, "bin", "pi-67.mjs"),
169
+ "--agent-dir",
170
+ repo,
171
+ "--repo-root",
172
+ repo,
173
+ "version",
174
+ "--json",
175
+ ], {
176
+ cwd: root,
177
+ env,
178
+ encoding: "utf8",
179
+ });
180
+ assert(json.status === 0, `version --json recommendation command failed\n${json.stderr || json.stdout}`);
181
+ const parsed = JSON.parse(json.stdout);
182
+ assert(
183
+ parsed.recommendations?.some((item) => item.message.includes("update --repair")),
184
+ "version --json must include actionable recommendations",
185
+ );
186
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
187
+ }
188
+
125
189
  function runPublishTargetSelfTests() {
126
190
  const unpublished = {
127
191
  skipped: false,
@@ -344,6 +408,94 @@ function runSettingsRuntimeStateSelfTests() {
344
408
  );
345
409
  }
346
410
 
411
+ function runUpdatePreflightMigrationSelfTests() {
412
+ if (spawnSync("git", ["--version"], { encoding: "utf8" }).status !== 0) return;
413
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-update-preflight-migration-"));
414
+ const home = path.join(tmpRoot, "home");
415
+ const repo = path.join(tmpRoot, "agent");
416
+ const skillsDir = path.join(tmpRoot, "skills");
417
+ const packagesDir = path.join(tmpRoot, "packages");
418
+ fs.mkdirSync(path.join(repo, "scripts"), { recursive: true });
419
+ fs.mkdirSync(home, { recursive: true });
420
+ fs.mkdirSync(skillsDir, { recursive: true });
421
+ fs.mkdirSync(packagesDir, { recursive: true });
422
+ fs.writeFileSync(path.join(repo, "VERSION"), "0.10.0\n");
423
+ fs.writeFileSync(path.join(repo, "settings.json"), "{\n \"theme\": \"gruvbox-dark\"\n}\n");
424
+ fs.writeFileSync(path.join(repo, "scripts", "pi67-update.sh"), `#!/usr/bin/env bash
425
+ set -euo pipefail
426
+ agent_dir=""
427
+ while [ "$#" -gt 0 ]; do
428
+ case "$1" in
429
+ --agent-dir)
430
+ agent_dir="$2"
431
+ shift 2
432
+ ;;
433
+ *)
434
+ shift
435
+ ;;
436
+ esac
437
+ done
438
+ if grep -q "lastChangelogVersion" "$agent_dir/settings.json"; then
439
+ echo "marker still present before distro script" >&2
440
+ exit 44
441
+ fi
442
+ `);
443
+ for (const args of [
444
+ ["-C", repo, "init", "-q"],
445
+ ["-C", repo, "config", "user.email", "pi67-check@example.invalid"],
446
+ ["-C", repo, "config", "user.name", "pi67-check"],
447
+ ["-C", repo, "add", "VERSION", "settings.json", "scripts/pi67-update.sh"],
448
+ ["-C", repo, "commit", "-q", "-m", "init"],
449
+ ]) {
450
+ const result = spawnSync("git", args, { encoding: "utf8" });
451
+ assert(result.status === 0, `git setup failed for update preflight migration self-test: git ${args.join(" ")}\n${result.stderr}`);
452
+ }
453
+ fs.writeFileSync(path.join(repo, "settings.json"), "{\n \"lastChangelogVersion\": \"0.80.4\",\n \"theme\": \"gruvbox-dark\"\n}\n");
454
+ const env = { ...process.env, HOME: home, USERPROFILE: home };
455
+ const result = spawnSync(process.execPath, [
456
+ path.join(root, "bin", "pi-67.mjs"),
457
+ "--agent-dir",
458
+ repo,
459
+ "--repo-root",
460
+ repo,
461
+ "--skills-dir",
462
+ skillsDir,
463
+ "--packages-dir",
464
+ packagesDir,
465
+ "update",
466
+ "--repair",
467
+ "--no-remote",
468
+ "--no-npm",
469
+ ], {
470
+ cwd: root,
471
+ env,
472
+ encoding: "utf8",
473
+ });
474
+ assert(
475
+ result.status === 0,
476
+ `update must preflight-normalize settings runtime marker before calling the distro script\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
477
+ );
478
+ assert(
479
+ result.stdout.includes("Preflight: Migrated settings.json lastChangelogVersion") &&
480
+ result.stdout.includes("Preflight: Normalized settings.json"),
481
+ `update output must report preflight runtime marker migration\n${result.stdout}`,
482
+ );
483
+ const settings = JSON.parse(fs.readFileSync(path.join(repo, "settings.json"), "utf8"));
484
+ assert(settings.lastChangelogVersion === undefined, "update preflight migration must remove lastChangelogVersion from settings.json");
485
+ const state = JSON.parse(fs.readFileSync(path.join(home, ".pi", "pi67", "state.json"), "utf8"));
486
+ assert(
487
+ state.runtimeMarkers?.lastChangelogVersion?.value === "0.80.4",
488
+ "update preflight migration must persist lastChangelogVersion into ignored state",
489
+ );
490
+ const status = spawnSync("git", ["-C", repo, "status", "--short"], { encoding: "utf8" });
491
+ assert(status.status === 0, `git status failed for update preflight migration self-test\n${status.stderr}`);
492
+ assert(
493
+ !status.stdout.split(/\r?\n/).some((line) => line.trim().endsWith("settings.json")),
494
+ `settings.json should be clean after preflight marker migration\n${status.stdout}`,
495
+ );
496
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
497
+ }
498
+
347
499
  function runUpdatePlanSelfTests() {
348
500
  assert(
349
501
  classifyGitShort(" M settings.json\n?? tmp.txt").preservedRuntime.includes("settings.json"),
@@ -61,6 +61,8 @@ export async function updateCommand(ctx, argv) {
61
61
  info(`Preserved runtime backup skipped: ${lifecycle.backupReason}`);
62
62
  }
63
63
 
64
+ reportSettingsRuntimeStateMigration(ctx, { dryRun, phase: "Preflight" });
65
+
64
66
  const args = isWindows()
65
67
  ? buildWindowsUpdateArgs(ctx, options, dryRun)
66
68
  : buildBashUpdateArgs(ctx, options, dryRun);
@@ -68,7 +70,7 @@ export async function updateCommand(ctx, argv) {
68
70
  runDistroScript(ctx, { sh: "pi67-update.sh", ps1: "pi67-update.ps1" }, args, { dryRun: false });
69
71
  if (!dryRun) {
70
72
  writeState(ctx, options.repair ? "repair" : "update");
71
- reportSettingsRuntimeStateMigration(ctx);
73
+ reportSettingsRuntimeStateMigration(ctx, { phase: "Post-update" });
72
74
  }
73
75
  } finally {
74
76
  lifecycle.release();
@@ -86,19 +88,22 @@ export async function updateCommand(ctx, argv) {
86
88
  }
87
89
  }
88
90
 
89
- function reportSettingsRuntimeStateMigration(ctx) {
91
+ function reportSettingsRuntimeStateMigration(ctx, options = {}) {
92
+ const dryRun = Boolean(options.dryRun);
93
+ const phase = options.phase ? `${options.phase}: ` : "";
90
94
  const result = migrateSettingsRuntimeState(ctx, {
91
95
  normalizeSettingsJson: true,
92
96
  installGitFilter: true,
97
+ dryRun,
93
98
  });
94
- if (result.markerFound && result.stateWritten) {
95
- info("Migrated settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json");
99
+ if (result.markerFound && (result.stateWritten || dryRun)) {
100
+ info(`${phase}${dryRun ? "would migrate" : "Migrated"} settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json`);
96
101
  }
97
102
  if (result.settingsNormalized) {
98
- info("Normalized settings.json by removing runtime-only lastChangelogVersion.");
103
+ info(`${phase}${dryRun ? "would normalize" : "Normalized"} settings.json by removing runtime-only lastChangelogVersion.`);
99
104
  }
100
105
  if (result.gitFilterInstalled) {
101
- info("Installed local git clean filter for future settings.json runtime markers.");
106
+ info(`${phase}${dryRun ? "would install" : "Installed"} local git clean filter for future settings.json runtime markers.`);
102
107
  }
103
108
  for (const error of result.errors) {
104
109
  warn(`settings runtime marker migration skipped: ${error}`);
@@ -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: readTextIfExists(path.join(ctx.repoRoot, "VERSION")).trim(),
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() {
@@ -138,6 +138,32 @@ export function installSettingsRuntimeGitFilter(ctx, options = {}) {
138
138
  }
139
139
 
140
140
  const cleanCommand = `node ${SETTINGS_RUNTIME_FILTER_SCRIPT} --clean`;
141
+ const existingClean = captureCommand("git", [
142
+ "-C",
143
+ ctx.repoRoot,
144
+ "config",
145
+ "--local",
146
+ "--get",
147
+ `filter.${SETTINGS_RUNTIME_FILTER_NAME}.clean`,
148
+ ]);
149
+ const existingRequired = captureCommand("git", [
150
+ "-C",
151
+ ctx.repoRoot,
152
+ "config",
153
+ "--local",
154
+ "--get",
155
+ `filter.${SETTINGS_RUNTIME_FILTER_NAME}.required`,
156
+ ]);
157
+ if (
158
+ existingClean.ok &&
159
+ existingClean.stdout.trim() === cleanCommand &&
160
+ existingRequired.ok &&
161
+ existingRequired.stdout.trim() === "false"
162
+ ) {
163
+ result.skipped = "settings runtime filter already installed";
164
+ return result;
165
+ }
166
+
141
167
  const clean = captureCommand("git", [
142
168
  "-C",
143
169
  ctx.repoRoot,