halfcycle 0.4.1 → 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/README.md +1 -1
- package/bin/bin.bundle.mjs +51 -24
- package/dist/claude-version-probe.d.ts.map +1 -1
- package/dist/cli.js +197 -98
- package/dist/cli.js.map +1 -1
- package/dist/confirm-identity.d.ts +31 -36
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +6 -4
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/device-signin.d.ts +35 -2
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/identity.d.ts +7 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -25
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +89 -11
- 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/pre-create-checks.d.ts +27 -0
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -1934,8 +1942,20 @@ function projectPageUrl(engagementId) {
|
|
|
1934
1942
|
}
|
|
1935
1943
|
|
|
1936
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
|
+
}
|
|
1937
1951
|
function isHalfcycleEntry(entry, halfcycleCommands) {
|
|
1938
|
-
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;
|
|
1939
1959
|
}
|
|
1940
1960
|
function mergeSettings(existing, generated) {
|
|
1941
1961
|
const merged = { ...existing };
|
|
@@ -1954,7 +1974,7 @@ function mergeSettings(existing, generated) {
|
|
|
1954
1974
|
const mergedHooks = {};
|
|
1955
1975
|
const events = /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(generatedHooks)]);
|
|
1956
1976
|
for (const event of events) {
|
|
1957
|
-
const developerEntries = (existingHooks[event] ?? []).
|
|
1977
|
+
const developerEntries = (existingHooks[event] ?? []).flatMap((entry) => withoutHalfcycleCommands(entry, halfcycleCommands) ?? []);
|
|
1958
1978
|
const halfcycleEntries = generatedHooks[event] ?? [];
|
|
1959
1979
|
mergedHooks[event] = [...developerEntries, ...halfcycleEntries];
|
|
1960
1980
|
}
|
|
@@ -2122,6 +2142,7 @@ function closedWriteSet() {
|
|
|
2122
2142
|
ownedDirs,
|
|
2123
2143
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
2124
2144
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
2145
|
+
settingsSchema: SETTINGS_SCHEMA,
|
|
2125
2146
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
2126
2147
|
gitattributesLines: /* @__PURE__ */ new Set([
|
|
2127
2148
|
GITATTRIBUTES_HEADER,
|
|
@@ -2143,6 +2164,11 @@ function sha256OfWhatThisVersionWrites(commands) {
|
|
|
2143
2164
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
2144
2165
|
return out;
|
|
2145
2166
|
}
|
|
2167
|
+
function bytesAreOurs(path, bytes, recorded, writeSet) {
|
|
2168
|
+
const hash = sha256Hex(bytes);
|
|
2169
|
+
const recordedHash = recorded !== void 0 && "sha256" in recorded ? recorded.sha256 : void 0;
|
|
2170
|
+
return hash === recordedHash || hash === writeSet.generatedSha256.get(path);
|
|
2171
|
+
}
|
|
2146
2172
|
function readPreviousManifest(targetRepo) {
|
|
2147
2173
|
try {
|
|
2148
2174
|
return parseManifest(readFileSync5(join5(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
|
|
@@ -2291,7 +2317,7 @@ function readPluginManifest() {
|
|
|
2291
2317
|
const raw = readFileSync5(manifestPath, "utf-8");
|
|
2292
2318
|
return JSON.parse(raw);
|
|
2293
2319
|
}
|
|
2294
|
-
function copyManifestCommands(manifest, targetRepo, report, draft) {
|
|
2320
|
+
function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
|
|
2295
2321
|
for (const cmd of manifest.commands ?? []) {
|
|
2296
2322
|
const srcFile = join5(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
2297
2323
|
if (!existsSync3(srcFile)) {
|
|
@@ -2300,8 +2326,13 @@ function copyManifestCommands(manifest, targetRepo, report, draft) {
|
|
|
2300
2326
|
const rel = commandStubRel(cmd.name);
|
|
2301
2327
|
const destFile = join5(targetRepo, rel);
|
|
2302
2328
|
const content = readFileSync5(srcFile, "utf-8");
|
|
2303
|
-
const
|
|
2329
|
+
const recorded = draft.files.get(rel);
|
|
2330
|
+
const { outcome, replacedExisting } = writeCollisionSafe(destFile, targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
2304
2331
|
record(report, outcome, rel);
|
|
2332
|
+
if (replacedExisting)
|
|
2333
|
+
report.replacedPaths.push(rel);
|
|
2334
|
+
if (outcome === "collided" && recorded !== void 0)
|
|
2335
|
+
report.editedCollidedPaths.push(rel);
|
|
2305
2336
|
noteFile(draft, targetRepo, rel, outcome);
|
|
2306
2337
|
}
|
|
2307
2338
|
}
|
|
@@ -2316,18 +2347,23 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
|
|
|
2316
2347
|
writeFileSync5(targetAbsPath, content, "utf-8");
|
|
2317
2348
|
writtenPaths.push(rel);
|
|
2318
2349
|
}
|
|
2319
|
-
function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
2350
|
+
function writeCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
|
|
2320
2351
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
2321
2352
|
if (!isAllowlisted(rel)) {
|
|
2322
2353
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
2323
2354
|
}
|
|
2355
|
+
let replacedExisting = false;
|
|
2324
2356
|
if (existsSync3(targetAbsPath)) {
|
|
2325
|
-
const current = readFileSync5(targetAbsPath
|
|
2326
|
-
|
|
2357
|
+
const current = readFileSync5(targetAbsPath);
|
|
2358
|
+
if (current.toString("utf-8") === content)
|
|
2359
|
+
return { outcome: "skipped", replacedExisting: false };
|
|
2360
|
+
if (!isOurs(current))
|
|
2361
|
+
return { outcome: "collided", replacedExisting: false };
|
|
2362
|
+
replacedExisting = true;
|
|
2327
2363
|
}
|
|
2328
2364
|
mkdirSync5(dirname3(targetAbsPath), { recursive: true });
|
|
2329
2365
|
writeFileSync5(targetAbsPath, content, "utf-8");
|
|
2330
|
-
return "written";
|
|
2366
|
+
return { outcome: "written", replacedExisting };
|
|
2331
2367
|
}
|
|
2332
2368
|
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
2333
2369
|
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
@@ -2420,6 +2456,7 @@ function settingsParseRefusal(targetRepo, err) {
|
|
|
2420
2456
|
}
|
|
2421
2457
|
return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
|
|
2422
2458
|
}
|
|
2459
|
+
var SETTINGS_SCHEMA = "https://json.schemastore.org/claude-code-settings.json";
|
|
2423
2460
|
function generateSettingsJson() {
|
|
2424
2461
|
const hooks = {};
|
|
2425
2462
|
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
@@ -2429,7 +2466,7 @@ function generateSettingsJson() {
|
|
|
2429
2466
|
});
|
|
2430
2467
|
}
|
|
2431
2468
|
const settings = {
|
|
2432
|
-
$schema:
|
|
2469
|
+
$schema: SETTINGS_SCHEMA,
|
|
2433
2470
|
permissions: {
|
|
2434
2471
|
// deny ONLY. See DENY_PATTERNS — an allow list evaporates under bypass mode
|
|
2435
2472
|
// and is the machine owner's grant to make, not ours.
|
|
@@ -2729,19 +2766,27 @@ function adoptBlockUnderHeader(before, header, claimable) {
|
|
|
2729
2766
|
let underHeader = false;
|
|
2730
2767
|
const lines = before.split("\n");
|
|
2731
2768
|
for (const [i, line] of lines.entries()) {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2769
|
+
const key = lineKey(line);
|
|
2770
|
+
if (key === header) {
|
|
2771
|
+
if (i > 0 && lineKey(lines[i - 1]) === "")
|
|
2772
|
+
claimed.push(lines[i - 1]);
|
|
2735
2773
|
claimed.push(line);
|
|
2736
2774
|
underHeader = true;
|
|
2737
|
-
} else if (
|
|
2775
|
+
} else if (key.trim() === "") {
|
|
2738
2776
|
underHeader = false;
|
|
2739
|
-
} else if (underHeader && claimable.has(
|
|
2777
|
+
} else if (underHeader && claimable.has(key)) {
|
|
2740
2778
|
claimed.push(line);
|
|
2741
2779
|
}
|
|
2742
2780
|
}
|
|
2743
2781
|
return claimed.length > 0 ? claimed.join("\n") + "\n" : "";
|
|
2744
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
|
+
}
|
|
2745
2790
|
function gitignoreCovers(content) {
|
|
2746
2791
|
const lines = new Set(content.split("\n").map((l) => l.trim()));
|
|
2747
2792
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
@@ -2760,9 +2805,8 @@ function reconcileGitignore(targetRepo) {
|
|
|
2760
2805
|
return "skipped";
|
|
2761
2806
|
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
2762
2807
|
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2765
|
-
` + toAdd.join("\n") + "\n";
|
|
2808
|
+
const eol = lineEndingOf(existing);
|
|
2809
|
+
const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
2766
2810
|
writeFileSync5(gitignorePath, existing + suffix, "utf-8");
|
|
2767
2811
|
return "written";
|
|
2768
2812
|
} catch {
|
|
@@ -2797,8 +2841,9 @@ function reconcileGitattributes(targetRepo) {
|
|
|
2797
2841
|
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
2798
2842
|
if (toAdd.length === 0)
|
|
2799
2843
|
return "skipped";
|
|
2800
|
-
const
|
|
2801
|
-
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;
|
|
2802
2847
|
writeAllowlisted(path, targetRepo, existing + suffix, routed);
|
|
2803
2848
|
return "written";
|
|
2804
2849
|
} catch {
|
|
@@ -2818,9 +2863,9 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
2818
2863
|
function nonEmpty(value) {
|
|
2819
2864
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
2820
2865
|
}
|
|
2821
|
-
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType,
|
|
2866
|
+
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion, writtenPaths) {
|
|
2822
2867
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
2823
|
-
const carried =
|
|
2868
|
+
const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
|
|
2824
2869
|
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
2825
2870
|
const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2826
2871
|
const projectPage = projectPageUrl(engagementId);
|
|
@@ -2829,7 +2874,7 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
|
|
|
2829
2874
|
engagementId,
|
|
2830
2875
|
engagementType,
|
|
2831
2876
|
installedAt,
|
|
2832
|
-
...carried !== void 0 ? {
|
|
2877
|
+
...carried !== void 0 ? { createdByAccountId: carried } : {},
|
|
2833
2878
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
2834
2879
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
2835
2880
|
};
|
|
@@ -2846,6 +2891,9 @@ function writeCrewRoster(targetRepoRoot, report) {
|
|
|
2846
2891
|
const crewPath = join5(targetRepoRoot, CREW_ROSTER_REL);
|
|
2847
2892
|
return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
|
|
2848
2893
|
}
|
|
2894
|
+
function pinCreatedByAccountId(pin) {
|
|
2895
|
+
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
2896
|
+
}
|
|
2849
2897
|
function readBundlePin(targetRepoRoot) {
|
|
2850
2898
|
const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
|
|
2851
2899
|
if (!existsSync3(pinPath))
|
|
@@ -2987,7 +3035,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
2987
3035
|
}
|
|
2988
3036
|
}
|
|
2989
3037
|
async function install(options) {
|
|
2990
|
-
const { targetRepo, engagementId, engagementType, credential, home,
|
|
3038
|
+
const { targetRepo, engagementId, engagementType, credential, home, createdByAccountId } = options;
|
|
2991
3039
|
assertEngagementId(engagementId);
|
|
2992
3040
|
if (!existsSync3(targetRepo)) {
|
|
2993
3041
|
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
@@ -2998,12 +3046,13 @@ async function install(options) {
|
|
|
2998
3046
|
skippedPaths: [],
|
|
2999
3047
|
mergedPaths: [],
|
|
3000
3048
|
collidedPaths: [],
|
|
3049
|
+
editedCollidedPaths: [],
|
|
3001
3050
|
replacedPaths: []
|
|
3002
3051
|
};
|
|
3003
3052
|
const writeSet = closedWriteSet();
|
|
3004
3053
|
const draft = startManifest(readPreviousManifest(targetRepo), (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
|
|
3005
3054
|
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
3006
|
-
copyManifestCommands(manifest, targetRepo, report, draft);
|
|
3055
|
+
copyManifestCommands(manifest, targetRepo, report, draft, writeSet);
|
|
3007
3056
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
3008
3057
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
3009
3058
|
const vendoredOutcome = recordOwned(report, vendoredBinDest, targetRepo, readFileSync5(vendoredBinSrc, "utf-8"), VENDORED_BIN_REL);
|
|
@@ -3125,7 +3174,7 @@ async function install(options) {
|
|
|
3125
3174
|
case "absent":
|
|
3126
3175
|
break;
|
|
3127
3176
|
}
|
|
3128
|
-
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType,
|
|
3177
|
+
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, createdByAccountId, credential?.methodVersion, report.writtenPaths);
|
|
3129
3178
|
noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
|
|
3130
3179
|
noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
|
|
3131
3180
|
const scanResult = runBootstrapScan(targetRepo);
|
|
@@ -3145,6 +3194,7 @@ async function install(options) {
|
|
|
3145
3194
|
skippedPaths: report.skippedPaths,
|
|
3146
3195
|
mergedPaths: report.mergedPaths,
|
|
3147
3196
|
collidedPaths: report.collidedPaths,
|
|
3197
|
+
editedCollidedPaths: report.editedCollidedPaths,
|
|
3148
3198
|
replacedPaths: report.replacedPaths,
|
|
3149
3199
|
bootstrapScanRan: scanResult.ran,
|
|
3150
3200
|
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
@@ -3329,8 +3379,8 @@ var SignInRefused = class extends Error {
|
|
|
3329
3379
|
}
|
|
3330
3380
|
};
|
|
3331
3381
|
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.
|
|
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.
|
|
3382
|
+
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.";
|
|
3383
|
+
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
3384
|
var DEADLINE_SLACK_INTERVALS = 1;
|
|
3335
3385
|
var STILL_WAITING_MS = 15e3;
|
|
3336
3386
|
var DEFAULT_SIGN_IN_REASON = "set up this project";
|
|
@@ -3425,6 +3475,9 @@ function isNonInteractive(env = process.env) {
|
|
|
3425
3475
|
const ci = (env["CI"] ?? "").trim().toLowerCase();
|
|
3426
3476
|
return ci !== "" && ci !== "0" && ci !== "false";
|
|
3427
3477
|
}
|
|
3478
|
+
function isInteractiveRun(env = process.env) {
|
|
3479
|
+
return process.stdin.isTTY === true && !isNonInteractive(env);
|
|
3480
|
+
}
|
|
3428
3481
|
async function startDeviceAuth(serviceUrl) {
|
|
3429
3482
|
const url = `${serviceUrl.replace(/\/+$/, "")}/auth/device/start`;
|
|
3430
3483
|
let res;
|
|
@@ -3435,16 +3488,16 @@ async function startDeviceAuth(serviceUrl) {
|
|
|
3435
3488
|
body: "{}"
|
|
3436
3489
|
});
|
|
3437
3490
|
} 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)}
|
|
3491
|
+
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
3492
|
}
|
|
3440
3493
|
const body = await res.json().catch(() => null);
|
|
3441
3494
|
if (res.status !== 201) {
|
|
3442
3495
|
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}
|
|
3496
|
+
throw new SignInRefused(refusal?.status ?? String(res.status), refusal?.message ?? `Sign-in could not be started: ${url} answered ${res.status}.`);
|
|
3444
3497
|
}
|
|
3445
3498
|
const parsed = deviceAuthStartResponseSchema.safeParse(body);
|
|
3446
3499
|
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
|
|
3500
|
+
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
3501
|
}
|
|
3449
3502
|
return parsed.data;
|
|
3450
3503
|
}
|
|
@@ -3582,7 +3635,7 @@ async function signIn(serviceUrl, deps = {}) {
|
|
|
3582
3635
|
const url = loopbackVerificationUrl(started.verificationUrl, bind.target);
|
|
3583
3636
|
const attempt = await openBrowser(url, env);
|
|
3584
3637
|
if (attempt.opened) {
|
|
3585
|
-
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.
|
|
3586
3639
|
`);
|
|
3587
3640
|
const startedAt = now();
|
|
3588
3641
|
const redirect = bind.waitForCallback({
|
|
@@ -3709,7 +3762,7 @@ async function createEngagementForProject(baseUrl, projectId, credential, derive
|
|
|
3709
3762
|
const body = { derivedName };
|
|
3710
3763
|
return requestEngagementValues("PUT", `${baseUrl.replace(/\/+$/, "")}/engagements/by-project/${encodeURIComponent(projectId)}`, body, credential, {
|
|
3711
3764
|
action: "Project setup",
|
|
3712
|
-
nothingHappened: "
|
|
3765
|
+
nothingHappened: "Halfcycle did not finish setting up this folder",
|
|
3713
3766
|
route: "PUT /engagements/by-project/:projectId",
|
|
3714
3767
|
// FALSE on this call, and only this one: the folder's id is already on
|
|
3715
3768
|
// disk by the time this request is sent. See CallShape.alsoNothingWritten.
|
|
@@ -4147,21 +4200,15 @@ async function askAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
|
4147
4200
|
async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
4148
4201
|
return (await askAccount(serviceUrl, credential, fetchImpl)).identity;
|
|
4149
4202
|
}
|
|
4150
|
-
function accountLabel(identity, storedAccountId, verbose2 = false
|
|
4203
|
+
function accountLabel(identity, storedAccountId, verbose2 = false) {
|
|
4151
4204
|
const accountId = identity?.accountId ?? storedAccountId;
|
|
4152
4205
|
if (identity?.email !== void 0 && identity.email.trim() !== "") {
|
|
4153
4206
|
return verbose2 && accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
|
|
4154
4207
|
}
|
|
4155
|
-
if (!confirmed) {
|
|
4156
|
-
return "the account this project is already set up with \u2014 this run could not reach the Halfcycle service to confirm who that is";
|
|
4157
|
-
}
|
|
4158
4208
|
if (accountId !== void 0)
|
|
4159
4209
|
return `account ${accountId} (no email on record for it)`;
|
|
4160
4210
|
return "the account this credential belongs to, which this service did not name";
|
|
4161
4211
|
}
|
|
4162
|
-
function isInteractiveRun(env = process.env) {
|
|
4163
|
-
return process.stdin.isTTY === true && !isNonInteractive(env);
|
|
4164
|
-
}
|
|
4165
4212
|
function onlyDate(iso) {
|
|
4166
4213
|
if (iso === void 0)
|
|
4167
4214
|
return void 0;
|
|
@@ -4210,7 +4257,7 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4210
4257
|
switch (credential.source) {
|
|
4211
4258
|
case "browser-handoff":
|
|
4212
4259
|
return {
|
|
4213
|
-
preamble: `[halfcycle]
|
|
4260
|
+
preamble: `[halfcycle] This install will ${actClause(act)}.
|
|
4214
4261
|
`,
|
|
4215
4262
|
asks: false,
|
|
4216
4263
|
noTerminalNote: "",
|
|
@@ -4232,7 +4279,7 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4232
4279
|
noTerminalNote: "",
|
|
4233
4280
|
switchOpening: `[halfcycle] Signing you in through your browser \u2014 this install will use that account instead.
|
|
4234
4281
|
`,
|
|
4235
|
-
stopRemedy: `Stopped before anything was
|
|
4282
|
+
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
4283
|
// The switch is real for THIS run (the new credential is what gets used) and it
|
|
4237
4284
|
// does not persist: `obtainCredential` reads the variable first, so the next run
|
|
4238
4285
|
// is the same variable again. Said here rather than discovered tomorrow.
|
|
@@ -4254,11 +4301,11 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4254
4301
|
// this is printed in), and where to look for the answer once this output is gone.
|
|
4255
4302
|
noTerminalNote: `[halfcycle] Nothing here to ask at (no terminal), so this is continuing as that account.
|
|
4256
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.
|
|
4257
|
-
[halfcycle]
|
|
4304
|
+
[halfcycle] .halfcycle/bundle.json records the account that created this project, so that can be checked after this output is gone.
|
|
4258
4305
|
`,
|
|
4259
4306
|
switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
|
|
4260
4307
|
`,
|
|
4261
|
-
stopRemedy: `Stopped before anything was
|
|
4308
|
+
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
4309
|
afterSwitch: ""
|
|
4263
4310
|
};
|
|
4264
4311
|
}
|
|
@@ -4299,7 +4346,7 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
|
4299
4346
|
write(script.switchOpening);
|
|
4300
4347
|
const replacement = await switchAccount(serviceUrl, deps);
|
|
4301
4348
|
const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
|
|
4302
|
-
write(`[halfcycle] Signed in
|
|
4349
|
+
write(`[halfcycle] Signed in. This install will ${actClause(act)}.
|
|
4303
4350
|
`);
|
|
4304
4351
|
if (script.afterSwitch !== "")
|
|
4305
4352
|
write(script.afterSwitch);
|
|
@@ -4415,6 +4462,7 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
|
|
|
4415
4462
|
import { spawnSync } from "node:child_process";
|
|
4416
4463
|
import { existsSync as existsSync4, readFileSync as readFileSync6, realpathSync } from "node:fs";
|
|
4417
4464
|
import { delimiter, dirname as dirname4, join as join6 } from "node:path";
|
|
4465
|
+
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
4466
|
var PreCreateRefused = class extends Error {
|
|
4419
4467
|
constructor(message) {
|
|
4420
4468
|
super(message);
|
|
@@ -4497,18 +4545,22 @@ function claimFolderId(targetRepo) {
|
|
|
4497
4545
|
} catch {
|
|
4498
4546
|
throw new PreCreateRefused(`cannot write to ${targetRepo} \u2014 run "npx halfcycle" in a folder you can write to.`);
|
|
4499
4547
|
}
|
|
4500
|
-
const
|
|
4548
|
+
const folderId = readFolderId(targetRepo);
|
|
4549
|
+
if (folderId === void 0) {
|
|
4550
|
+
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.`);
|
|
4551
|
+
}
|
|
4552
|
+
return folderId;
|
|
4553
|
+
}
|
|
4554
|
+
function readFolderId(targetRepo) {
|
|
4501
4555
|
let folderId;
|
|
4502
4556
|
try {
|
|
4503
|
-
|
|
4557
|
+
const text = readFileSync6(join6(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
|
|
4558
|
+
folderId = JSON.parse(text)?.projectId;
|
|
4504
4559
|
} catch {
|
|
4505
|
-
|
|
4560
|
+
return void 0;
|
|
4506
4561
|
}
|
|
4507
4562
|
const parsed = projectIdSchema.safeParse(folderId);
|
|
4508
|
-
|
|
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;
|
|
4563
|
+
return parsed.success ? parsed.data : void 0;
|
|
4512
4564
|
}
|
|
4513
4565
|
function checkSettingsParse(targetRepo) {
|
|
4514
4566
|
try {
|
|
@@ -4603,9 +4655,9 @@ var UPGRADE_COMMANDS = {
|
|
|
4603
4655
|
};
|
|
4604
4656
|
var INSTALL_METHOD_RULES = [
|
|
4605
4657
|
{ method: "winget", pattern: /[\\/]WinGet[\\/]/i },
|
|
4606
|
-
{ method: "npm", pattern: /node_modules[\\/]
|
|
4658
|
+
{ method: "npm", pattern: /node_modules[\\/]|[\\/]npm[\\/]claude(\.cmd|\.exe)?$/i },
|
|
4607
4659
|
{ method: "brew", pattern: /[\\/](Cellar|Caskroom|linuxbrew|homebrew)[\\/]/i },
|
|
4608
|
-
{ method: "native", pattern: /[\\/]\.local[\\/]share[\\/]claude(-code)
|
|
4660
|
+
{ method: "native", pattern: /[\\/]\.local[\\/](share[\\/]claude(-code)?|bin)[\\/]/i }
|
|
4609
4661
|
];
|
|
4610
4662
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
4611
4663
|
const env = deps.env ?? process.env;
|
|
@@ -5810,9 +5862,6 @@ function ourHookEntries(value, commands) {
|
|
|
5810
5862
|
}
|
|
5811
5863
|
return found;
|
|
5812
5864
|
}
|
|
5813
|
-
function isOurHook(hook, commands) {
|
|
5814
|
-
return isPlainObject2(hook) && typeof hook["command"] === "string" && commands.has(hook["command"]);
|
|
5815
|
-
}
|
|
5816
5865
|
function ourMcpServers(value) {
|
|
5817
5866
|
const servers = value["mcpServers"];
|
|
5818
5867
|
return isPlainObject2(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
@@ -5841,9 +5890,9 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5841
5890
|
continue;
|
|
5842
5891
|
}
|
|
5843
5892
|
touched = true;
|
|
5844
|
-
const remaining = entry
|
|
5845
|
-
if (remaining
|
|
5846
|
-
next.push(
|
|
5893
|
+
const remaining = withoutHalfcycleCommands(entry, commands);
|
|
5894
|
+
if (remaining !== null)
|
|
5895
|
+
next.push(remaining);
|
|
5847
5896
|
}
|
|
5848
5897
|
if (touched)
|
|
5849
5898
|
changed = true;
|
|
@@ -5882,16 +5931,12 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5882
5931
|
changed = true;
|
|
5883
5932
|
}
|
|
5884
5933
|
}
|
|
5885
|
-
if ("schemaBefore" in rec) {
|
|
5886
|
-
if (rec.schemaBefore === null)
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
changed = true;
|
|
5890
|
-
}
|
|
5891
|
-
} 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
|
|
5892
5938
|
value["$schema"] = rec.schemaBefore;
|
|
5893
|
-
|
|
5894
|
-
}
|
|
5939
|
+
changed = true;
|
|
5895
5940
|
}
|
|
5896
5941
|
return { changed, refused };
|
|
5897
5942
|
}
|
|
@@ -5931,36 +5976,70 @@ function gitattributesRules(writeSet) {
|
|
|
5931
5976
|
function trimAppendedBlocks(text, rec, rules) {
|
|
5932
5977
|
const keptLines = [];
|
|
5933
5978
|
const foreignLines = [];
|
|
5979
|
+
const eol = lineEndingOf(text);
|
|
5934
5980
|
let current = text;
|
|
5935
5981
|
for (const block of [...rec.appended].reverse()) {
|
|
5936
|
-
const lines = block.split("\n").filter((line) => line !== "");
|
|
5982
|
+
const lines = block.split("\n").map(lineKey).filter((line) => line !== "");
|
|
5937
5983
|
const foreign = lines.filter((line) => !rules.ourLines.has(line));
|
|
5938
5984
|
foreignLines.push(...foreign);
|
|
5939
5985
|
const ours = lines.filter((line) => rules.ourLines.has(line));
|
|
5940
5986
|
const entries = ours.filter((line) => line !== rules.header);
|
|
5941
5987
|
const kept = entries.filter((line) => rules.keepLine(line));
|
|
5942
5988
|
keptLines.push(...kept);
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
const head = current.slice(0,
|
|
5946
|
-
const tail = current.slice(
|
|
5947
|
-
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;
|
|
5948
5994
|
continue;
|
|
5949
5995
|
}
|
|
5950
|
-
const
|
|
5996
|
+
const flat = withoutLineEndCr(block).text;
|
|
5997
|
+
const separated = flat.startsWith("\n") || flat.includes("\n\n");
|
|
5951
5998
|
const removable = ours.filter((line) => line === rules.header ? kept.length === 0 && foreign.length === 0 : !kept.includes(line));
|
|
5952
5999
|
for (const line of removable) {
|
|
5953
6000
|
const fileLines = current.split("\n");
|
|
5954
|
-
const
|
|
6001
|
+
const keys = fileLines.map(lineKey);
|
|
6002
|
+
const at = keys.lastIndexOf(line);
|
|
5955
6003
|
if (at < 0)
|
|
5956
6004
|
continue;
|
|
5957
|
-
const withSeparator = line === rules.header && separated && at > 0 &&
|
|
6005
|
+
const withSeparator = line === rules.header && separated && at > 0 && keys[at - 1] === "";
|
|
5958
6006
|
fileLines.splice(withSeparator ? at - 1 : at, withSeparator ? 2 : 1);
|
|
5959
6007
|
current = fileLines.join("\n");
|
|
5960
6008
|
}
|
|
5961
6009
|
}
|
|
5962
6010
|
return { text: current, keptLines, foreignLines };
|
|
5963
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
|
+
}
|
|
5964
6043
|
function coveredPathExists(targetRepo, line) {
|
|
5965
6044
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
5966
6045
|
return path !== "" && lexists(join10(targetRepo, path));
|
|
@@ -6111,16 +6190,15 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
6111
6190
|
return;
|
|
6112
6191
|
}
|
|
6113
6192
|
if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
|
|
6114
|
-
let
|
|
6193
|
+
let bytes;
|
|
6115
6194
|
try {
|
|
6116
|
-
|
|
6195
|
+
bytes = readFileSync9(abs);
|
|
6117
6196
|
} catch {
|
|
6118
6197
|
name(lists.failed, path, path);
|
|
6119
6198
|
return;
|
|
6120
6199
|
}
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
name(recordedHash !== void 0 ? lists.kept : lists.leftAlone, path, path);
|
|
6200
|
+
if (!bytesAreOurs(path, bytes, entry, writeSet)) {
|
|
6201
|
+
name(entry !== void 0 && "sha256" in entry ? lists.kept : lists.leftAlone, path, path);
|
|
6124
6202
|
return;
|
|
6125
6203
|
}
|
|
6126
6204
|
}
|
|
@@ -6530,6 +6608,14 @@ ${USAGE}`);
|
|
|
6530
6608
|
process.exit(1);
|
|
6531
6609
|
return;
|
|
6532
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);
|
|
6533
6619
|
try {
|
|
6534
6620
|
const controlOrigin = resolveControlOrigin(process.env);
|
|
6535
6621
|
const serviceUrl = controlOrigin.origin;
|
|
@@ -6550,13 +6636,14 @@ ${USAGE}`);
|
|
|
6550
6636
|
let credential;
|
|
6551
6637
|
let notThisAccountMessage;
|
|
6552
6638
|
let actingAccountId;
|
|
6639
|
+
let createdByAccountId;
|
|
6553
6640
|
let actingIdentity;
|
|
6554
6641
|
let foundLine = "";
|
|
6555
6642
|
let reusedWithoutFreshAnswer = false;
|
|
6556
6643
|
if (reusable !== void 0 && pinned !== null) {
|
|
6557
6644
|
engagementId = pinned.engagementId;
|
|
6558
6645
|
credential = reusable;
|
|
6559
|
-
actingAccountId =
|
|
6646
|
+
actingAccountId = heldAnswer?.identity?.accountId;
|
|
6560
6647
|
foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
|
|
6561
6648
|
reusedWithoutFreshAnswer = true;
|
|
6562
6649
|
if (verbose) {
|
|
@@ -6606,6 +6693,7 @@ ${USAGE}`);
|
|
|
6606
6693
|
`);
|
|
6607
6694
|
}
|
|
6608
6695
|
} else {
|
|
6696
|
+
const hadUsableIdBeforeThisRun = readFolderId(targetRepo) !== void 0;
|
|
6609
6697
|
const projectId = runPreCreateChecks(targetRepo);
|
|
6610
6698
|
const created = await createOwnedEngagement(serviceUrl, targetRepo, projectId, { assumeYes, verbose });
|
|
6611
6699
|
if (isProjectOwnedElsewhere(created)) {
|
|
@@ -6617,6 +6705,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6617
6705
|
}
|
|
6618
6706
|
engagementId = created.engagementId;
|
|
6619
6707
|
actingAccountId = created.actingAccountId;
|
|
6708
|
+
createdByAccountId = created.actingAccountId;
|
|
6620
6709
|
actingIdentity = created.actingIdentity;
|
|
6621
6710
|
credential = {
|
|
6622
6711
|
serviceUrl,
|
|
@@ -6636,7 +6725,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6636
6725
|
consoleUrl: created.consoleUrl,
|
|
6637
6726
|
methodVersion: created.methodVersion
|
|
6638
6727
|
};
|
|
6639
|
-
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)})`;
|
|
6640
6729
|
if (verbose) {
|
|
6641
6730
|
process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
|
|
6642
6731
|
`);
|
|
@@ -6647,7 +6736,7 @@ ${PROJECT_RESET_LINE}
|
|
|
6647
6736
|
engagementId,
|
|
6648
6737
|
engagementType: engagementTypeRaw,
|
|
6649
6738
|
credential,
|
|
6650
|
-
|
|
6739
|
+
createdByAccountId
|
|
6651
6740
|
});
|
|
6652
6741
|
if (verbose) {
|
|
6653
6742
|
process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
|
|
@@ -6663,8 +6752,14 @@ ${PROJECT_RESET_LINE}
|
|
|
6663
6752
|
`);
|
|
6664
6753
|
}
|
|
6665
6754
|
}
|
|
6666
|
-
|
|
6667
|
-
|
|
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(", ")}
|
|
6668
6763
|
`);
|
|
6669
6764
|
}
|
|
6670
6765
|
if (result.replacedPaths.length > 0) {
|
|
@@ -6719,8 +6814,10 @@ ${PROJECT_RESET_LINE}
|
|
|
6719
6814
|
const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
6720
6815
|
const written = result.writtenPaths.length;
|
|
6721
6816
|
const merged = result.mergedPaths.length;
|
|
6722
|
-
|
|
6817
|
+
if (identity !== void 0) {
|
|
6818
|
+
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
6723
6819
|
`);
|
|
6820
|
+
}
|
|
6724
6821
|
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
6725
6822
|
`);
|
|
6726
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"}
|
|
@@ -6732,7 +6829,8 @@ ${PROJECT_RESET_LINE}
|
|
|
6732
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.
|
|
6733
6830
|
`);
|
|
6734
6831
|
} else if (credential) {
|
|
6735
|
-
process.stdout.write(`[halfcycle]
|
|
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.
|
|
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.
|
|
6736
6834
|
`);
|
|
6737
6835
|
}
|
|
6738
6836
|
if (credential) {
|
|
@@ -6759,22 +6857,23 @@ ${PROJECT_RESET_LINE}
|
|
|
6759
6857
|
`);
|
|
6760
6858
|
process.exit(0);
|
|
6761
6859
|
} catch (err) {
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
`
|
|
6860
|
+
const stop = (message) => {
|
|
6861
|
+
const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
|
|
6862
|
+
` : "";
|
|
6863
|
+
process.stderr.write(`${message}
|
|
6864
|
+
${kept}`);
|
|
6765
6865
|
process.exit(1);
|
|
6866
|
+
};
|
|
6867
|
+
if (err instanceof SignInRefused || err instanceof PreCreateRefused || err instanceof SettingsParseRefused) {
|
|
6868
|
+
stop(`halfcycle: ${err.message}`);
|
|
6766
6869
|
return;
|
|
6767
6870
|
}
|
|
6768
6871
|
const fsCode = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
6769
6872
|
if (fsCode !== void 0) {
|
|
6770
|
-
|
|
6771
|
-
`);
|
|
6772
|
-
process.exit(1);
|
|
6873
|
+
stop(`halfcycle: could not write to ${targetRepo} \u2014 check that you can write to this folder and run "npx halfcycle" again.`);
|
|
6773
6874
|
return;
|
|
6774
6875
|
}
|
|
6775
|
-
|
|
6776
|
-
`);
|
|
6777
|
-
process.exit(1);
|
|
6876
|
+
stop(`halfcycle install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6778
6877
|
}
|
|
6779
6878
|
return;
|
|
6780
6879
|
}
|