@bigking67/pi-67 0.10.12 → 0.10.14

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.14]
4
+
5
+ - Refreshes the Git index stat cache for `settings.json` during
6
+ `update --repair` when Git reports `M settings.json` but `git diff` has no
7
+ real content changes, clearing the remaining Windows false-dirty state after
8
+ line-ending/runtime-state cleanup.
9
+
10
+ ## [0.10.13]
11
+
12
+ - Normalizes `settings.json` BOM/CRLF line endings during `update --repair`
13
+ even when the runtime `lastChangelogVersion` marker is already absent,
14
+ clearing Windows Git EOL false-dirty states without changing provider,
15
+ model, theme, or other JSON content.
16
+
3
17
  ## [0.10.12]
4
18
 
5
19
  - Pins `settings.json` to LF line endings in `.gitattributes` while preserving
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigking67/pi-67",
3
- "version": "0.10.12",
3
+ "version": "0.10.14",
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
@@ -9,7 +9,9 @@ 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
11
  import {
12
+ migrateSettingsRuntimeState,
12
13
  mergeSettingsRuntimeMarkerIntoState,
14
+ refreshSettingsGitIndex,
13
15
  settingsRuntimeMarkerFromObject,
14
16
  stripSettingsRuntimeMarkerText,
15
17
  } from "../src/lib/settings-runtime-state.mjs";
@@ -406,6 +408,54 @@ function runSettingsRuntimeStateSelfTests() {
406
408
  state.runtimeMarkers.lastChangelogVersion.storage === "state.json",
407
409
  "settings runtime marker must migrate into state runtimeMarkers",
408
410
  );
411
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-settings-runtime-state-"));
412
+ const agentDir = path.join(tmpRoot, "agent");
413
+ const stateDir = path.join(tmpRoot, "state");
414
+ fs.mkdirSync(agentDir, { recursive: true });
415
+ fs.writeFileSync(path.join(agentDir, "settings.json"), "{\r\n \"theme\": \"gruvbox-dark\"\r\n}\r\n", "utf8");
416
+ const lineEndingResult = migrateSettingsRuntimeState(
417
+ { agentDir, repoRoot: agentDir, stateDir },
418
+ { normalizeSettingsJson: true },
419
+ );
420
+ const normalizedSettings = fs.readFileSync(path.join(agentDir, "settings.json"), "utf8");
421
+ assert(lineEndingResult.markerFound === false, "line-ending normalization must not require runtime marker");
422
+ assert(lineEndingResult.settingsNormalized === true, "CRLF settings.json must be normalized under --normalize");
423
+ assert(
424
+ lineEndingResult.settingsNormalizeReasons.includes("line-endings"),
425
+ "settings normalization must classify CRLF as line-endings",
426
+ );
427
+ assert(
428
+ normalizedSettings === "{\n \"theme\": \"gruvbox-dark\"\n}\n",
429
+ "settings line-ending normalization must preserve JSON content and indentation",
430
+ );
431
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
432
+
433
+ if (spawnSync("git", ["--version"], { encoding: "utf8" }).status === 0) {
434
+ const gitRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-settings-index-refresh-"));
435
+ const gitStateDir = path.join(gitRoot, "state");
436
+ fs.writeFileSync(path.join(gitRoot, ".gitattributes"), "settings.json text eol=lf\n");
437
+ fs.writeFileSync(path.join(gitRoot, "settings.json"), "{\n \"theme\": \"gruvbox-dark\"\n}\n");
438
+ for (const args of [
439
+ ["-C", gitRoot, "init", "-q"],
440
+ ["-C", gitRoot, "config", "user.email", "pi67-check@example.invalid"],
441
+ ["-C", gitRoot, "config", "user.name", "pi67-check"],
442
+ ["-C", gitRoot, "add", ".gitattributes", "settings.json"],
443
+ ["-C", gitRoot, "commit", "-q", "-m", "init"],
444
+ ]) {
445
+ const result = spawnSync("git", args, { encoding: "utf8" });
446
+ assert(result.status === 0, `git setup failed for settings index refresh self-test: git ${args.join(" ")}\n${result.stderr}`);
447
+ }
448
+ fs.writeFileSync(path.join(gitRoot, "settings.json"), "{\r\n \"theme\": \"gruvbox-dark\"\r\n}\r\n");
449
+ const statusBefore = spawnSync("git", ["-C", gitRoot, "status", "--short", "--", "settings.json"], { encoding: "utf8" });
450
+ const diffBefore = spawnSync("git", ["-C", gitRoot, "diff", "--quiet", "--", "settings.json"], { encoding: "utf8" });
451
+ assert(statusBefore.stdout.includes("settings.json"), "settings index refresh self-test must start with false-dirty status");
452
+ assert(diffBefore.status === 0, "settings index refresh self-test must have no real content diff");
453
+ const refreshResult = refreshSettingsGitIndex({ agentDir: gitRoot, repoRoot: gitRoot, stateDir: gitStateDir });
454
+ const statusAfter = spawnSync("git", ["-C", gitRoot, "status", "--short", "--", "settings.json"], { encoding: "utf8" });
455
+ assert(refreshResult.refreshed === true, "settings index refresh must classify cleared false-dirty status");
456
+ assert(!statusAfter.stdout.trim(), `settings index refresh must clear false-dirty status\n${statusAfter.stdout}`);
457
+ fs.rmSync(gitRoot, { recursive: true, force: true });
458
+ }
409
459
  }
410
460
 
411
461
  function runUpdatePreflightMigrationSelfTests() {
@@ -58,7 +58,10 @@ export async function installCommand(ctx, argv) {
58
58
  info("Migrated settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json");
59
59
  }
60
60
  if (runtimeState.settingsNormalized) {
61
- info("Normalized settings.json by removing runtime-only lastChangelogVersion.");
61
+ info("Normalized settings.json runtime marker/line endings.");
62
+ }
63
+ if (runtimeState.gitIndexRefreshed) {
64
+ info("Refreshed settings.json Git index stat cache.");
62
65
  }
63
66
  if (runtimeState.gitFilterInstalled) {
64
67
  info("Installed local git clean filter for future settings.json runtime markers.");
@@ -100,7 +100,10 @@ function reportSettingsRuntimeStateMigration(ctx, options = {}) {
100
100
  info(`${phase}${dryRun ? "would migrate" : "Migrated"} settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json`);
101
101
  }
102
102
  if (result.settingsNormalized) {
103
- info(`${phase}${dryRun ? "would normalize" : "Normalized"} settings.json by removing runtime-only lastChangelogVersion.`);
103
+ info(`${phase}${dryRun ? "would normalize" : "Normalized"} settings.json runtime marker/line endings.`);
104
+ }
105
+ if (result.gitIndexRefreshed) {
106
+ info(`${phase}${dryRun ? "would refresh" : "Refreshed"} settings.json Git index stat cache.`);
104
107
  }
105
108
  if (result.gitFilterInstalled) {
106
109
  info(`${phase}${dryRun ? "would install" : "Installed"} local git clean filter for future settings.json runtime markers.`);
@@ -67,6 +67,9 @@ export function migrateSettingsRuntimeState(ctx, options = {}) {
67
67
  markerValue: "",
68
68
  stateWritten: false,
69
69
  settingsNormalized: false,
70
+ settingsNormalizeReasons: [],
71
+ gitIndexRefreshed: false,
72
+ gitIndexRefreshSkipped: "",
70
73
  gitFilterInstalled: false,
71
74
  skipped: [],
72
75
  errors: [],
@@ -75,10 +78,12 @@ export function migrateSettingsRuntimeState(ctx, options = {}) {
75
78
  if (!fs.existsSync(settingsPath)) {
76
79
  result.skipped.push("settings.json missing");
77
80
  } else {
81
+ let rawSettingsText = "";
78
82
  let settingsText = "";
79
83
  let settings = null;
80
84
  try {
81
- settingsText = fs.readFileSync(settingsPath, "utf8").replace(/^\uFEFF/, "");
85
+ rawSettingsText = fs.readFileSync(settingsPath, "utf8");
86
+ settingsText = rawSettingsText.replace(/^\uFEFF/, "");
82
87
  settings = JSON.parse(settingsText);
83
88
  } catch (error) {
84
89
  result.errors.push(`settings.json is not valid JSON: ${error.message}`);
@@ -95,14 +100,28 @@ export function migrateSettingsRuntimeState(ctx, options = {}) {
95
100
  }
96
101
  if (normalizeSettingsJson) {
97
102
  const normalizedText = stableSettingsJson(stripSettingsRuntimeMarker(settings));
98
- if (normalizedText !== settingsText) {
103
+ if (normalizedText !== rawSettingsText) {
99
104
  if (!dryRun) {
100
105
  fs.writeFileSync(settingsPath, normalizedText, "utf8");
101
106
  }
102
107
  result.settingsNormalized = true;
108
+ result.settingsNormalizeReasons.push("runtime-marker");
109
+ if (normalizeSettingsTextLineEndings(rawSettingsText) !== rawSettingsText) {
110
+ result.settingsNormalizeReasons.push("line-endings");
111
+ }
103
112
  }
104
113
  }
105
114
  } else {
115
+ if (normalizeSettingsJson && settings) {
116
+ const normalizedText = normalizeSettingsTextLineEndings(rawSettingsText);
117
+ if (normalizedText !== rawSettingsText) {
118
+ if (!dryRun) {
119
+ fs.writeFileSync(settingsPath, normalizedText, "utf8");
120
+ }
121
+ result.settingsNormalized = true;
122
+ result.settingsNormalizeReasons.push("line-endings");
123
+ }
124
+ }
106
125
  result.skipped.push("settings.json runtime marker absent");
107
126
  }
108
127
  }
@@ -114,6 +133,76 @@ export function migrateSettingsRuntimeState(ctx, options = {}) {
114
133
  if (filterResult.error) result.errors.push(filterResult.error);
115
134
  }
116
135
 
136
+ if (normalizeSettingsJson) {
137
+ const refreshResult = refreshSettingsGitIndex(ctx, { dryRun });
138
+ result.gitIndexRefreshed = refreshResult.refreshed;
139
+ result.gitIndexRefreshSkipped = refreshResult.skipped;
140
+ if (refreshResult.error) result.errors.push(refreshResult.error);
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ export function normalizeSettingsTextLineEndings(text) {
147
+ return String(text || "")
148
+ .replace(/^\uFEFF/, "")
149
+ .replace(/\r\n?/g, "\n");
150
+ }
151
+
152
+ export function refreshSettingsGitIndex(ctx, options = {}) {
153
+ const result = {
154
+ refreshed: false,
155
+ skipped: "",
156
+ error: "",
157
+ };
158
+ if (options.dryRun) {
159
+ result.refreshed = true;
160
+ return result;
161
+ }
162
+ if (!isGitRepo(ctx.repoRoot)) {
163
+ result.skipped = "repo root is not a git checkout";
164
+ return result;
165
+ }
166
+
167
+ const settingsPath = path.join(ctx.agentDir, "settings.json");
168
+ if (!fs.existsSync(settingsPath)) {
169
+ result.skipped = "settings.json missing";
170
+ return result;
171
+ }
172
+
173
+ const relativePath = path.relative(ctx.repoRoot, settingsPath);
174
+ if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
175
+ result.skipped = "settings.json is outside repo root";
176
+ return result;
177
+ }
178
+ const gitPath = relativePath.split(path.sep).join("/");
179
+
180
+ const statusBefore = captureCommand("git", ["-C", ctx.repoRoot, "status", "--short", "--", gitPath]);
181
+ if (!statusBefore.ok) {
182
+ result.skipped = "could not inspect settings.json git status before index refresh";
183
+ return result;
184
+ }
185
+ if (!statusBefore.stdout.trim()) {
186
+ result.skipped = "settings.json git index already fresh";
187
+ return result;
188
+ }
189
+
190
+ const diffBefore = captureCommand("git", ["-C", ctx.repoRoot, "diff", "--quiet", "--", gitPath]);
191
+ if (diffBefore.status !== 0) {
192
+ result.skipped = "settings.json has real content diff; index refresh left it visible";
193
+ return result;
194
+ }
195
+
196
+ const refresh = captureCommand("git", ["-C", ctx.repoRoot, "update-index", "--refresh", "--", gitPath]);
197
+
198
+ const statusAfter = captureCommand("git", ["-C", ctx.repoRoot, "status", "--short", "--", gitPath]);
199
+ if (statusAfter.ok && !statusAfter.stdout.trim()) {
200
+ result.refreshed = true;
201
+ } else if (!refresh.ok) {
202
+ result.error = refresh.stderr || refresh.error || "failed to refresh settings.json git index";
203
+ } else {
204
+ result.skipped = "settings.json still appears dirty after git index refresh";
205
+ }
117
206
  return result;
118
207
  }
119
208
 
@@ -51,7 +51,10 @@ function printHuman(result) {
51
51
  console.log("PASS settings.json runtime marker is already absent");
52
52
  }
53
53
  if (result.settingsNormalized) {
54
- console.log("PASS normalized settings.json by removing runtime-only lastChangelogVersion");
54
+ console.log("PASS normalized settings.json runtime marker/line endings");
55
+ }
56
+ if (result.gitIndexRefreshed) {
57
+ console.log("PASS refreshed settings.json Git index stat cache");
55
58
  }
56
59
  if (result.gitFilterInstalled) {
57
60
  console.log("PASS installed local git clean filter for settings.json runtime marker");