halfcycle 0.4.2 → 0.4.3
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/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +26 -6
- package/dist/claude-version-probe.d.ts.map +1 -1
- package/dist/cli.js +131 -59
- package/dist/cli.js.map +1 -1
- package/dist/confirm-identity.d.ts +27 -17
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -18
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +68 -9
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +35 -16
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/own-engagement.d.ts +8 -7
- package/dist/own-engagement.d.ts.map +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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/bin/bin.bundle.mjs
CHANGED
|
@@ -17517,11 +17517,16 @@ function deleteMarker(repoRoot, sessionId) {
|
|
|
17517
17517
|
}
|
|
17518
17518
|
}
|
|
17519
17519
|
}
|
|
17520
|
-
var
|
|
17521
|
-
|
|
17520
|
+
var SESSION_NOTICE_SUFFIX = {
|
|
17521
|
+
"no-repository": ".norepo",
|
|
17522
|
+
"credential-rejected": ".credential",
|
|
17523
|
+
"credential-renewed-for-this-run": ".renewed"
|
|
17524
|
+
};
|
|
17525
|
+
function firstNoticeThisSession(notice, folder, sessionId) {
|
|
17522
17526
|
if (!sessionId)
|
|
17523
17527
|
return true;
|
|
17524
|
-
const
|
|
17528
|
+
const suffix = SESSION_NOTICE_SUFFIX[notice];
|
|
17529
|
+
const path = join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}${suffix}`);
|
|
17525
17530
|
try {
|
|
17526
17531
|
writeFileSync3(path, "", { encoding: "utf-8", flag: "wx" });
|
|
17527
17532
|
return true;
|
|
@@ -17529,6 +17534,15 @@ function firstNoRepoNoticeThisSession(folder, sessionId) {
|
|
|
17529
17534
|
return err.code !== "EEXIST";
|
|
17530
17535
|
}
|
|
17531
17536
|
}
|
|
17537
|
+
function warnOnceThisSession(notice, folder, sessionId) {
|
|
17538
|
+
return (message) => {
|
|
17539
|
+
if (firstNoticeThisSession(notice, folder, sessionId))
|
|
17540
|
+
process.stderr.write(message);
|
|
17541
|
+
};
|
|
17542
|
+
}
|
|
17543
|
+
function firstNoRepoNoticeThisSession(folder, sessionId) {
|
|
17544
|
+
return firstNoticeThisSession("no-repository", folder, sessionId);
|
|
17545
|
+
}
|
|
17532
17546
|
function readDedupe(repoRoot, sessionId) {
|
|
17533
17547
|
const path = dedupePath(repoRoot, sessionId);
|
|
17534
17548
|
if (!existsSync4(path))
|
|
@@ -17805,7 +17819,8 @@ async function evaluateEdit(reply, readSeat) {
|
|
|
17805
17819
|
guardServiceUrl,
|
|
17806
17820
|
token: guardServiceToken,
|
|
17807
17821
|
engagementId: guardEngagementId,
|
|
17808
|
-
request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
|
|
17822
|
+
request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
|
|
17823
|
+
warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
|
|
17809
17824
|
});
|
|
17810
17825
|
const clientResult = evaluation.result;
|
|
17811
17826
|
if (!clientResult.ok) {
|
|
@@ -17820,6 +17835,8 @@ async function evaluateEdit(reply, readSeat) {
|
|
|
17820
17835
|
credentialRefused: failure.kind === "credential"
|
|
17821
17836
|
});
|
|
17822
17837
|
if (failure.kind === "credential") {
|
|
17838
|
+
if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
|
|
17839
|
+
return;
|
|
17823
17840
|
emitCredentialRejected(
|
|
17824
17841
|
failure.statusCode,
|
|
17825
17842
|
failure.message,
|
|
@@ -17981,7 +17998,8 @@ async function evaluateSession(input, kind, reply, readSeat) {
|
|
|
17981
17998
|
guardServiceUrl,
|
|
17982
17999
|
token: guardServiceToken,
|
|
17983
18000
|
engagementId: guardEngagementId,
|
|
17984
|
-
request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
|
|
18001
|
+
request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
|
|
18002
|
+
warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, sessionId)
|
|
17985
18003
|
});
|
|
17986
18004
|
const clientResult = evaluation.result;
|
|
17987
18005
|
if (!clientResult.ok) {
|
|
@@ -17997,6 +18015,8 @@ async function evaluateSession(input, kind, reply, readSeat) {
|
|
|
17997
18015
|
credentialRefused: failure.kind === "credential"
|
|
17998
18016
|
});
|
|
17999
18017
|
if (failure.kind === "credential") {
|
|
18018
|
+
if (!firstNoticeThisSession("credential-rejected", repoRoot, sessionId))
|
|
18019
|
+
return;
|
|
18000
18020
|
emitCredentialRejected(
|
|
18001
18021
|
failure.statusCode,
|
|
18002
18022
|
failure.message,
|
|
@@ -18411,7 +18431,7 @@ async function refreshCredentialToken(opts) {
|
|
|
18411
18431
|
return { token: refreshed.token };
|
|
18412
18432
|
}
|
|
18413
18433
|
var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
|
|
18414
|
-
var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If
|
|
18434
|
+
var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If its tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if it is not listed, run "claude mcp reset-project-choices" here and restart Claude Code.`;
|
|
18415
18435
|
var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
|
|
18416
18436
|
function registeredMcpUrl(projectRoot) {
|
|
18417
18437
|
let parsed;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-version-probe.d.ts","sourceRoot":"","sources":["../src/claude-version-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH;0DAC0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;IACP,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CAC7C,KACE,MAAM,CAAC;AAEZ,MAAM,WAAW,sBAAsB;IACrC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,sBAA2B,GAAG,MAAM,GAAG,SAAS,CAqB/G;AAMD;kCACkC;AAClC,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,eAAO,MAAM,wBAAwB,mCAAmC,CAAC;AAEzE,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAK1E,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-version-probe.d.ts","sourceRoot":"","sources":["../src/claude-version-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH;0DAC0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;IACP,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CAC7C,KACE,MAAM,CAAC;AAEZ,MAAM,WAAW,sBAAsB;IACrC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,sBAA2B,GAAG,MAAM,GAAG,SAAS,CAqB/G;AAMD;kCACkC;AAClC,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,eAAO,MAAM,wBAAwB,mCAAmC,CAAC;AAEzE,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAK1E,CAAC;AAyBF,MAAM,WAAW,uBAAuB;IACtC,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;sDACkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,uBAA4B,GAAG,mBAAmB,GAAG,SAAS,CAwBhH"}
|
package/dist/cli.js
CHANGED
|
@@ -1942,8 +1942,20 @@ function projectPageUrl(engagementId) {
|
|
|
1942
1942
|
}
|
|
1943
1943
|
|
|
1944
1944
|
// dist/merge-settings.js
|
|
1945
|
+
function isHalfcycleCommand(hook, halfcycleCommands) {
|
|
1946
|
+
if (typeof hook !== "object" || hook === null)
|
|
1947
|
+
return false;
|
|
1948
|
+
const command = hook.command;
|
|
1949
|
+
return typeof command === "string" && halfcycleCommands.has(command);
|
|
1950
|
+
}
|
|
1945
1951
|
function isHalfcycleEntry(entry, halfcycleCommands) {
|
|
1946
|
-
return entry.hooks.some((h) =>
|
|
1952
|
+
return entry.hooks.some((h) => isHalfcycleCommand(h, halfcycleCommands));
|
|
1953
|
+
}
|
|
1954
|
+
function withoutHalfcycleCommands(entry, halfcycleCommands) {
|
|
1955
|
+
if (!isHalfcycleEntry(entry, halfcycleCommands))
|
|
1956
|
+
return entry;
|
|
1957
|
+
const remaining = entry.hooks.filter((h) => !isHalfcycleCommand(h, halfcycleCommands));
|
|
1958
|
+
return remaining.length > 0 ? { ...entry, hooks: remaining } : null;
|
|
1947
1959
|
}
|
|
1948
1960
|
function mergeSettings(existing, generated) {
|
|
1949
1961
|
const merged = { ...existing };
|
|
@@ -1962,7 +1974,7 @@ function mergeSettings(existing, generated) {
|
|
|
1962
1974
|
const mergedHooks = {};
|
|
1963
1975
|
const events = /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(generatedHooks)]);
|
|
1964
1976
|
for (const event of events) {
|
|
1965
|
-
const developerEntries = (existingHooks[event] ?? []).
|
|
1977
|
+
const developerEntries = (existingHooks[event] ?? []).flatMap((entry) => withoutHalfcycleCommands(entry, halfcycleCommands) ?? []);
|
|
1966
1978
|
const halfcycleEntries = generatedHooks[event] ?? [];
|
|
1967
1979
|
mergedHooks[event] = [...developerEntries, ...halfcycleEntries];
|
|
1968
1980
|
}
|
|
@@ -2130,6 +2142,7 @@ function closedWriteSet() {
|
|
|
2130
2142
|
ownedDirs,
|
|
2131
2143
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
2132
2144
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
2145
|
+
settingsSchema: SETTINGS_SCHEMA,
|
|
2133
2146
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
2134
2147
|
gitattributesLines: /* @__PURE__ */ new Set([
|
|
2135
2148
|
GITATTRIBUTES_HEADER,
|
|
@@ -2318,6 +2331,8 @@ function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
|
|
|
2318
2331
|
record(report, outcome, rel);
|
|
2319
2332
|
if (replacedExisting)
|
|
2320
2333
|
report.replacedPaths.push(rel);
|
|
2334
|
+
if (outcome === "collided" && recorded !== void 0)
|
|
2335
|
+
report.editedCollidedPaths.push(rel);
|
|
2321
2336
|
noteFile(draft, targetRepo, rel, outcome);
|
|
2322
2337
|
}
|
|
2323
2338
|
}
|
|
@@ -2441,6 +2456,7 @@ function settingsParseRefusal(targetRepo, err) {
|
|
|
2441
2456
|
}
|
|
2442
2457
|
return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
|
|
2443
2458
|
}
|
|
2459
|
+
var SETTINGS_SCHEMA = "https://json.schemastore.org/claude-code-settings.json";
|
|
2444
2460
|
function generateSettingsJson() {
|
|
2445
2461
|
const hooks = {};
|
|
2446
2462
|
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
@@ -2450,7 +2466,7 @@ function generateSettingsJson() {
|
|
|
2450
2466
|
});
|
|
2451
2467
|
}
|
|
2452
2468
|
const settings = {
|
|
2453
|
-
$schema:
|
|
2469
|
+
$schema: SETTINGS_SCHEMA,
|
|
2454
2470
|
permissions: {
|
|
2455
2471
|
// deny ONLY. See DENY_PATTERNS — an allow list evaporates under bypass mode
|
|
2456
2472
|
// and is the machine owner's grant to make, not ours.
|
|
@@ -2750,19 +2766,27 @@ function adoptBlockUnderHeader(before, header, claimable) {
|
|
|
2750
2766
|
let underHeader = false;
|
|
2751
2767
|
const lines = before.split("\n");
|
|
2752
2768
|
for (const [i, line] of lines.entries()) {
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2769
|
+
const key = lineKey(line);
|
|
2770
|
+
if (key === header) {
|
|
2771
|
+
if (i > 0 && lineKey(lines[i - 1]) === "")
|
|
2772
|
+
claimed.push(lines[i - 1]);
|
|
2756
2773
|
claimed.push(line);
|
|
2757
2774
|
underHeader = true;
|
|
2758
|
-
} else if (
|
|
2775
|
+
} else if (key.trim() === "") {
|
|
2759
2776
|
underHeader = false;
|
|
2760
|
-
} else if (underHeader && claimable.has(
|
|
2777
|
+
} else if (underHeader && claimable.has(key)) {
|
|
2761
2778
|
claimed.push(line);
|
|
2762
2779
|
}
|
|
2763
2780
|
}
|
|
2764
2781
|
return claimed.length > 0 ? claimed.join("\n") + "\n" : "";
|
|
2765
2782
|
}
|
|
2783
|
+
function lineKey(line) {
|
|
2784
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
2785
|
+
}
|
|
2786
|
+
function lineEndingOf(text) {
|
|
2787
|
+
const at = text.indexOf("\n");
|
|
2788
|
+
return at > 0 && text[at - 1] === "\r" ? "\r\n" : "\n";
|
|
2789
|
+
}
|
|
2766
2790
|
function gitignoreCovers(content) {
|
|
2767
2791
|
const lines = new Set(content.split("\n").map((l) => l.trim()));
|
|
2768
2792
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
@@ -2781,9 +2805,8 @@ function reconcileGitignore(targetRepo) {
|
|
|
2781
2805
|
return "skipped";
|
|
2782
2806
|
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
2783
2807
|
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
2784
|
-
const
|
|
2785
|
-
|
|
2786
|
-
` + toAdd.join("\n") + "\n";
|
|
2808
|
+
const eol = lineEndingOf(existing);
|
|
2809
|
+
const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
2787
2810
|
writeFileSync5(gitignorePath, existing + suffix, "utf-8");
|
|
2788
2811
|
return "written";
|
|
2789
2812
|
} catch {
|
|
@@ -2818,8 +2841,9 @@ function reconcileGitattributes(targetRepo) {
|
|
|
2818
2841
|
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
2819
2842
|
if (toAdd.length === 0)
|
|
2820
2843
|
return "skipped";
|
|
2821
|
-
const
|
|
2822
|
-
const
|
|
2844
|
+
const eol = lineEndingOf(existing);
|
|
2845
|
+
const separator = existing === "" ? "" : existing.endsWith("\n") ? eol : eol + eol;
|
|
2846
|
+
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
|
|
2823
2847
|
writeAllowlisted(path, targetRepo, existing + suffix, routed);
|
|
2824
2848
|
return "written";
|
|
2825
2849
|
} catch {
|
|
@@ -2839,9 +2863,9 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
2839
2863
|
function nonEmpty(value) {
|
|
2840
2864
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
2841
2865
|
}
|
|
2842
|
-
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType,
|
|
2866
|
+
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion, writtenPaths) {
|
|
2843
2867
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
2844
|
-
const carried =
|
|
2868
|
+
const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
|
|
2845
2869
|
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
2846
2870
|
const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2847
2871
|
const projectPage = projectPageUrl(engagementId);
|
|
@@ -2850,7 +2874,7 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
|
|
|
2850
2874
|
engagementId,
|
|
2851
2875
|
engagementType,
|
|
2852
2876
|
installedAt,
|
|
2853
|
-
...carried !== void 0 ? {
|
|
2877
|
+
...carried !== void 0 ? { createdByAccountId: carried } : {},
|
|
2854
2878
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
2855
2879
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
2856
2880
|
};
|
|
@@ -2867,6 +2891,9 @@ function writeCrewRoster(targetRepoRoot, report) {
|
|
|
2867
2891
|
const crewPath = join5(targetRepoRoot, CREW_ROSTER_REL);
|
|
2868
2892
|
return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
|
|
2869
2893
|
}
|
|
2894
|
+
function pinCreatedByAccountId(pin) {
|
|
2895
|
+
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
2896
|
+
}
|
|
2870
2897
|
function readBundlePin(targetRepoRoot) {
|
|
2871
2898
|
const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
|
|
2872
2899
|
if (!existsSync3(pinPath))
|
|
@@ -3008,7 +3035,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
3008
3035
|
}
|
|
3009
3036
|
}
|
|
3010
3037
|
async function install(options) {
|
|
3011
|
-
const { targetRepo, engagementId, engagementType, credential, home,
|
|
3038
|
+
const { targetRepo, engagementId, engagementType, credential, home, createdByAccountId } = options;
|
|
3012
3039
|
assertEngagementId(engagementId);
|
|
3013
3040
|
if (!existsSync3(targetRepo)) {
|
|
3014
3041
|
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
@@ -3019,6 +3046,7 @@ async function install(options) {
|
|
|
3019
3046
|
skippedPaths: [],
|
|
3020
3047
|
mergedPaths: [],
|
|
3021
3048
|
collidedPaths: [],
|
|
3049
|
+
editedCollidedPaths: [],
|
|
3022
3050
|
replacedPaths: []
|
|
3023
3051
|
};
|
|
3024
3052
|
const writeSet = closedWriteSet();
|
|
@@ -3146,7 +3174,7 @@ async function install(options) {
|
|
|
3146
3174
|
case "absent":
|
|
3147
3175
|
break;
|
|
3148
3176
|
}
|
|
3149
|
-
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType,
|
|
3177
|
+
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, createdByAccountId, credential?.methodVersion, report.writtenPaths);
|
|
3150
3178
|
noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
|
|
3151
3179
|
noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
|
|
3152
3180
|
const scanResult = runBootstrapScan(targetRepo);
|
|
@@ -3166,6 +3194,7 @@ async function install(options) {
|
|
|
3166
3194
|
skippedPaths: report.skippedPaths,
|
|
3167
3195
|
mergedPaths: report.mergedPaths,
|
|
3168
3196
|
collidedPaths: report.collidedPaths,
|
|
3197
|
+
editedCollidedPaths: report.editedCollidedPaths,
|
|
3169
3198
|
replacedPaths: report.replacedPaths,
|
|
3170
3199
|
bootstrapScanRan: scanResult.ran,
|
|
3171
3200
|
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
@@ -3606,7 +3635,7 @@ async function signIn(serviceUrl, deps = {}) {
|
|
|
3606
3635
|
const url = loopbackVerificationUrl(started.verificationUrl, bind.target);
|
|
3607
3636
|
const attempt = await openBrowser(url, env);
|
|
3608
3637
|
if (attempt.opened) {
|
|
3609
|
-
write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${
|
|
3638
|
+
write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${started.verificationUrl}. This terminal is waiting.
|
|
3610
3639
|
`);
|
|
3611
3640
|
const startedAt = now();
|
|
3612
3641
|
const redirect = bind.waitForCallback({
|
|
@@ -4171,14 +4200,11 @@ async function askAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
|
4171
4200
|
async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
4172
4201
|
return (await askAccount(serviceUrl, credential, fetchImpl)).identity;
|
|
4173
4202
|
}
|
|
4174
|
-
function accountLabel(identity, storedAccountId, verbose2 = false
|
|
4203
|
+
function accountLabel(identity, storedAccountId, verbose2 = false) {
|
|
4175
4204
|
const accountId = identity?.accountId ?? storedAccountId;
|
|
4176
4205
|
if (identity?.email !== void 0 && identity.email.trim() !== "") {
|
|
4177
4206
|
return verbose2 && accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
|
|
4178
4207
|
}
|
|
4179
|
-
if (!confirmed) {
|
|
4180
|
-
return "the account this project is already set up with \u2014 this run could not reach the Halfcycle service to confirm who that is";
|
|
4181
|
-
}
|
|
4182
4208
|
if (accountId !== void 0)
|
|
4183
4209
|
return `account ${accountId} (no email on record for it)`;
|
|
4184
4210
|
return "the account this credential belongs to, which this service did not name";
|
|
@@ -4231,7 +4257,7 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4231
4257
|
switch (credential.source) {
|
|
4232
4258
|
case "browser-handoff":
|
|
4233
4259
|
return {
|
|
4234
|
-
preamble: `[halfcycle]
|
|
4260
|
+
preamble: `[halfcycle] This install will ${actClause(act)}.
|
|
4235
4261
|
`,
|
|
4236
4262
|
asks: false,
|
|
4237
4263
|
noTerminalNote: "",
|
|
@@ -4275,7 +4301,7 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4275
4301
|
// this is printed in), and where to look for the answer once this output is gone.
|
|
4276
4302
|
noTerminalNote: `[halfcycle] Nothing here to ask at (no terminal), so this is continuing as that account.
|
|
4277
4303
|
[halfcycle] To run as a different one, set HALFCYCLE_TOKEN for that account, or remove ${accountStorePath(home)} and run this again with a terminal to sign in.
|
|
4278
|
-
[halfcycle]
|
|
4304
|
+
[halfcycle] .halfcycle/bundle.json records the account that created this project, so that can be checked after this output is gone.
|
|
4279
4305
|
`,
|
|
4280
4306
|
switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
|
|
4281
4307
|
`,
|
|
@@ -4320,7 +4346,7 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
|
4320
4346
|
write(script.switchOpening);
|
|
4321
4347
|
const replacement = await switchAccount(serviceUrl, deps);
|
|
4322
4348
|
const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
|
|
4323
|
-
write(`[halfcycle] Signed in
|
|
4349
|
+
write(`[halfcycle] Signed in. This install will ${actClause(act)}.
|
|
4324
4350
|
`);
|
|
4325
4351
|
if (script.afterSwitch !== "")
|
|
4326
4352
|
write(script.afterSwitch);
|
|
@@ -4629,9 +4655,9 @@ var UPGRADE_COMMANDS = {
|
|
|
4629
4655
|
};
|
|
4630
4656
|
var INSTALL_METHOD_RULES = [
|
|
4631
4657
|
{ method: "winget", pattern: /[\\/]WinGet[\\/]/i },
|
|
4632
|
-
{ method: "npm", pattern: /node_modules[\\/]
|
|
4658
|
+
{ method: "npm", pattern: /node_modules[\\/]|[\\/]npm[\\/]claude(\.cmd|\.exe)?$/i },
|
|
4633
4659
|
{ method: "brew", pattern: /[\\/](Cellar|Caskroom|linuxbrew|homebrew)[\\/]/i },
|
|
4634
|
-
{ method: "native", pattern: /[\\/]\.local[\\/]share[\\/]claude(-code)
|
|
4660
|
+
{ method: "native", pattern: /[\\/]\.local[\\/](share[\\/]claude(-code)?|bin)[\\/]/i }
|
|
4635
4661
|
];
|
|
4636
4662
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
4637
4663
|
const env = deps.env ?? process.env;
|
|
@@ -5836,9 +5862,6 @@ function ourHookEntries(value, commands) {
|
|
|
5836
5862
|
}
|
|
5837
5863
|
return found;
|
|
5838
5864
|
}
|
|
5839
|
-
function isOurHook(hook, commands) {
|
|
5840
|
-
return isPlainObject2(hook) && typeof hook["command"] === "string" && commands.has(hook["command"]);
|
|
5841
|
-
}
|
|
5842
5865
|
function ourMcpServers(value) {
|
|
5843
5866
|
const servers = value["mcpServers"];
|
|
5844
5867
|
return isPlainObject2(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
@@ -5867,9 +5890,9 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5867
5890
|
continue;
|
|
5868
5891
|
}
|
|
5869
5892
|
touched = true;
|
|
5870
|
-
const remaining = entry
|
|
5871
|
-
if (remaining
|
|
5872
|
-
next.push(
|
|
5893
|
+
const remaining = withoutHalfcycleCommands(entry, commands);
|
|
5894
|
+
if (remaining !== null)
|
|
5895
|
+
next.push(remaining);
|
|
5873
5896
|
}
|
|
5874
5897
|
if (touched)
|
|
5875
5898
|
changed = true;
|
|
@@ -5908,16 +5931,12 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5908
5931
|
changed = true;
|
|
5909
5932
|
}
|
|
5910
5933
|
}
|
|
5911
|
-
if ("schemaBefore" in rec) {
|
|
5912
|
-
if (rec.schemaBefore === null)
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
changed = true;
|
|
5916
|
-
}
|
|
5917
|
-
} else if (value["$schema"] !== rec.schemaBefore) {
|
|
5934
|
+
if ("schemaBefore" in rec && value["$schema"] === writeSet.settingsSchema) {
|
|
5935
|
+
if (rec.schemaBefore === null)
|
|
5936
|
+
delete value["$schema"];
|
|
5937
|
+
else
|
|
5918
5938
|
value["$schema"] = rec.schemaBefore;
|
|
5919
|
-
|
|
5920
|
-
}
|
|
5939
|
+
changed = true;
|
|
5921
5940
|
}
|
|
5922
5941
|
return { changed, refused };
|
|
5923
5942
|
}
|
|
@@ -5957,36 +5976,70 @@ function gitattributesRules(writeSet) {
|
|
|
5957
5976
|
function trimAppendedBlocks(text, rec, rules) {
|
|
5958
5977
|
const keptLines = [];
|
|
5959
5978
|
const foreignLines = [];
|
|
5979
|
+
const eol = lineEndingOf(text);
|
|
5960
5980
|
let current = text;
|
|
5961
5981
|
for (const block of [...rec.appended].reverse()) {
|
|
5962
|
-
const lines = block.split("\n").filter((line) => line !== "");
|
|
5982
|
+
const lines = block.split("\n").map(lineKey).filter((line) => line !== "");
|
|
5963
5983
|
const foreign = lines.filter((line) => !rules.ourLines.has(line));
|
|
5964
5984
|
foreignLines.push(...foreign);
|
|
5965
5985
|
const ours = lines.filter((line) => rules.ourLines.has(line));
|
|
5966
5986
|
const entries = ours.filter((line) => line !== rules.header);
|
|
5967
5987
|
const kept = entries.filter((line) => rules.keepLine(line));
|
|
5968
5988
|
keptLines.push(...kept);
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
const head = current.slice(0,
|
|
5972
|
-
const tail = current.slice(
|
|
5973
|
-
current = head + (head !== "" && tail !== "" && !head.endsWith("\n") ?
|
|
5989
|
+
const found = kept.length === 0 && foreign.length === 0 ? lastOccurrence(current, block) : null;
|
|
5990
|
+
if (found !== null) {
|
|
5991
|
+
const head = current.slice(0, found.start);
|
|
5992
|
+
const tail = current.slice(found.end);
|
|
5993
|
+
current = head + (head !== "" && tail !== "" && !head.endsWith("\n") ? eol : "") + tail;
|
|
5974
5994
|
continue;
|
|
5975
5995
|
}
|
|
5976
|
-
const
|
|
5996
|
+
const flat = withoutLineEndCr(block).text;
|
|
5997
|
+
const separated = flat.startsWith("\n") || flat.includes("\n\n");
|
|
5977
5998
|
const removable = ours.filter((line) => line === rules.header ? kept.length === 0 && foreign.length === 0 : !kept.includes(line));
|
|
5978
5999
|
for (const line of removable) {
|
|
5979
6000
|
const fileLines = current.split("\n");
|
|
5980
|
-
const
|
|
6001
|
+
const keys = fileLines.map(lineKey);
|
|
6002
|
+
const at = keys.lastIndexOf(line);
|
|
5981
6003
|
if (at < 0)
|
|
5982
6004
|
continue;
|
|
5983
|
-
const withSeparator = line === rules.header && separated && at > 0 &&
|
|
6005
|
+
const withSeparator = line === rules.header && separated && at > 0 && keys[at - 1] === "";
|
|
5984
6006
|
fileLines.splice(withSeparator ? at - 1 : at, withSeparator ? 2 : 1);
|
|
5985
6007
|
current = fileLines.join("\n");
|
|
5986
6008
|
}
|
|
5987
6009
|
}
|
|
5988
6010
|
return { text: current, keptLines, foreignLines };
|
|
5989
6011
|
}
|
|
6012
|
+
function withoutLineEndCr(text) {
|
|
6013
|
+
let out = "";
|
|
6014
|
+
const at = [];
|
|
6015
|
+
let offset = 0;
|
|
6016
|
+
const lines = text.split("\n");
|
|
6017
|
+
for (const [i, line] of lines.entries()) {
|
|
6018
|
+
const key = lineKey(line);
|
|
6019
|
+
for (let j = 0; j < key.length; j++)
|
|
6020
|
+
at.push(offset + j);
|
|
6021
|
+
out += key;
|
|
6022
|
+
offset += line.length;
|
|
6023
|
+
if (i < lines.length - 1) {
|
|
6024
|
+
out += "\n";
|
|
6025
|
+
at.push(offset);
|
|
6026
|
+
offset += 1;
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
return { text: out, at };
|
|
6030
|
+
}
|
|
6031
|
+
function lastOccurrence(text, block) {
|
|
6032
|
+
const needle = withoutLineEndCr(block).text;
|
|
6033
|
+
if (needle === "")
|
|
6034
|
+
return null;
|
|
6035
|
+
const hay = withoutLineEndCr(text);
|
|
6036
|
+
const found = hay.text.lastIndexOf(needle);
|
|
6037
|
+
if (found < 0)
|
|
6038
|
+
return null;
|
|
6039
|
+
const first = hay.at[found];
|
|
6040
|
+
const start = first > 0 && text[first] === "\n" && text[first - 1] === "\r" ? first - 1 : first;
|
|
6041
|
+
return { start, end: hay.at[found + needle.length - 1] + 1 };
|
|
6042
|
+
}
|
|
5990
6043
|
function coveredPathExists(targetRepo, line) {
|
|
5991
6044
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
5992
6045
|
return path !== "" && lexists(join10(targetRepo, path));
|
|
@@ -6555,6 +6608,14 @@ ${USAGE}`);
|
|
|
6555
6608
|
process.exit(1);
|
|
6556
6609
|
return;
|
|
6557
6610
|
}
|
|
6611
|
+
const onInstallSigint = () => {
|
|
6612
|
+
const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
|
|
6613
|
+
` : "";
|
|
6614
|
+
process.stderr.write(`halfcycle: interrupted.
|
|
6615
|
+
${kept}`);
|
|
6616
|
+
process.exit(1);
|
|
6617
|
+
};
|
|
6618
|
+
process.once("SIGINT", onInstallSigint);
|
|
6558
6619
|
try {
|
|
6559
6620
|
const controlOrigin = resolveControlOrigin(process.env);
|
|
6560
6621
|
const serviceUrl = controlOrigin.origin;
|
|
@@ -6575,13 +6636,14 @@ ${USAGE}`);
|
|
|
6575
6636
|
let credential;
|
|
6576
6637
|
let notThisAccountMessage;
|
|
6577
6638
|
let actingAccountId;
|
|
6639
|
+
let createdByAccountId;
|
|
6578
6640
|
let actingIdentity;
|
|
6579
6641
|
let foundLine = "";
|
|
6580
6642
|
let reusedWithoutFreshAnswer = false;
|
|
6581
6643
|
if (reusable !== void 0 && pinned !== null) {
|
|
6582
6644
|
engagementId = pinned.engagementId;
|
|
6583
6645
|
credential = reusable;
|
|
6584
|
-
actingAccountId =
|
|
6646
|
+
actingAccountId = heldAnswer?.identity?.accountId;
|
|
6585
6647
|
foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
|
|
6586
6648
|
reusedWithoutFreshAnswer = true;
|
|
6587
6649
|
if (verbose) {
|
|
@@ -6631,6 +6693,7 @@ ${USAGE}`);
|
|
|
6631
6693
|
`);
|
|
6632
6694
|
}
|
|
6633
6695
|
} else {
|
|
6696
|
+
const hadUsableIdBeforeThisRun = readFolderId(targetRepo) !== void 0;
|
|
6634
6697
|
const projectId = runPreCreateChecks(targetRepo);
|
|
6635
6698
|
const created = await createOwnedEngagement(serviceUrl, targetRepo, projectId, { assumeYes, verbose });
|
|
6636
6699
|
if (isProjectOwnedElsewhere(created)) {
|
|
@@ -6642,6 +6705,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6642
6705
|
}
|
|
6643
6706
|
engagementId = created.engagementId;
|
|
6644
6707
|
actingAccountId = created.actingAccountId;
|
|
6708
|
+
createdByAccountId = created.actingAccountId;
|
|
6645
6709
|
actingIdentity = created.actingIdentity;
|
|
6646
6710
|
credential = {
|
|
6647
6711
|
serviceUrl,
|
|
@@ -6661,7 +6725,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6661
6725
|
consoleUrl: created.consoleUrl,
|
|
6662
6726
|
methodVersion: created.methodVersion
|
|
6663
6727
|
};
|
|
6664
|
-
foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
6728
|
+
foundLine = hadUsableIdBeforeThisRun ? `picking up this folder's project (${shortId(created.engagementId)})` : `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
6665
6729
|
if (verbose) {
|
|
6666
6730
|
process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
|
|
6667
6731
|
`);
|
|
@@ -6672,7 +6736,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6672
6736
|
engagementId,
|
|
6673
6737
|
engagementType: engagementTypeRaw,
|
|
6674
6738
|
credential,
|
|
6675
|
-
|
|
6739
|
+
createdByAccountId
|
|
6676
6740
|
});
|
|
6677
6741
|
if (verbose) {
|
|
6678
6742
|
process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
|
|
@@ -6688,8 +6752,14 @@ ${PROJECT_RESET_LINE}
|
|
|
6688
6752
|
`);
|
|
6689
6753
|
}
|
|
6690
6754
|
}
|
|
6691
|
-
|
|
6692
|
-
|
|
6755
|
+
const edited = new Set(result.editedCollidedPaths);
|
|
6756
|
+
const trueCollisions = result.collidedPaths.filter((path) => !edited.has(path));
|
|
6757
|
+
if (result.editedCollidedPaths.length > 0) {
|
|
6758
|
+
process.stdout.write(`[halfcycle] Kept (your edited copy of an older Halfcycle file \u2014 NOT overwritten): ${result.editedCollidedPaths.join(", ")}
|
|
6759
|
+
`);
|
|
6760
|
+
}
|
|
6761
|
+
if (trueCollisions.length > 0) {
|
|
6762
|
+
process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${trueCollisions.join(", ")}
|
|
6693
6763
|
`);
|
|
6694
6764
|
}
|
|
6695
6765
|
if (result.replacedPaths.length > 0) {
|
|
@@ -6744,8 +6814,10 @@ ${PROJECT_RESET_LINE}
|
|
|
6744
6814
|
const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
6745
6815
|
const written = result.writtenPaths.length;
|
|
6746
6816
|
const merged = result.mergedPaths.length;
|
|
6747
|
-
|
|
6817
|
+
if (identity !== void 0) {
|
|
6818
|
+
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
6748
6819
|
`);
|
|
6820
|
+
}
|
|
6749
6821
|
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
6750
6822
|
`);
|
|
6751
6823
|
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, ${credential ? "guards armed on every edit" : "not checked \u2014 this copy is not this account\u2019s project"}
|
|
@@ -6757,7 +6829,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6757
6829
|
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.
|
|
6758
6830
|
`);
|
|
6759
6831
|
} else if (credential) {
|
|
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
|
|
6832
|
+
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 to get checks on every edit.
|
|
6761
6833
|
` : `[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.
|
|
6762
6834
|
`);
|
|
6763
6835
|
}
|