halfcycle 0.4.1 → 0.4.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
5
5
  "commands": [
6
6
  {
package/README.md CHANGED
@@ -165,7 +165,7 @@ Nothing you already had is changed. Existing settings are preserved, and a name
165
165
  ## Requirements
166
166
 
167
167
  - Claude Code **2.1.118+**
168
- - Node 20+
168
+ - Node 18+
169
169
  - A git repository
170
170
  - On Windows, in PowerShell, use `npx.cmd` instead of `npx` (PowerShell's default settings block the `npx` script; `npx.cmd` always works, with nothing to change).
171
171
 
@@ -15376,6 +15376,9 @@ function infraWarningText() {
15376
15376
  function localFaultText(message) {
15377
15377
  return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
15378
15378
  }
15379
+ function noRepositoryText(failure) {
15380
+ return failure.gitMissing ? "[Halfcycle] WARNING: Git is not installed on this machine, and Halfcycle's checks need it, so nothing will be checked in this session. Install Git, then start a new session." : localFaultText(`Could not find git repo root: ${failure.message}`);
15381
+ }
15379
15382
  function contractFaultText(message) {
15380
15383
  return `[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
15381
15384
  This is not a service outage: the guard service answered. The suspect is this repository's own installed halfcycle runner, which is out of date with what the service now sends. Update it: run "npx halfcycle" in this repository, then retry. Session continues (fail-open).`;
@@ -15711,7 +15714,7 @@ async function attempt(url2, token, request, fetchFn, retry) {
15711
15714
 
15712
15715
  // dist/git.js
15713
15716
  import { spawnSync } from "node:child_process";
15714
- import { readFileSync } from "node:fs";
15717
+ import { existsSync, readFileSync } from "node:fs";
15715
15718
  import { relative } from "node:path";
15716
15719
  var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
15717
15720
  function git(args2, cwd, timeoutMs) {
@@ -15722,7 +15725,8 @@ function git(args2, cwd, timeoutMs) {
15722
15725
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
15723
15726
  });
15724
15727
  if (result.error) {
15725
- return { ok: false, message: `git error: ${result.error.message}` };
15728
+ const missing = result.error.code === "ENOENT" && existsSync(cwd);
15729
+ return { ok: false, message: `git error: ${result.error.message}`, ...missing ? { gitMissing: true } : {} };
15726
15730
  }
15727
15731
  if (result.status !== 0) {
15728
15732
  const stderr = result.stderr?.trim() ?? "";
@@ -16605,7 +16609,7 @@ function resolveWireFields(lines, decls) {
16605
16609
  }
16606
16610
 
16607
16611
  // dist/fixtures-manifest.js
16608
- import { existsSync, mkdirSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
16612
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
16609
16613
  import { dirname, join as join2 } from "node:path";
16610
16614
  function manifestPath(repoRoot) {
16611
16615
  return join2(repoRoot, "test", "fixtures", "captured", "manifest.json");
@@ -16644,7 +16648,7 @@ function upsertManifest(repoRoot, entry) {
16644
16648
  }
16645
16649
  function removeFixtureIfExists(path) {
16646
16650
  try {
16647
- if (existsSync(path))
16651
+ if (existsSync2(path))
16648
16652
  unlinkSync(path);
16649
16653
  } catch {
16650
16654
  }
@@ -17266,7 +17270,7 @@ function cleanRunSummary(verdict, scope) {
17266
17270
  }
17267
17271
 
17268
17272
  // dist/credential-store.js
17269
- import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
17273
+ import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
17270
17274
  import { homedir as homedir2, platform } from "node:os";
17271
17275
  import { dirname as dirname3, join as join5 } from "node:path";
17272
17276
  var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
@@ -17275,7 +17279,7 @@ function composeEngagementEnvPath(engagementId, home) {
17275
17279
  return join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, ENGAGEMENT_ENV_FILENAME);
17276
17280
  }
17277
17281
  function hasNotThisAccountMarker(engagementId, home) {
17278
- return existsSync2(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, NOT_THIS_ACCOUNT_MARKER_FILENAME));
17282
+ return existsSync3(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, NOT_THIS_ACCOUNT_MARKER_FILENAME));
17279
17283
  }
17280
17284
  function readAccountCredential(controlOrigin, home) {
17281
17285
  let raw;
@@ -17467,7 +17471,7 @@ async function evaluateWithRefresh(opts) {
17467
17471
 
17468
17472
  // dist/hooks/marker.js
17469
17473
  import { createHash } from "node:crypto";
17470
- import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17474
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17471
17475
  import { tmpdir } from "node:os";
17472
17476
  import { join as join6 } from "node:path";
17473
17477
  var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -17496,7 +17500,7 @@ function dedupePath(repoRoot, sessionId) {
17496
17500
  }
17497
17501
  function readMarker(repoRoot, sessionId) {
17498
17502
  const path = markerPath(repoRoot, sessionId);
17499
- if (!existsSync3(path))
17503
+ if (!existsSync4(path))
17500
17504
  return null;
17501
17505
  try {
17502
17506
  return readFileSync6(path, "utf-8").trim() || null;
@@ -17507,7 +17511,7 @@ function readMarker(repoRoot, sessionId) {
17507
17511
  function deleteMarker(repoRoot, sessionId) {
17508
17512
  for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17509
17513
  try {
17510
- if (existsSync3(path))
17514
+ if (existsSync4(path))
17511
17515
  unlinkSync3(path);
17512
17516
  } catch {
17513
17517
  }
@@ -17527,7 +17531,7 @@ function firstNoRepoNoticeThisSession(folder, sessionId) {
17527
17531
  }
17528
17532
  function readDedupe(repoRoot, sessionId) {
17529
17533
  const path = dedupePath(repoRoot, sessionId);
17530
- if (!existsSync3(path))
17534
+ if (!existsSync4(path))
17531
17535
  return null;
17532
17536
  try {
17533
17537
  const parsed = JSON.parse(readFileSync6(path, "utf-8"));
@@ -17599,7 +17603,7 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17599
17603
  }
17600
17604
 
17601
17605
  // dist/seat-configuration.js
17602
- import { existsSync as existsSync4, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17606
+ import { existsSync as existsSync5, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17603
17607
  import { homedir as homedir3 } from "node:os";
17604
17608
  import { join as join7, relative as relative3, resolve } from "node:path";
17605
17609
  function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
@@ -17708,7 +17712,7 @@ function readPathStates(projectDir, repoRoot) {
17708
17712
  const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
17709
17713
  if (!status.ok)
17710
17714
  return unknown2();
17711
- const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync4(join7(projectDir, rel)));
17715
+ const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync5(join7(projectDir, rel)));
17712
17716
  return mapped ?? unknown2();
17713
17717
  }
17714
17718
  function mapWiringStatus(output, prefix, paths, onDisk) {
@@ -17774,7 +17778,7 @@ async function evaluateEdit(reply, readSeat) {
17774
17778
  const repoRootResult = getRepoRoot(process.cwd());
17775
17779
  if (!repoRootResult.ok) {
17776
17780
  if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
17777
- reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17781
+ reply.notice(noRepositoryText(repoRootResult));
17778
17782
  }
17779
17783
  return;
17780
17784
  }
@@ -17934,7 +17938,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
17934
17938
  const repoRootResult = getRepoRoot(process.cwd());
17935
17939
  if (!repoRootResult.ok) {
17936
17940
  if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
17937
- reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17941
+ reply.notice(noRepositoryText(repoRootResult));
17938
17942
  }
17939
17943
  return;
17940
17944
  }
@@ -18110,14 +18114,14 @@ async function runStop() {
18110
18114
  }
18111
18115
 
18112
18116
  // dist/resolve-engagement.js
18113
- import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18117
+ import { existsSync as existsSync6, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18114
18118
  import { join as join8, sep as sep2 } from "node:path";
18115
18119
  import { homedir as homedir4 } from "node:os";
18116
18120
  function defaultRegistryPath() {
18117
18121
  return join8(homedir4(), ".halfcycle", "registry.json");
18118
18122
  }
18119
18123
  function readRegistry(registryPath = defaultRegistryPath()) {
18120
- if (!existsSync5(registryPath))
18124
+ if (!existsSync6(registryPath))
18121
18125
  return {};
18122
18126
  try {
18123
18127
  const parsed = JSON.parse(readFileSync8(registryPath, "utf-8"));
@@ -18407,7 +18411,7 @@ async function refreshCredentialToken(opts) {
18407
18411
  return { token: refreshed.token };
18408
18412
  }
18409
18413
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
18410
- var CONNECTED_SENTENCE = "Halfcycle connected.";
18414
+ var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If Halfcycle's tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if /mcp does not list it, run "claude mcp reset-project-choices" in this folder and restart Claude Code.`;
18411
18415
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
18412
18416
  function registeredMcpUrl(projectRoot) {
18413
18417
  let parsed;
@@ -18522,8 +18526,11 @@ async function runRefreshCredential(opts) {
18522
18526
  const remedySaid = parts.some(namesTheSignInCommand);
18523
18527
  parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid));
18524
18528
  }
18525
- for (const said of renewalSaid)
18529
+ for (const said of renewalSaid) {
18530
+ if (namesTheSignInCommand(said) && parts.some(namesTheSignInCommand))
18531
+ continue;
18526
18532
  parts.push(said.trimEnd());
18533
+ }
18527
18534
  return parts.length === 0 ? "" : `${nonBlockingReply("SessionStart", parts.join("\n"))}
18528
18535
  `;
18529
18536
  } catch (err) {
@@ -18541,7 +18548,7 @@ async function runRefreshCredential(opts) {
18541
18548
  import { fileURLToPath } from "node:url";
18542
18549
 
18543
18550
  // dist/capture.js
18544
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6 } from "node:fs";
18551
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
18545
18552
  import { dirname as dirname5 } from "node:path";
18546
18553
  function runCapture(provider, endpoint, opts = {}) {
18547
18554
  const repoRoot = opts.repoRoot ?? process.cwd();
package/dist/cli.js CHANGED
@@ -1566,6 +1566,14 @@ function readRootCommit(targetRepoRoot) {
1566
1566
  function isGitWorkTree(targetRepoRoot) {
1567
1567
  return git(targetRepoRoot, ["rev-parse", "--is-inside-work-tree"]) === "true";
1568
1568
  }
1569
+ function gitIsInstalled() {
1570
+ try {
1571
+ execFileSync("git", ["--version"], { stdio: "ignore", env: gitEnvForNamedRepo() });
1572
+ return true;
1573
+ } catch {
1574
+ return false;
1575
+ }
1576
+ }
1569
1577
  function readRemote(targetRepoRoot) {
1570
1578
  return git(targetRepoRoot, ["remote", "get-url", "origin"]);
1571
1579
  }
@@ -2143,6 +2151,11 @@ function sha256OfWhatThisVersionWrites(commands) {
2143
2151
  fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
2144
2152
  return out;
2145
2153
  }
2154
+ function bytesAreOurs(path, bytes, recorded, writeSet) {
2155
+ const hash = sha256Hex(bytes);
2156
+ const recordedHash = recorded !== void 0 && "sha256" in recorded ? recorded.sha256 : void 0;
2157
+ return hash === recordedHash || hash === writeSet.generatedSha256.get(path);
2158
+ }
2146
2159
  function readPreviousManifest(targetRepo) {
2147
2160
  try {
2148
2161
  return parseManifest(readFileSync5(join5(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
@@ -2291,7 +2304,7 @@ function readPluginManifest() {
2291
2304
  const raw = readFileSync5(manifestPath, "utf-8");
2292
2305
  return JSON.parse(raw);
2293
2306
  }
2294
- function copyManifestCommands(manifest, targetRepo, report, draft) {
2307
+ function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
2295
2308
  for (const cmd of manifest.commands ?? []) {
2296
2309
  const srcFile = join5(BUNDLE_ROOT, ".claude-plugin", cmd.path);
2297
2310
  if (!existsSync3(srcFile)) {
@@ -2300,8 +2313,11 @@ function copyManifestCommands(manifest, targetRepo, report, draft) {
2300
2313
  const rel = commandStubRel(cmd.name);
2301
2314
  const destFile = join5(targetRepo, rel);
2302
2315
  const content = readFileSync5(srcFile, "utf-8");
2303
- const outcome = writeCollisionSafe(destFile, targetRepo, content);
2316
+ const recorded = draft.files.get(rel);
2317
+ const { outcome, replacedExisting } = writeCollisionSafe(destFile, targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
2304
2318
  record(report, outcome, rel);
2319
+ if (replacedExisting)
2320
+ report.replacedPaths.push(rel);
2305
2321
  noteFile(draft, targetRepo, rel, outcome);
2306
2322
  }
2307
2323
  }
@@ -2316,18 +2332,23 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
2316
2332
  writeFileSync5(targetAbsPath, content, "utf-8");
2317
2333
  writtenPaths.push(rel);
2318
2334
  }
2319
- function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
2335
+ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
2320
2336
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
2321
2337
  if (!isAllowlisted(rel)) {
2322
2338
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2323
2339
  }
2340
+ let replacedExisting = false;
2324
2341
  if (existsSync3(targetAbsPath)) {
2325
- const current = readFileSync5(targetAbsPath, "utf-8");
2326
- return current === content ? "skipped" : "collided";
2342
+ const current = readFileSync5(targetAbsPath);
2343
+ if (current.toString("utf-8") === content)
2344
+ return { outcome: "skipped", replacedExisting: false };
2345
+ if (!isOurs(current))
2346
+ return { outcome: "collided", replacedExisting: false };
2347
+ replacedExisting = true;
2327
2348
  }
2328
2349
  mkdirSync5(dirname3(targetAbsPath), { recursive: true });
2329
2350
  writeFileSync5(targetAbsPath, content, "utf-8");
2330
- return "written";
2351
+ return { outcome: "written", replacedExisting };
2331
2352
  }
2332
2353
  var INSTALLER_OWNED_DIR = ".halfcycle/";
2333
2354
  function writeOwned(targetAbsPath, targetRepoRoot, content) {
@@ -3003,7 +3024,7 @@ async function install(options) {
3003
3024
  const writeSet = closedWriteSet();
3004
3025
  const draft = startManifest(readPreviousManifest(targetRepo), (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
3005
3026
  const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
3006
- copyManifestCommands(manifest, targetRepo, report, draft);
3027
+ copyManifestCommands(manifest, targetRepo, report, draft, writeSet);
3007
3028
  const vendoredBinSrc = resolveVendoredBinary();
3008
3029
  const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
3009
3030
  const vendoredOutcome = recordOwned(report, vendoredBinDest, targetRepo, readFileSync5(vendoredBinSrc, "utf-8"), VENDORED_BIN_REL);
@@ -3329,8 +3350,8 @@ var SignInRefused = class extends Error {
3329
3350
  }
3330
3351
  };
3331
3352
  var SIGN_IN_TIMED_OUT = "timed-out";
3332
- var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle service stopped answering while this terminal was waiting for the browser. Nothing was created. Check your connection and run the command again.";
3333
- var SIGN_IN_NON_INTERACTIVE_MESSAGE = "Halfcycle needs a signed-in account and this looks like a non-interactive run (CI is set), so there is no browser to sign in with. Set HALFCYCLE_TOKEN in the environment to a Halfcycle credential and run this again. Nothing was created.";
3353
+ var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle service stopped answering while this terminal was waiting for the browser. Check your connection and run the command again.";
3354
+ var SIGN_IN_NON_INTERACTIVE_MESSAGE = "Halfcycle needs a signed-in account and this looks like a non-interactive run (CI is set), so there is no browser to sign in with. Set HALFCYCLE_TOKEN in the environment to a Halfcycle credential and run this again.";
3334
3355
  var DEADLINE_SLACK_INTERVALS = 1;
3335
3356
  var STILL_WAITING_MS = 15e3;
3336
3357
  var DEFAULT_SIGN_IN_REASON = "set up this project";
@@ -3425,6 +3446,9 @@ function isNonInteractive(env = process.env) {
3425
3446
  const ci = (env["CI"] ?? "").trim().toLowerCase();
3426
3447
  return ci !== "" && ci !== "0" && ci !== "false";
3427
3448
  }
3449
+ function isInteractiveRun(env = process.env) {
3450
+ return process.stdin.isTTY === true && !isNonInteractive(env);
3451
+ }
3428
3452
  async function startDeviceAuth(serviceUrl) {
3429
3453
  const url = `${serviceUrl.replace(/\/+$/, "")}/auth/device/start`;
3430
3454
  let res;
@@ -3435,16 +3459,16 @@ async function startDeviceAuth(serviceUrl) {
3435
3459
  body: "{}"
3436
3460
  });
3437
3461
  } catch (err) {
3438
- throw new SignInRefused(SIGN_IN_TIMED_OUT, `Could not reach the Halfcycle service at ${url} to start a sign-in: ${err instanceof Error ? err.message : String(err)}. Nothing was created.`);
3462
+ throw new SignInRefused(SIGN_IN_TIMED_OUT, `Could not reach the Halfcycle service at ${url} to start a sign-in: ${err instanceof Error ? err.message : String(err)}.`);
3439
3463
  }
3440
3464
  const body = await res.json().catch(() => null);
3441
3465
  if (res.status !== 201) {
3442
3466
  const refusal = tryRefusal(body);
3443
- throw new SignInRefused(refusal?.status ?? String(res.status), refusal?.message ?? `Sign-in could not be started: ${url} answered ${res.status}. Nothing was created.`);
3467
+ throw new SignInRefused(refusal?.status ?? String(res.status), refusal?.message ?? `Sign-in could not be started: ${url} answered ${res.status}.`);
3444
3468
  }
3445
3469
  const parsed = deviceAuthStartResponseSchema.safeParse(body);
3446
3470
  if (!parsed.success) {
3447
- throw new SignInRefused(SIGN_IN_TIMED_OUT, `The Halfcycle service at ${url} answered a sign-in start this client cannot read. Either that service is older than this installer, or the address is not a Halfcycle service. Nothing was created.`);
3471
+ throw new SignInRefused(SIGN_IN_TIMED_OUT, `The Halfcycle service at ${url} answered a sign-in start this client cannot read. Either that service is older than this installer, or the address is not a Halfcycle service.`);
3448
3472
  }
3449
3473
  return parsed.data;
3450
3474
  }
@@ -3709,7 +3733,7 @@ async function createEngagementForProject(baseUrl, projectId, credential, derive
3709
3733
  const body = { derivedName };
3710
3734
  return requestEngagementValues("PUT", `${baseUrl.replace(/\/+$/, "")}/engagements/by-project/${encodeURIComponent(projectId)}`, body, credential, {
3711
3735
  action: "Project setup",
3712
- nothingHappened: "This folder was not set up",
3736
+ nothingHappened: "Halfcycle did not finish setting up this folder",
3713
3737
  route: "PUT /engagements/by-project/:projectId",
3714
3738
  // FALSE on this call, and only this one: the folder's id is already on
3715
3739
  // disk by the time this request is sent. See CallShape.alsoNothingWritten.
@@ -4159,9 +4183,6 @@ function accountLabel(identity, storedAccountId, verbose2 = false, confirmed = t
4159
4183
  return `account ${accountId} (no email on record for it)`;
4160
4184
  return "the account this credential belongs to, which this service did not name";
4161
4185
  }
4162
- function isInteractiveRun(env = process.env) {
4163
- return process.stdin.isTTY === true && !isNonInteractive(env);
4164
- }
4165
4186
  function onlyDate(iso) {
4166
4187
  if (iso === void 0)
4167
4188
  return void 0;
@@ -4232,7 +4253,7 @@ function armScript(credential, act, label, plainLabel, home) {
4232
4253
  noTerminalNote: "",
4233
4254
  switchOpening: `[halfcycle] Signing you in through your browser \u2014 this install will use that account instead.
4234
4255
  `,
4235
- stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. HALFCYCLE_TOKEN in this environment is the credential for ${plainLabel}, and it takes precedence over any saved sign-in \u2014 so unset it, or set it to your own account's credential, before running this again.`,
4256
+ stopRemedy: `Stopped before anything was set up. HALFCYCLE_TOKEN in this environment is the credential for ${plainLabel}, and it takes precedence over any saved sign-in \u2014 so unset it, or set it to your own account's credential, before running this again.`,
4236
4257
  // The switch is real for THIS run (the new credential is what gets used) and it
4237
4258
  // does not persist: `obtainCredential` reads the variable first, so the next run
4238
4259
  // is the same variable again. Said here rather than discovered tomorrow.
@@ -4258,7 +4279,7 @@ function armScript(credential, act, label, plainLabel, home) {
4258
4279
  `,
4259
4280
  switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
4260
4281
  `,
4261
- stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. This machine's saved sign-in is for ${plainLabel}. Run this again and answer "s" to sign in as a different account, or remove ${accountStorePath(home)} to be asked for a sign-in next time.`,
4282
+ stopRemedy: `Stopped before anything was set up. This machine's saved sign-in is for ${plainLabel}. Run this again and answer "s" to sign in as a different account, or remove ${accountStorePath(home)} to be asked for a sign-in next time.`,
4262
4283
  afterSwitch: ""
4263
4284
  };
4264
4285
  }
@@ -4415,6 +4436,7 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
4415
4436
  import { spawnSync } from "node:child_process";
4416
4437
  import { existsSync as existsSync4, readFileSync as readFileSync6, realpathSync } from "node:fs";
4417
4438
  import { delimiter, dirname as dirname4, join as join6 } from "node:path";
4439
+ var KEPT_FOLDER_ID_LINE = 'Nothing on the Halfcycle service needs undoing, and this folder keeps its project id, so running "npx halfcycle" again picks up where it left off.';
4418
4440
  var PreCreateRefused = class extends Error {
4419
4441
  constructor(message) {
4420
4442
  super(message);
@@ -4497,18 +4519,22 @@ function claimFolderId(targetRepo) {
4497
4519
  } catch {
4498
4520
  throw new PreCreateRefused(`cannot write to ${targetRepo} \u2014 run "npx halfcycle" in a folder you can write to.`);
4499
4521
  }
4500
- const file = join6(targetRepo, PROJECT_IDENTITY_REL);
4522
+ const folderId = readFolderId(targetRepo);
4523
+ if (folderId === void 0) {
4524
+ throw new PreCreateRefused(`${join6(targetRepo, PROJECT_IDENTITY_REL)} could not be read as a Halfcycle project file \u2014 delete the .halfcycle folder in ${targetRepo} and run "npx halfcycle" again to start a new project here.`);
4525
+ }
4526
+ return folderId;
4527
+ }
4528
+ function readFolderId(targetRepo) {
4501
4529
  let folderId;
4502
4530
  try {
4503
- folderId = JSON.parse(readFileSync6(file, "utf-8"))?.projectId;
4531
+ const text = readFileSync6(join6(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
4532
+ folderId = JSON.parse(text)?.projectId;
4504
4533
  } catch {
4505
- folderId = void 0;
4534
+ return void 0;
4506
4535
  }
4507
4536
  const parsed = projectIdSchema.safeParse(folderId);
4508
- if (!parsed.success) {
4509
- throw new PreCreateRefused(`${file} could not be read as a Halfcycle project file \u2014 delete the .halfcycle folder in ${targetRepo} and run "npx halfcycle" again to start a new project here.`);
4510
- }
4511
- return parsed.data;
4537
+ return parsed.success ? parsed.data : void 0;
4512
4538
  }
4513
4539
  function checkSettingsParse(targetRepo) {
4514
4540
  try {
@@ -6111,16 +6137,15 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
6111
6137
  return;
6112
6138
  }
6113
6139
  if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
6114
- let hash;
6140
+ let bytes;
6115
6141
  try {
6116
- hash = sha256Hex(readFileSync9(abs));
6142
+ bytes = readFileSync9(abs);
6117
6143
  } catch {
6118
6144
  name(lists.failed, path, path);
6119
6145
  return;
6120
6146
  }
6121
- const recordedHash = entry !== void 0 && "sha256" in entry ? entry.sha256 : void 0;
6122
- if (hash !== recordedHash && hash !== writeSet.generatedSha256.get(path)) {
6123
- name(recordedHash !== void 0 ? lists.kept : lists.leftAlone, path, path);
6147
+ if (!bytesAreOurs(path, bytes, entry, writeSet)) {
6148
+ name(entry !== void 0 && "sha256" in entry ? lists.kept : lists.leftAlone, path, path);
6124
6149
  return;
6125
6150
  }
6126
6151
  }
@@ -6732,7 +6757,8 @@ ${PROJECT_RESET_LINE}
6732
6757
  process.stdout.write(`[halfcycle] Commit the files Halfcycle set up here, including ${BUNDLE_PIN_REL} and ${PROJECT_IDENTITY_REL}, so every copy of this project joins it and runs the same checks.
6733
6758
  `);
6734
6759
  } else if (credential) {
6735
- process.stdout.write(`[halfcycle] This folder is not a git repository. Halfcycle's checks compare against your git history, so run "git init" here to get checks on every edit.
6760
+ process.stdout.write(process.platform !== "win32" && !gitIsInstalled() ? `[halfcycle] Halfcycle needs Git, and it is not installed on this machine \u2014 get it from https://git-scm.com/downloads, then run "git init" here to get checks on every edit.
6761
+ ` : `[halfcycle] This folder is not a git repository. Halfcycle's checks compare against your git history, so run "git init" here to get checks on every edit.
6736
6762
  `);
6737
6763
  }
6738
6764
  if (credential) {
@@ -6759,22 +6785,23 @@ ${PROJECT_RESET_LINE}
6759
6785
  `);
6760
6786
  process.exit(0);
6761
6787
  } catch (err) {
6762
- if (err instanceof SignInRefused || err instanceof PreCreateRefused || err instanceof SettingsParseRefused) {
6763
- process.stderr.write(`halfcycle: ${err.message}
6764
- `);
6788
+ const stop = (message) => {
6789
+ const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
6790
+ ` : "";
6791
+ process.stderr.write(`${message}
6792
+ ${kept}`);
6765
6793
  process.exit(1);
6794
+ };
6795
+ if (err instanceof SignInRefused || err instanceof PreCreateRefused || err instanceof SettingsParseRefused) {
6796
+ stop(`halfcycle: ${err.message}`);
6766
6797
  return;
6767
6798
  }
6768
6799
  const fsCode = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
6769
6800
  if (fsCode !== void 0) {
6770
- process.stderr.write(`halfcycle: could not write to ${targetRepo} \u2014 check that you can write to this folder and run "npx halfcycle" again.
6771
- `);
6772
- process.exit(1);
6801
+ stop(`halfcycle: could not write to ${targetRepo} \u2014 check that you can write to this folder and run "npx halfcycle" again.`);
6773
6802
  return;
6774
6803
  }
6775
- process.stderr.write(`halfcycle install failed: ${err instanceof Error ? err.message : String(err)}
6776
- `);
6777
- process.exit(1);
6804
+ stop(`halfcycle install failed: ${err instanceof Error ? err.message : String(err)}`);
6778
6805
  }
6779
6806
  return;
6780
6807
  }