halfcycle 0.4.2 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +31 -6
- package/dist/claude-version-probe.d.ts.map +1 -1
- package/dist/cli.js +185 -91
- package/dist/cli.js.map +2 -2
- package/dist/confirm-identity.d.ts +42 -22
- 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 +73 -32
- package/dist/index.js.map +1 -1
- package/dist/install-manifest.d.ts +27 -2
- package/dist/install-manifest.d.ts.map +1 -1
- package/dist/install.d.ts +79 -10
- 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
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/bin.js
|
|
4
|
-
import { existsSync as existsSync7, readFileSync as readFileSync11, realpathSync as realpathSync3, statSync } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11, realpathSync as realpathSync3, statSync, writeSync as writeSync2 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
import { join as join12 } from "node:path";
|
|
7
7
|
|
|
@@ -1648,12 +1648,20 @@ function createProjectIdentityFile(targetRepoRoot) {
|
|
|
1648
1648
|
}
|
|
1649
1649
|
|
|
1650
1650
|
// dist/install-manifest.js
|
|
1651
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
1651
1652
|
import { createHash } from "node:crypto";
|
|
1652
1653
|
var INSTALL_MANIFEST_REL = ".halfcycle/install-manifest.json";
|
|
1653
1654
|
var INSTALL_MANIFEST_FORMAT = "halfcycle-install-manifest/v1";
|
|
1654
1655
|
function sha256Hex(bytes) {
|
|
1655
1656
|
return createHash("sha256").update(bytes).digest("hex");
|
|
1656
1657
|
}
|
|
1658
|
+
function lineKey(line) {
|
|
1659
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1660
|
+
}
|
|
1661
|
+
function fileSha256(bytes) {
|
|
1662
|
+
const text = typeof bytes === "string" ? bytes : Buffer2.from(bytes).toString("utf-8");
|
|
1663
|
+
return sha256Hex(text.split("\n").map(lineKey).join("\n"));
|
|
1664
|
+
}
|
|
1657
1665
|
function canonicalJson(value) {
|
|
1658
1666
|
if (Array.isArray(value))
|
|
1659
1667
|
return value.map(canonicalJson);
|
|
@@ -1836,7 +1844,7 @@ function startManifest(previous, isMember, isMemberDir) {
|
|
|
1836
1844
|
};
|
|
1837
1845
|
}
|
|
1838
1846
|
function recordInstalledFile(draft, path, bytes) {
|
|
1839
|
-
draft.files.set(path, { path, sha256:
|
|
1847
|
+
draft.files.set(path, { path, sha256: fileSha256(bytes) });
|
|
1840
1848
|
}
|
|
1841
1849
|
function recordRemovedFile(draft, path) {
|
|
1842
1850
|
draft.files.delete(path);
|
|
@@ -1942,8 +1950,20 @@ function projectPageUrl(engagementId) {
|
|
|
1942
1950
|
}
|
|
1943
1951
|
|
|
1944
1952
|
// dist/merge-settings.js
|
|
1953
|
+
function isHalfcycleCommand(hook, halfcycleCommands) {
|
|
1954
|
+
if (typeof hook !== "object" || hook === null)
|
|
1955
|
+
return false;
|
|
1956
|
+
const command = hook.command;
|
|
1957
|
+
return typeof command === "string" && halfcycleCommands.has(command);
|
|
1958
|
+
}
|
|
1945
1959
|
function isHalfcycleEntry(entry, halfcycleCommands) {
|
|
1946
|
-
return entry.hooks.some((h) =>
|
|
1960
|
+
return entry.hooks.some((h) => isHalfcycleCommand(h, halfcycleCommands));
|
|
1961
|
+
}
|
|
1962
|
+
function withoutHalfcycleCommands(entry, halfcycleCommands) {
|
|
1963
|
+
if (!isHalfcycleEntry(entry, halfcycleCommands))
|
|
1964
|
+
return entry;
|
|
1965
|
+
const remaining = entry.hooks.filter((h) => !isHalfcycleCommand(h, halfcycleCommands));
|
|
1966
|
+
return remaining.length > 0 ? { ...entry, hooks: remaining } : null;
|
|
1947
1967
|
}
|
|
1948
1968
|
function mergeSettings(existing, generated) {
|
|
1949
1969
|
const merged = { ...existing };
|
|
@@ -1962,7 +1982,7 @@ function mergeSettings(existing, generated) {
|
|
|
1962
1982
|
const mergedHooks = {};
|
|
1963
1983
|
const events = /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(generatedHooks)]);
|
|
1964
1984
|
for (const event of events) {
|
|
1965
|
-
const developerEntries = (existingHooks[event] ?? []).
|
|
1985
|
+
const developerEntries = (existingHooks[event] ?? []).flatMap((entry) => withoutHalfcycleCommands(entry, halfcycleCommands) ?? []);
|
|
1966
1986
|
const halfcycleEntries = generatedHooks[event] ?? [];
|
|
1967
1987
|
mergedHooks[event] = [...developerEntries, ...halfcycleEntries];
|
|
1968
1988
|
}
|
|
@@ -2130,6 +2150,7 @@ function closedWriteSet() {
|
|
|
2130
2150
|
ownedDirs,
|
|
2131
2151
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
2132
2152
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
2153
|
+
settingsSchema: SETTINGS_SCHEMA,
|
|
2133
2154
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
2134
2155
|
gitattributesLines: /* @__PURE__ */ new Set([
|
|
2135
2156
|
GITATTRIBUTES_HEADER,
|
|
@@ -2142,17 +2163,17 @@ function sha256OfWhatThisVersionWrites(commands) {
|
|
|
2142
2163
|
const out = /* @__PURE__ */ new Map();
|
|
2143
2164
|
const fromFile = (rel, src) => {
|
|
2144
2165
|
if (existsSync3(src))
|
|
2145
|
-
out.set(rel,
|
|
2166
|
+
out.set(rel, fileSha256(readFileSync5(src)));
|
|
2146
2167
|
};
|
|
2147
2168
|
for (const cmd of commands)
|
|
2148
2169
|
fromFile(commandStubRel(cmd.name), join5(BUNDLE_ROOT, ".claude-plugin", cmd.path));
|
|
2149
2170
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
2150
|
-
out.set(rel,
|
|
2171
|
+
out.set(rel, fileSha256(content));
|
|
2151
2172
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
2152
2173
|
return out;
|
|
2153
2174
|
}
|
|
2154
2175
|
function bytesAreOurs(path, bytes, recorded, writeSet) {
|
|
2155
|
-
const hash =
|
|
2176
|
+
const hash = fileSha256(bytes);
|
|
2156
2177
|
const recordedHash = recorded !== void 0 && "sha256" in recorded ? recorded.sha256 : void 0;
|
|
2157
2178
|
return hash === recordedHash || hash === writeSet.generatedSha256.get(path);
|
|
2158
2179
|
}
|
|
@@ -2318,6 +2339,8 @@ function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
|
|
|
2318
2339
|
record(report, outcome, rel);
|
|
2319
2340
|
if (replacedExisting)
|
|
2320
2341
|
report.replacedPaths.push(rel);
|
|
2342
|
+
if (outcome === "collided" && recorded !== void 0)
|
|
2343
|
+
report.editedCollidedPaths.push(rel);
|
|
2321
2344
|
noteFile(draft, targetRepo, rel, outcome);
|
|
2322
2345
|
}
|
|
2323
2346
|
}
|
|
@@ -2338,16 +2361,17 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
|
|
|
2338
2361
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
2339
2362
|
}
|
|
2340
2363
|
let replacedExisting = false;
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2364
|
+
const current = existsSync3(targetAbsPath) ? readFileSync5(targetAbsPath) : null;
|
|
2365
|
+
const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
|
|
2366
|
+
if (current !== null) {
|
|
2367
|
+
if (current.toString("utf-8") === text)
|
|
2344
2368
|
return { outcome: "skipped", replacedExisting: false };
|
|
2345
2369
|
if (!isOurs(current))
|
|
2346
2370
|
return { outcome: "collided", replacedExisting: false };
|
|
2347
2371
|
replacedExisting = true;
|
|
2348
2372
|
}
|
|
2349
2373
|
mkdirSync5(dirname3(targetAbsPath), { recursive: true });
|
|
2350
|
-
writeFileSync5(targetAbsPath,
|
|
2374
|
+
writeFileSync5(targetAbsPath, text, "utf-8");
|
|
2351
2375
|
return { outcome: "written", replacedExisting };
|
|
2352
2376
|
}
|
|
2353
2377
|
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
@@ -2359,11 +2383,12 @@ function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
|
2359
2383
|
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
2360
2384
|
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.`);
|
|
2361
2385
|
}
|
|
2362
|
-
|
|
2386
|
+
const current = readFileOrNull(targetAbsPath);
|
|
2387
|
+
const text = inLineEndingOf(current, content);
|
|
2388
|
+
if (current === text)
|
|
2363
2389
|
return "skipped";
|
|
2364
|
-
}
|
|
2365
2390
|
mkdirSync5(dirname3(targetAbsPath), { recursive: true });
|
|
2366
|
-
writeFileSync5(targetAbsPath,
|
|
2391
|
+
writeFileSync5(targetAbsPath, text, "utf-8");
|
|
2367
2392
|
return "written";
|
|
2368
2393
|
}
|
|
2369
2394
|
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
@@ -2441,6 +2466,7 @@ function settingsParseRefusal(targetRepo, err) {
|
|
|
2441
2466
|
}
|
|
2442
2467
|
return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
|
|
2443
2468
|
}
|
|
2469
|
+
var SETTINGS_SCHEMA = "https://json.schemastore.org/claude-code-settings.json";
|
|
2444
2470
|
function generateSettingsJson() {
|
|
2445
2471
|
const hooks = {};
|
|
2446
2472
|
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
@@ -2450,7 +2476,7 @@ function generateSettingsJson() {
|
|
|
2450
2476
|
});
|
|
2451
2477
|
}
|
|
2452
2478
|
const settings = {
|
|
2453
|
-
$schema:
|
|
2479
|
+
$schema: SETTINGS_SCHEMA,
|
|
2454
2480
|
permissions: {
|
|
2455
2481
|
// deny ONLY. See DENY_PATTERNS — an allow list evaporates under bypass mode
|
|
2456
2482
|
// and is the machine owner's grant to make, not ours.
|
|
@@ -2750,19 +2776,27 @@ function adoptBlockUnderHeader(before, header, claimable) {
|
|
|
2750
2776
|
let underHeader = false;
|
|
2751
2777
|
const lines = before.split("\n");
|
|
2752
2778
|
for (const [i, line] of lines.entries()) {
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2779
|
+
const key = lineKey(line);
|
|
2780
|
+
if (key === header) {
|
|
2781
|
+
if (i > 0 && lineKey(lines[i - 1]) === "")
|
|
2782
|
+
claimed.push(lines[i - 1]);
|
|
2756
2783
|
claimed.push(line);
|
|
2757
2784
|
underHeader = true;
|
|
2758
|
-
} else if (
|
|
2785
|
+
} else if (key.trim() === "") {
|
|
2759
2786
|
underHeader = false;
|
|
2760
|
-
} else if (underHeader && claimable.has(
|
|
2787
|
+
} else if (underHeader && claimable.has(key)) {
|
|
2761
2788
|
claimed.push(line);
|
|
2762
2789
|
}
|
|
2763
2790
|
}
|
|
2764
2791
|
return claimed.length > 0 ? claimed.join("\n") + "\n" : "";
|
|
2765
2792
|
}
|
|
2793
|
+
function lineEndingOf(text) {
|
|
2794
|
+
const at = text.indexOf("\n");
|
|
2795
|
+
return at > 0 && text[at - 1] === "\r" ? "\r\n" : "\n";
|
|
2796
|
+
}
|
|
2797
|
+
function inLineEndingOf(existing, generated) {
|
|
2798
|
+
return existing === null ? generated : generated.split("\n").join(lineEndingOf(existing));
|
|
2799
|
+
}
|
|
2766
2800
|
function gitignoreCovers(content) {
|
|
2767
2801
|
const lines = new Set(content.split("\n").map((l) => l.trim()));
|
|
2768
2802
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
@@ -2781,9 +2815,8 @@ function reconcileGitignore(targetRepo) {
|
|
|
2781
2815
|
return "skipped";
|
|
2782
2816
|
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
2783
2817
|
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
2784
|
-
const
|
|
2785
|
-
|
|
2786
|
-
` + toAdd.join("\n") + "\n";
|
|
2818
|
+
const eol = lineEndingOf(existing);
|
|
2819
|
+
const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
2787
2820
|
writeFileSync5(gitignorePath, existing + suffix, "utf-8");
|
|
2788
2821
|
return "written";
|
|
2789
2822
|
} catch {
|
|
@@ -2818,8 +2851,9 @@ function reconcileGitattributes(targetRepo) {
|
|
|
2818
2851
|
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
2819
2852
|
if (toAdd.length === 0)
|
|
2820
2853
|
return "skipped";
|
|
2821
|
-
const
|
|
2822
|
-
const
|
|
2854
|
+
const eol = lineEndingOf(existing);
|
|
2855
|
+
const separator = existing === "" ? "" : existing.endsWith("\n") ? eol : eol + eol;
|
|
2856
|
+
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
|
|
2823
2857
|
writeAllowlisted(path, targetRepo, existing + suffix, routed);
|
|
2824
2858
|
return "written";
|
|
2825
2859
|
} catch {
|
|
@@ -2839,9 +2873,9 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
2839
2873
|
function nonEmpty(value) {
|
|
2840
2874
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
2841
2875
|
}
|
|
2842
|
-
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType,
|
|
2876
|
+
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion, writtenPaths) {
|
|
2843
2877
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
2844
|
-
const carried =
|
|
2878
|
+
const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
|
|
2845
2879
|
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
2846
2880
|
const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2847
2881
|
const projectPage = projectPageUrl(engagementId);
|
|
@@ -2850,12 +2884,13 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
|
|
|
2850
2884
|
engagementId,
|
|
2851
2885
|
engagementType,
|
|
2852
2886
|
installedAt,
|
|
2853
|
-
...carried !== void 0 ? {
|
|
2887
|
+
...carried !== void 0 ? { createdByAccountId: carried } : {},
|
|
2854
2888
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
2855
2889
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
2856
2890
|
};
|
|
2857
2891
|
const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
|
|
2858
|
-
|
|
2892
|
+
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(pin, null, 2) + "\n");
|
|
2893
|
+
writeAllowlisted(pinPath, targetRepoRoot, pinText, writtenPaths);
|
|
2859
2894
|
}
|
|
2860
2895
|
function writeCrewRoster(targetRepoRoot, report) {
|
|
2861
2896
|
const doc = {
|
|
@@ -2867,6 +2902,9 @@ function writeCrewRoster(targetRepoRoot, report) {
|
|
|
2867
2902
|
const crewPath = join5(targetRepoRoot, CREW_ROSTER_REL);
|
|
2868
2903
|
return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
|
|
2869
2904
|
}
|
|
2905
|
+
function pinCreatedByAccountId(pin) {
|
|
2906
|
+
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
2907
|
+
}
|
|
2870
2908
|
function readBundlePin(targetRepoRoot) {
|
|
2871
2909
|
const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
|
|
2872
2910
|
if (!existsSync3(pinPath))
|
|
@@ -3008,7 +3046,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
3008
3046
|
}
|
|
3009
3047
|
}
|
|
3010
3048
|
async function install(options) {
|
|
3011
|
-
const { targetRepo, engagementId, engagementType, credential, home,
|
|
3049
|
+
const { targetRepo, engagementId, engagementType, credential, home, createdByAccountId } = options;
|
|
3012
3050
|
assertEngagementId(engagementId);
|
|
3013
3051
|
if (!existsSync3(targetRepo)) {
|
|
3014
3052
|
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
@@ -3019,6 +3057,7 @@ async function install(options) {
|
|
|
3019
3057
|
skippedPaths: [],
|
|
3020
3058
|
mergedPaths: [],
|
|
3021
3059
|
collidedPaths: [],
|
|
3060
|
+
editedCollidedPaths: [],
|
|
3022
3061
|
replacedPaths: []
|
|
3023
3062
|
};
|
|
3024
3063
|
const writeSet = closedWriteSet();
|
|
@@ -3040,7 +3079,7 @@ async function install(options) {
|
|
|
3040
3079
|
}
|
|
3041
3080
|
recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted);
|
|
3042
3081
|
const mergedSettings = mergeSettings(existingSettings, generatedSettings);
|
|
3043
|
-
const mergedSettingsText = JSON.stringify(mergedSettings, null, 2) + "\n";
|
|
3082
|
+
const mergedSettingsText = inLineEndingOf(readFileOrNull(settingsPath), JSON.stringify(mergedSettings, null, 2) + "\n");
|
|
3044
3083
|
mkdirSync5(dirname3(settingsPath), { recursive: true });
|
|
3045
3084
|
writeFileSync5(settingsPath, mergedSettingsText, "utf-8");
|
|
3046
3085
|
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
@@ -3068,7 +3107,7 @@ async function install(options) {
|
|
|
3068
3107
|
if (credential) {
|
|
3069
3108
|
const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
|
|
3070
3109
|
const existingMcp = existsSync3(mcpPath) ? readFileSync5(mcpPath, "utf-8") : null;
|
|
3071
|
-
const mcpContent = generateMcpRegistration(existingMcp, credential.mcpUrl);
|
|
3110
|
+
const mcpContent = inLineEndingOf(existingMcp, generateMcpRegistration(existingMcp, credential.mcpUrl));
|
|
3072
3111
|
recordMcpMerge(draft, existingMcp, mcpContent);
|
|
3073
3112
|
if (existingMcp === null) {
|
|
3074
3113
|
writeAllowlisted(mcpPath, targetRepo, mcpContent, report.writtenPaths);
|
|
@@ -3146,7 +3185,7 @@ async function install(options) {
|
|
|
3146
3185
|
case "absent":
|
|
3147
3186
|
break;
|
|
3148
3187
|
}
|
|
3149
|
-
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType,
|
|
3188
|
+
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, createdByAccountId, credential?.methodVersion, report.writtenPaths);
|
|
3150
3189
|
noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
|
|
3151
3190
|
noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
|
|
3152
3191
|
const scanResult = runBootstrapScan(targetRepo);
|
|
@@ -3166,6 +3205,7 @@ async function install(options) {
|
|
|
3166
3205
|
skippedPaths: report.skippedPaths,
|
|
3167
3206
|
mergedPaths: report.mergedPaths,
|
|
3168
3207
|
collidedPaths: report.collidedPaths,
|
|
3208
|
+
editedCollidedPaths: report.editedCollidedPaths,
|
|
3169
3209
|
replacedPaths: report.replacedPaths,
|
|
3170
3210
|
bootstrapScanRan: scanResult.ran,
|
|
3171
3211
|
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
@@ -3606,7 +3646,7 @@ async function signIn(serviceUrl, deps = {}) {
|
|
|
3606
3646
|
const url = loopbackVerificationUrl(started.verificationUrl, bind.target);
|
|
3607
3647
|
const attempt = await openBrowser(url, env);
|
|
3608
3648
|
if (attempt.opened) {
|
|
3609
|
-
write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${
|
|
3649
|
+
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
3650
|
`);
|
|
3611
3651
|
const startedAt = now();
|
|
3612
3652
|
const redirect = bind.waitForCallback({
|
|
@@ -4171,17 +4211,13 @@ async function askAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
|
4171
4211
|
async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
4172
4212
|
return (await askAccount(serviceUrl, credential, fetchImpl)).identity;
|
|
4173
4213
|
}
|
|
4174
|
-
function accountLabel(identity, storedAccountId, verbose2 = false
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
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";
|
|
4214
|
+
function accountLabel(identity, storedAccountId, verbose2 = false) {
|
|
4215
|
+
if (identity === void 0)
|
|
4216
|
+
return void 0;
|
|
4217
|
+
if (identity.email !== void 0 && identity.email.trim() !== "") {
|
|
4218
|
+
return verbose2 ? `${identity.email} (account ${identity.accountId})` : identity.email;
|
|
4181
4219
|
}
|
|
4182
|
-
|
|
4183
|
-
return `account ${accountId} (no email on record for it)`;
|
|
4184
|
-
return "the account this credential belongs to, which this service did not name";
|
|
4220
|
+
return `account ${identity.accountId} (no email on record for it)`;
|
|
4185
4221
|
}
|
|
4186
4222
|
function onlyDate(iso) {
|
|
4187
4223
|
if (iso === void 0)
|
|
@@ -4227,11 +4263,15 @@ function actingAs(credential, identity) {
|
|
|
4227
4263
|
function question(colour) {
|
|
4228
4264
|
return `[halfcycle] Continue as that account? ${ink("good", "[y]", colour)} yes ${ink("key", "[s]", colour)} sign in as someone else ${ink("faint", "[anything else]", colour)} stop: `;
|
|
4229
4265
|
}
|
|
4230
|
-
function
|
|
4266
|
+
function accountLine(identity, label) {
|
|
4267
|
+
return identity === void 0 ? "" : `[halfcycle] Account: ${label}
|
|
4268
|
+
`;
|
|
4269
|
+
}
|
|
4270
|
+
function armScript(credential, act, accountLineText, plainLabel, home) {
|
|
4231
4271
|
switch (credential.source) {
|
|
4232
4272
|
case "browser-handoff":
|
|
4233
4273
|
return {
|
|
4234
|
-
preamble: `[halfcycle]
|
|
4274
|
+
preamble: `[halfcycle] This install will ${actClause(act)}.
|
|
4235
4275
|
`,
|
|
4236
4276
|
asks: false,
|
|
4237
4277
|
noTerminalNote: "",
|
|
@@ -4239,11 +4279,11 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4239
4279
|
stopRemedy: "",
|
|
4240
4280
|
afterSwitch: ""
|
|
4241
4281
|
};
|
|
4242
|
-
case "environment":
|
|
4282
|
+
case "environment": {
|
|
4283
|
+
const credentialSentence = plainLabel !== void 0 ? `HALFCYCLE_TOKEN in this environment is the credential for ${plainLabel}, and it takes precedence over any saved sign-in` : `The credential in your environment could not be checked, because the Halfcycle service did not answer, and it takes precedence over any saved sign-in`;
|
|
4243
4284
|
return {
|
|
4244
4285
|
preamble: `[halfcycle] HALFCYCLE_TOKEN is set in this environment, and this install will use it.
|
|
4245
|
-
[halfcycle]
|
|
4246
|
-
[halfcycle] About to ${actClause(act)}
|
|
4286
|
+
` + accountLineText + `[halfcycle] About to ${actClause(act)}
|
|
4247
4287
|
[halfcycle] A variable exported by a shell profile is not a choice about this run \u2014 if that is not your account, ${wrongAccountConsequence(act)}.
|
|
4248
4288
|
`,
|
|
4249
4289
|
asks: true,
|
|
@@ -4253,19 +4293,20 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4253
4293
|
noTerminalNote: "",
|
|
4254
4294
|
switchOpening: `[halfcycle] Signing you in through your browser \u2014 this install will use that account instead.
|
|
4255
4295
|
`,
|
|
4256
|
-
stopRemedy: `Stopped before anything was set up.
|
|
4296
|
+
stopRemedy: `Stopped before anything was set up. ${credentialSentence} \u2014 so unset it, or set it to your own account's credential, before running this again.`,
|
|
4257
4297
|
// The switch is real for THIS run (the new credential is what gets used) and it
|
|
4258
4298
|
// does not persist: `obtainCredential` reads the variable first, so the next run
|
|
4259
4299
|
// is the same variable again. Said here rather than discovered tomorrow.
|
|
4260
4300
|
afterSwitch: `[halfcycle] HALFCYCLE_TOKEN is still set in this environment and still wins over a saved sign-in \u2014 unset it, or the next run will use it again.
|
|
4261
4301
|
`
|
|
4262
4302
|
};
|
|
4303
|
+
}
|
|
4263
4304
|
case "stored": {
|
|
4264
4305
|
const savedOn = onlyDate(credential.signedInAt);
|
|
4306
|
+
const savedSignInSentence = plainLabel !== void 0 ? `This machine's saved sign-in is for ${plainLabel}.` : `This machine's saved sign-in could not be checked, because the Halfcycle service did not answer.`;
|
|
4265
4307
|
return {
|
|
4266
4308
|
preamble: `[halfcycle] This machine already has a Halfcycle sign-in saved, and this install will use it.
|
|
4267
|
-
[halfcycle]
|
|
4268
|
-
` + (savedOn !== void 0 ? `[halfcycle] Saved on this machine: ${savedOn}
|
|
4309
|
+
` + accountLineText + (savedOn !== void 0 ? `[halfcycle] Saved on this machine: ${savedOn}
|
|
4269
4310
|
` : "") + `[halfcycle] About to ${actClause(act)}
|
|
4270
4311
|
[halfcycle] If that is not your account, ${wrongAccountConsequence(act)}${wrongAccountVisibility(act) === void 0 ? "" : ` \u2014 ${wrongAccountVisibility(act)}`}.
|
|
4271
4312
|
`,
|
|
@@ -4275,11 +4316,11 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4275
4316
|
// this is printed in), and where to look for the answer once this output is gone.
|
|
4276
4317
|
noTerminalNote: `[halfcycle] Nothing here to ask at (no terminal), so this is continuing as that account.
|
|
4277
4318
|
[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]
|
|
4319
|
+
[halfcycle] .halfcycle/bundle.json records the account that created this project, so that can be checked after this output is gone.
|
|
4279
4320
|
`,
|
|
4280
4321
|
switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
|
|
4281
4322
|
`,
|
|
4282
|
-
stopRemedy: `Stopped before anything was set up.
|
|
4323
|
+
stopRemedy: `Stopped before anything was set up. ${savedSignInSentence} 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.`,
|
|
4283
4324
|
afterSwitch: ""
|
|
4284
4325
|
};
|
|
4285
4326
|
}
|
|
@@ -4294,9 +4335,9 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
|
4294
4335
|
const colour = deps.colour ?? decideColour({ isTTY: process.stdout.isTTY === true, env: deps.env ?? process.env });
|
|
4295
4336
|
const identity = await describeAccount(serviceUrl, credential.credential, deps.fetchImpl);
|
|
4296
4337
|
const plainLabel = accountLabel(identity, credential.accountId, deps.verbose === true);
|
|
4297
|
-
const label = ink("keyBold", plainLabel, colour);
|
|
4338
|
+
const label = plainLabel !== void 0 ? ink("keyBold", plainLabel, colour) : "";
|
|
4298
4339
|
const acting = actingAs(credential, identity);
|
|
4299
|
-
const script = armScript(credential, act, label, plainLabel, deps.home);
|
|
4340
|
+
const script = armScript(credential, act, accountLine(identity, label), plainLabel, deps.home);
|
|
4300
4341
|
write(script.preamble);
|
|
4301
4342
|
if (!script.asks)
|
|
4302
4343
|
return acting;
|
|
@@ -4320,7 +4361,7 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
|
4320
4361
|
write(script.switchOpening);
|
|
4321
4362
|
const replacement = await switchAccount(serviceUrl, deps);
|
|
4322
4363
|
const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
|
|
4323
|
-
write(`[halfcycle] Signed in
|
|
4364
|
+
write(`[halfcycle] Signed in. This install will ${actClause(act)}.
|
|
4324
4365
|
`);
|
|
4325
4366
|
if (script.afterSwitch !== "")
|
|
4326
4367
|
write(script.afterSwitch);
|
|
@@ -4629,9 +4670,9 @@ var UPGRADE_COMMANDS = {
|
|
|
4629
4670
|
};
|
|
4630
4671
|
var INSTALL_METHOD_RULES = [
|
|
4631
4672
|
{ method: "winget", pattern: /[\\/]WinGet[\\/]/i },
|
|
4632
|
-
{ method: "npm", pattern: /node_modules[\\/]
|
|
4673
|
+
{ method: "npm", pattern: /node_modules[\\/]|[\\/]npm[\\/]claude(\.cmd|\.exe)?$/i },
|
|
4633
4674
|
{ method: "brew", pattern: /[\\/](Cellar|Caskroom|linuxbrew|homebrew)[\\/]/i },
|
|
4634
|
-
{ method: "native", pattern: /[\\/]\.local[\\/]share[\\/]claude(-code)
|
|
4675
|
+
{ method: "native", pattern: /[\\/]\.local[\\/](share[\\/]claude(-code)?|bin)[\\/]/i }
|
|
4635
4676
|
];
|
|
4636
4677
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
4637
4678
|
const env = deps.env ?? process.env;
|
|
@@ -5836,9 +5877,6 @@ function ourHookEntries(value, commands) {
|
|
|
5836
5877
|
}
|
|
5837
5878
|
return found;
|
|
5838
5879
|
}
|
|
5839
|
-
function isOurHook(hook, commands) {
|
|
5840
|
-
return isPlainObject2(hook) && typeof hook["command"] === "string" && commands.has(hook["command"]);
|
|
5841
|
-
}
|
|
5842
5880
|
function ourMcpServers(value) {
|
|
5843
5881
|
const servers = value["mcpServers"];
|
|
5844
5882
|
return isPlainObject2(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
@@ -5867,9 +5905,9 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5867
5905
|
continue;
|
|
5868
5906
|
}
|
|
5869
5907
|
touched = true;
|
|
5870
|
-
const remaining = entry
|
|
5871
|
-
if (remaining
|
|
5872
|
-
next.push(
|
|
5908
|
+
const remaining = withoutHalfcycleCommands(entry, commands);
|
|
5909
|
+
if (remaining !== null)
|
|
5910
|
+
next.push(remaining);
|
|
5873
5911
|
}
|
|
5874
5912
|
if (touched)
|
|
5875
5913
|
changed = true;
|
|
@@ -5908,16 +5946,12 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5908
5946
|
changed = true;
|
|
5909
5947
|
}
|
|
5910
5948
|
}
|
|
5911
|
-
if ("schemaBefore" in rec) {
|
|
5912
|
-
if (rec.schemaBefore === null)
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
changed = true;
|
|
5916
|
-
}
|
|
5917
|
-
} else if (value["$schema"] !== rec.schemaBefore) {
|
|
5949
|
+
if ("schemaBefore" in rec && value["$schema"] === writeSet.settingsSchema) {
|
|
5950
|
+
if (rec.schemaBefore === null)
|
|
5951
|
+
delete value["$schema"];
|
|
5952
|
+
else
|
|
5918
5953
|
value["$schema"] = rec.schemaBefore;
|
|
5919
|
-
|
|
5920
|
-
}
|
|
5954
|
+
changed = true;
|
|
5921
5955
|
}
|
|
5922
5956
|
return { changed, refused };
|
|
5923
5957
|
}
|
|
@@ -5957,36 +5991,70 @@ function gitattributesRules(writeSet) {
|
|
|
5957
5991
|
function trimAppendedBlocks(text, rec, rules) {
|
|
5958
5992
|
const keptLines = [];
|
|
5959
5993
|
const foreignLines = [];
|
|
5994
|
+
const eol = lineEndingOf(text);
|
|
5960
5995
|
let current = text;
|
|
5961
5996
|
for (const block of [...rec.appended].reverse()) {
|
|
5962
|
-
const lines = block.split("\n").filter((line) => line !== "");
|
|
5997
|
+
const lines = block.split("\n").map(lineKey).filter((line) => line !== "");
|
|
5963
5998
|
const foreign = lines.filter((line) => !rules.ourLines.has(line));
|
|
5964
5999
|
foreignLines.push(...foreign);
|
|
5965
6000
|
const ours = lines.filter((line) => rules.ourLines.has(line));
|
|
5966
6001
|
const entries = ours.filter((line) => line !== rules.header);
|
|
5967
6002
|
const kept = entries.filter((line) => rules.keepLine(line));
|
|
5968
6003
|
keptLines.push(...kept);
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
const head = current.slice(0,
|
|
5972
|
-
const tail = current.slice(
|
|
5973
|
-
current = head + (head !== "" && tail !== "" && !head.endsWith("\n") ?
|
|
6004
|
+
const found = kept.length === 0 && foreign.length === 0 ? lastOccurrence(current, block) : null;
|
|
6005
|
+
if (found !== null) {
|
|
6006
|
+
const head = current.slice(0, found.start);
|
|
6007
|
+
const tail = current.slice(found.end);
|
|
6008
|
+
current = head + (head !== "" && tail !== "" && !head.endsWith("\n") ? eol : "") + tail;
|
|
5974
6009
|
continue;
|
|
5975
6010
|
}
|
|
5976
|
-
const
|
|
6011
|
+
const flat = withoutLineEndCr(block).text;
|
|
6012
|
+
const separated = flat.startsWith("\n") || flat.includes("\n\n");
|
|
5977
6013
|
const removable = ours.filter((line) => line === rules.header ? kept.length === 0 && foreign.length === 0 : !kept.includes(line));
|
|
5978
6014
|
for (const line of removable) {
|
|
5979
6015
|
const fileLines = current.split("\n");
|
|
5980
|
-
const
|
|
6016
|
+
const keys = fileLines.map(lineKey);
|
|
6017
|
+
const at = keys.lastIndexOf(line);
|
|
5981
6018
|
if (at < 0)
|
|
5982
6019
|
continue;
|
|
5983
|
-
const withSeparator = line === rules.header && separated && at > 0 &&
|
|
6020
|
+
const withSeparator = line === rules.header && separated && at > 0 && keys[at - 1] === "";
|
|
5984
6021
|
fileLines.splice(withSeparator ? at - 1 : at, withSeparator ? 2 : 1);
|
|
5985
6022
|
current = fileLines.join("\n");
|
|
5986
6023
|
}
|
|
5987
6024
|
}
|
|
5988
6025
|
return { text: current, keptLines, foreignLines };
|
|
5989
6026
|
}
|
|
6027
|
+
function withoutLineEndCr(text) {
|
|
6028
|
+
let out = "";
|
|
6029
|
+
const at = [];
|
|
6030
|
+
let offset = 0;
|
|
6031
|
+
const lines = text.split("\n");
|
|
6032
|
+
for (const [i, line] of lines.entries()) {
|
|
6033
|
+
const key = lineKey(line);
|
|
6034
|
+
for (let j = 0; j < key.length; j++)
|
|
6035
|
+
at.push(offset + j);
|
|
6036
|
+
out += key;
|
|
6037
|
+
offset += line.length;
|
|
6038
|
+
if (i < lines.length - 1) {
|
|
6039
|
+
out += "\n";
|
|
6040
|
+
at.push(offset);
|
|
6041
|
+
offset += 1;
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
return { text: out, at };
|
|
6045
|
+
}
|
|
6046
|
+
function lastOccurrence(text, block) {
|
|
6047
|
+
const needle = withoutLineEndCr(block).text;
|
|
6048
|
+
if (needle === "")
|
|
6049
|
+
return null;
|
|
6050
|
+
const hay = withoutLineEndCr(text);
|
|
6051
|
+
const found = hay.text.lastIndexOf(needle);
|
|
6052
|
+
if (found < 0)
|
|
6053
|
+
return null;
|
|
6054
|
+
const first = hay.at[found];
|
|
6055
|
+
const start = first > 0 && text[first] === "\n" && text[first - 1] === "\r" ? first - 1 : first;
|
|
6056
|
+
return { start, end: hay.at[found + needle.length - 1] + 1 };
|
|
6057
|
+
}
|
|
5990
6058
|
function coveredPathExists(targetRepo, line) {
|
|
5991
6059
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
5992
6060
|
return path !== "" && lexists(join10(targetRepo, path));
|
|
@@ -6111,7 +6179,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
6111
6179
|
if (!changed)
|
|
6112
6180
|
return;
|
|
6113
6181
|
const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
|
|
6114
|
-
writeBack(targetRepo, rel, file.text, empty ? null : serializeJson(value), lists, name);
|
|
6182
|
+
writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
|
|
6115
6183
|
if (adopted && lexists(join10(targetRepo, rel))) {
|
|
6116
6184
|
name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
|
|
6117
6185
|
}
|
|
@@ -6555,6 +6623,21 @@ ${USAGE}`);
|
|
|
6555
6623
|
process.exit(1);
|
|
6556
6624
|
return;
|
|
6557
6625
|
}
|
|
6626
|
+
let interrupted = false;
|
|
6627
|
+
const onInstallSigint = () => {
|
|
6628
|
+
if (interrupted)
|
|
6629
|
+
return;
|
|
6630
|
+
interrupted = true;
|
|
6631
|
+
const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
|
|
6632
|
+
` : "";
|
|
6633
|
+
try {
|
|
6634
|
+
writeSync2(2, `halfcycle: interrupted.
|
|
6635
|
+
${kept}`);
|
|
6636
|
+
} catch {
|
|
6637
|
+
}
|
|
6638
|
+
process.exit(1);
|
|
6639
|
+
};
|
|
6640
|
+
process.on("SIGINT", onInstallSigint);
|
|
6558
6641
|
try {
|
|
6559
6642
|
const controlOrigin = resolveControlOrigin(process.env);
|
|
6560
6643
|
const serviceUrl = controlOrigin.origin;
|
|
@@ -6575,13 +6658,14 @@ ${USAGE}`);
|
|
|
6575
6658
|
let credential;
|
|
6576
6659
|
let notThisAccountMessage;
|
|
6577
6660
|
let actingAccountId;
|
|
6661
|
+
let createdByAccountId;
|
|
6578
6662
|
let actingIdentity;
|
|
6579
6663
|
let foundLine = "";
|
|
6580
6664
|
let reusedWithoutFreshAnswer = false;
|
|
6581
6665
|
if (reusable !== void 0 && pinned !== null) {
|
|
6582
6666
|
engagementId = pinned.engagementId;
|
|
6583
6667
|
credential = reusable;
|
|
6584
|
-
actingAccountId =
|
|
6668
|
+
actingAccountId = heldAnswer?.identity?.accountId;
|
|
6585
6669
|
foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
|
|
6586
6670
|
reusedWithoutFreshAnswer = true;
|
|
6587
6671
|
if (verbose) {
|
|
@@ -6631,6 +6715,7 @@ ${USAGE}`);
|
|
|
6631
6715
|
`);
|
|
6632
6716
|
}
|
|
6633
6717
|
} else {
|
|
6718
|
+
const hadUsableIdBeforeThisRun = readFolderId(targetRepo) !== void 0;
|
|
6634
6719
|
const projectId = runPreCreateChecks(targetRepo);
|
|
6635
6720
|
const created = await createOwnedEngagement(serviceUrl, targetRepo, projectId, { assumeYes, verbose });
|
|
6636
6721
|
if (isProjectOwnedElsewhere(created)) {
|
|
@@ -6642,6 +6727,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6642
6727
|
}
|
|
6643
6728
|
engagementId = created.engagementId;
|
|
6644
6729
|
actingAccountId = created.actingAccountId;
|
|
6730
|
+
createdByAccountId = created.actingAccountId;
|
|
6645
6731
|
actingIdentity = created.actingIdentity;
|
|
6646
6732
|
credential = {
|
|
6647
6733
|
serviceUrl,
|
|
@@ -6661,7 +6747,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6661
6747
|
consoleUrl: created.consoleUrl,
|
|
6662
6748
|
methodVersion: created.methodVersion
|
|
6663
6749
|
};
|
|
6664
|
-
foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
6750
|
+
foundLine = hadUsableIdBeforeThisRun ? `picking up this folder's project (${shortId(created.engagementId)})` : `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
6665
6751
|
if (verbose) {
|
|
6666
6752
|
process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
|
|
6667
6753
|
`);
|
|
@@ -6672,7 +6758,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6672
6758
|
engagementId,
|
|
6673
6759
|
engagementType: engagementTypeRaw,
|
|
6674
6760
|
credential,
|
|
6675
|
-
|
|
6761
|
+
createdByAccountId
|
|
6676
6762
|
});
|
|
6677
6763
|
if (verbose) {
|
|
6678
6764
|
process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
|
|
@@ -6688,8 +6774,14 @@ ${PROJECT_RESET_LINE}
|
|
|
6688
6774
|
`);
|
|
6689
6775
|
}
|
|
6690
6776
|
}
|
|
6691
|
-
|
|
6692
|
-
|
|
6777
|
+
const edited = new Set(result.editedCollidedPaths);
|
|
6778
|
+
const trueCollisions = result.collidedPaths.filter((path) => !edited.has(path));
|
|
6779
|
+
if (result.editedCollidedPaths.length > 0) {
|
|
6780
|
+
process.stdout.write(`[halfcycle] Kept (your edited copy of an older Halfcycle file \u2014 NOT overwritten): ${result.editedCollidedPaths.join(", ")}
|
|
6781
|
+
`);
|
|
6782
|
+
}
|
|
6783
|
+
if (trueCollisions.length > 0) {
|
|
6784
|
+
process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${trueCollisions.join(", ")}
|
|
6693
6785
|
`);
|
|
6694
6786
|
}
|
|
6695
6787
|
if (result.replacedPaths.length > 0) {
|
|
@@ -6744,8 +6836,10 @@ ${PROJECT_RESET_LINE}
|
|
|
6744
6836
|
const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
6745
6837
|
const written = result.writtenPaths.length;
|
|
6746
6838
|
const merged = result.mergedPaths.length;
|
|
6747
|
-
|
|
6839
|
+
if (identity !== void 0) {
|
|
6840
|
+
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
6748
6841
|
`);
|
|
6842
|
+
}
|
|
6749
6843
|
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
6750
6844
|
`);
|
|
6751
6845
|
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 +6851,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6757
6851
|
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
6852
|
`);
|
|
6759
6853
|
} 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
|
|
6854
|
+
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
6855
|
` : `[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
6856
|
`);
|
|
6763
6857
|
}
|