halfcycle 0.4.3 → 0.4.4

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.
@@ -235,27 +235,37 @@ export declare function describeAccount(serviceUrl: string, credential: string,
235
235
  * scrollback — and `--verbose` puts this run's id back on screen for the run that
236
236
  * wants it.
237
237
  *
238
- * WITH NO EMAIL the id is all there is, and it is printed: an unnamed account named by
239
- * its row is better than an unnamed account.
238
+ * WITH NO EMAIL BUT AN ANSWERED ACCOUNT the id is all there is, and it is printed —
239
+ * ONLY when `identity` itself is defined: an unnamed account named by its row is better
240
+ * than an unnamed account, but that is a claim about what `GET /account` just said, not
241
+ * about a credential this run merely holds. `(no email on record for it)` is the
242
+ * accepted exception to the "no internal words" scan precisely because it is scoped to
243
+ * this one case.
240
244
  *
241
- * WHEN NOTHING IS KNOWN IT SAYS SO, and does not invent. `'the account this machine
242
- * signed in as'` is a true sentence about an unnamed account; `'unknown'` printed where
243
- * a name goes reads as a name.
245
+ * **`identity === undefined` MEANS THE SAME THING EVERYWHERE THIS FUNCTION IS CALLED,
246
+ * AND IT MEANS "SAY NOTHING THAT CLAIMS AN ANSWER" (onboarding fix wave 2, F-01, the
247
+ * review's ruling on RW-02/RW-03 F-2, 2026-10-18).** `storedAccountId` is a credential
248
+ * this machine holds, not a fact the service stated — printing it, or printing
249
+ * "(no email on record for it)" beside it, asserts an answer this run never got.
250
+ * EVERY CALLER funnels through this one branch rather than deciding per-caller: `bin.ts`'s
251
+ * final report never reaches it (guarded by `identity !== undefined` before the call), and
252
+ * `confirm-identity.ts`'s own preamble (`accountLine`, gated the same way) and its stop
253
+ * remedy both go through it. The first cut of this fix gated only the preamble and left the
254
+ * stop remedy printing `account <id> (no email on record for it)` offline — the review's
255
+ * finding that fixing one caller and not the function itself reproduces the defect at the
256
+ * next call site.
244
257
  *
245
- * THE CALLER DECIDES WHETHER TO PRINT AT ALL, not this function. `bin.ts`'s final
246
- * report (E8, the wording batch, onboarding fix wave 2) only calls this when `GET
247
- * /account` actually answered with an account — a network error, a timeout, a 5xx
248
- * and a reached-but-unidentifiable answer (a 401/403) are all withheld there, rather
249
- * than printed through a `confirmed` flag here (onboarding fix wave T-16, #1567's
250
- * original shape, since removed: the flag's one caller was that same report line,
251
- * and the line it produced — "this run could not reach the service to confirm who
252
- * that is" — is now simply never printed, which is the more honest of the two). A
253
- * `storedAccountId` with no `identity` can still reach the "no email" branch below
254
- * through `confirm-identity.ts`'s OWN prompt line, a different surface with a
255
- * different, still-true claim: a stored or environment credential's cached id,
256
- * named while this run's own fresh identity read is still pending.
258
+ * **RETURNS `undefined` FOR THAT CASE RATHER THAN A SPLICED-IN NOUN PHRASE (round 2 of the
259
+ * same review, 2026-10-18).** A first attempt here returned a generic phrase ("the account
260
+ * this machine's saved sign-in belongs to" / "this credential's account") for every caller
261
+ * to interpolate — which reads naturally nowhere it is actually interpolated ("This
262
+ * machine's saved sign-in is for the account this machine's saved sign-in belongs to" / "is
263
+ * the credential for this credential's account"). Pushing the UNKNOWN case out to
264
+ * `undefined` keeps the one rule here (no id, no email claim, when `identity` is absent)
265
+ * while leaving each caller free to write its own grammatically whole sentence for that
266
+ * case — `armScript` below is the one caller that needs to, and does.
257
267
  */
258
- export declare function accountLabel(identity: AccountIdentity | undefined, storedAccountId: string | undefined, verbose?: boolean): string;
268
+ export declare function accountLabel(identity: AccountIdentity | undefined, storedAccountId: string | undefined, verbose?: boolean): string | undefined;
259
269
  /**
260
270
  * Read one line from the terminal. Kept out of the decision so the decision is testable.
261
271
  * Also the Git for Windows offer's prompt (`pre-create-checks.ts`), so the terminal is
@@ -1 +1 @@
1
- {"version":3,"file":"confirm-identity.d.ts","sourceRoot":"","sources":["../src/confirm-identity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"confirm-identity.d.ts","sourceRoot":"","sources":["../src/confirm-identity.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1356,12 +1356,20 @@ function createProjectIdentityFile(targetRepoRoot) {
1356
1356
  }
1357
1357
 
1358
1358
  // dist/install-manifest.js
1359
+ import { Buffer } from "node:buffer";
1359
1360
  import { createHash } from "node:crypto";
1360
1361
  var INSTALL_MANIFEST_REL = ".halfcycle/install-manifest.json";
1361
1362
  var INSTALL_MANIFEST_FORMAT = "halfcycle-install-manifest/v1";
1362
1363
  function sha256Hex(bytes) {
1363
1364
  return createHash("sha256").update(bytes).digest("hex");
1364
1365
  }
1366
+ function lineKey(line) {
1367
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
1368
+ }
1369
+ function fileSha256(bytes) {
1370
+ const text = typeof bytes === "string" ? bytes : Buffer.from(bytes).toString("utf-8");
1371
+ return sha256Hex(text.split("\n").map(lineKey).join("\n"));
1372
+ }
1365
1373
  function canonicalJson(value) {
1366
1374
  if (Array.isArray(value))
1367
1375
  return value.map(canonicalJson);
@@ -1544,7 +1552,7 @@ function startManifest(previous, isMember, isMemberDir) {
1544
1552
  };
1545
1553
  }
1546
1554
  function recordInstalledFile(draft, path, bytes) {
1547
- draft.files.set(path, { path, sha256: sha256Hex(bytes) });
1555
+ draft.files.set(path, { path, sha256: fileSha256(bytes) });
1548
1556
  }
1549
1557
  function recordRemovedFile(draft, path) {
1550
1558
  draft.files.delete(path);
@@ -1871,17 +1879,17 @@ function sha256OfWhatThisVersionWrites(commands) {
1871
1879
  const out = /* @__PURE__ */ new Map();
1872
1880
  const fromFile = (rel, src) => {
1873
1881
  if (existsSync3(src))
1874
- out.set(rel, sha256Hex(readFileSync4(src)));
1882
+ out.set(rel, fileSha256(readFileSync4(src)));
1875
1883
  };
1876
1884
  for (const cmd of commands)
1877
1885
  fromFile(commandStubRel(cmd.name), join5(BUNDLE_ROOT, ".claude-plugin", cmd.path));
1878
1886
  for (const [rel, content] of hookScriptsThisVersionWrites())
1879
- out.set(rel, sha256Hex(content));
1887
+ out.set(rel, fileSha256(content));
1880
1888
  fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
1881
1889
  return out;
1882
1890
  }
1883
1891
  function bytesAreOurs(path, bytes, recorded, writeSet) {
1884
- const hash = sha256Hex(bytes);
1892
+ const hash = fileSha256(bytes);
1885
1893
  const recordedHash = recorded !== void 0 && "sha256" in recorded ? recorded.sha256 : void 0;
1886
1894
  return hash === recordedHash || hash === writeSet.generatedSha256.get(path);
1887
1895
  }
@@ -2069,16 +2077,17 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
2069
2077
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2070
2078
  }
2071
2079
  let replacedExisting = false;
2072
- if (existsSync3(targetAbsPath)) {
2073
- const current = readFileSync4(targetAbsPath);
2074
- if (current.toString("utf-8") === content)
2080
+ const current = existsSync3(targetAbsPath) ? readFileSync4(targetAbsPath) : null;
2081
+ const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
2082
+ if (current !== null) {
2083
+ if (current.toString("utf-8") === text)
2075
2084
  return { outcome: "skipped", replacedExisting: false };
2076
2085
  if (!isOurs(current))
2077
2086
  return { outcome: "collided", replacedExisting: false };
2078
2087
  replacedExisting = true;
2079
2088
  }
2080
2089
  mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2081
- writeFileSync4(targetAbsPath, content, "utf-8");
2090
+ writeFileSync4(targetAbsPath, text, "utf-8");
2082
2091
  return { outcome: "written", replacedExisting };
2083
2092
  }
2084
2093
  var INSTALLER_OWNED_DIR = ".halfcycle/";
@@ -2090,11 +2099,12 @@ function writeOwned(targetAbsPath, targetRepoRoot, content) {
2090
2099
  if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
2091
2100
  throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (writeOwnedGenerated) rather than assumed from where it sits.`);
2092
2101
  }
2093
- if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
2102
+ const current = readFileOrNull(targetAbsPath);
2103
+ const text = inLineEndingOf(current, content);
2104
+ if (current === text)
2094
2105
  return "skipped";
2095
- }
2096
2106
  mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2097
- writeFileSync4(targetAbsPath, content, "utf-8");
2107
+ writeFileSync4(targetAbsPath, text, "utf-8");
2098
2108
  return "written";
2099
2109
  }
2100
2110
  var GENERATED_HEADER_LINE_INDEX = 1;
@@ -2496,13 +2506,13 @@ function adoptBlockUnderHeader(before, header, claimable) {
2496
2506
  }
2497
2507
  return claimed.length > 0 ? claimed.join("\n") + "\n" : "";
2498
2508
  }
2499
- function lineKey(line) {
2500
- return line.endsWith("\r") ? line.slice(0, -1) : line;
2501
- }
2502
2509
  function lineEndingOf(text) {
2503
2510
  const at = text.indexOf("\n");
2504
2511
  return at > 0 && text[at - 1] === "\r" ? "\r\n" : "\n";
2505
2512
  }
2513
+ function inLineEndingOf(existing, generated) {
2514
+ return existing === null ? generated : generated.split("\n").join(lineEndingOf(existing));
2515
+ }
2506
2516
  function gitignoreCovers(content) {
2507
2517
  const lines = new Set(content.split("\n").map((l) => l.trim()));
2508
2518
  return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
@@ -2595,7 +2605,8 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
2595
2605
  ...projectPage !== void 0 ? { projectPage } : {}
2596
2606
  };
2597
2607
  const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
2598
- writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
2608
+ const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(pin, null, 2) + "\n");
2609
+ writeAllowlisted(pinPath, targetRepoRoot, pinText, writtenPaths);
2599
2610
  }
2600
2611
  function writeCrewRoster(targetRepoRoot, report) {
2601
2612
  const doc = {
@@ -2743,7 +2754,7 @@ async function install(options) {
2743
2754
  }
2744
2755
  recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted);
2745
2756
  const mergedSettings = mergeSettings(existingSettings, generatedSettings);
2746
- const mergedSettingsText = JSON.stringify(mergedSettings, null, 2) + "\n";
2757
+ const mergedSettingsText = inLineEndingOf(readFileOrNull(settingsPath), JSON.stringify(mergedSettings, null, 2) + "\n");
2747
2758
  mkdirSync4(dirname3(settingsPath), { recursive: true });
2748
2759
  writeFileSync4(settingsPath, mergedSettingsText, "utf-8");
2749
2760
  (settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
@@ -2771,7 +2782,7 @@ async function install(options) {
2771
2782
  if (credential) {
2772
2783
  const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
2773
2784
  const existingMcp = existsSync3(mcpPath) ? readFileSync4(mcpPath, "utf-8") : null;
2774
- const mcpContent = generateMcpRegistration(existingMcp, credential.mcpUrl);
2785
+ const mcpContent = inLineEndingOf(existingMcp, generateMcpRegistration(existingMcp, credential.mcpUrl));
2775
2786
  recordMcpMerge(draft, existingMcp, mcpContent);
2776
2787
  if (existingMcp === null) {
2777
2788
  writeAllowlisted(mcpPath, targetRepo, mcpContent, report.writtenPaths);