halfcycle 0.11.0 → 0.12.0-rc.1
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 +525 -144
- package/bin/pre-tool-use-filter.sh +1 -0
- package/dist/banner-facts.d.ts +7 -2
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/check-criteria.d.ts +1 -1
- package/dist/check-criteria.d.ts.map +1 -1
- package/dist/check-report.d.ts +1 -1
- package/dist/check-report.d.ts.map +1 -1
- package/dist/check-tasks.d.ts +10 -1
- package/dist/check-tasks.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +1018 -731
- package/dist/cli.js.map +3 -3
- package/dist/crew.d.ts +37 -0
- package/dist/crew.d.ts.map +1 -0
- package/dist/draw/index.d.ts +1 -1
- package/dist/draw/index.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +4 -0
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/failure-line.d.ts +3 -2
- package/dist/failure-line.d.ts.map +1 -1
- package/dist/folder-refusals.d.ts.map +1 -1
- package/dist/index.js +271 -206
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +20 -7
- package/dist/install.d.ts.map +1 -1
- package/dist/local-allow.d.ts +10 -3
- package/dist/local-allow.d.ts.map +1 -1
- package/dist/open-phase.d.ts +1 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/own-version.d.ts +42 -0
- package/dist/own-version.d.ts.map +1 -0
- package/dist/pace.d.ts +2 -2
- package/dist/pace.d.ts.map +1 -1
- package/dist/plan.d.ts +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/ready.d.ts +1 -1
- package/dist/ready.d.ts.map +1 -1
- package/dist/regression.d.ts +48 -0
- package/dist/regression.d.ts.map +1 -0
- package/dist/request-shapes-verb.d.ts +1 -1
- package/dist/request-shapes-verb.d.ts.map +1 -1
- package/dist/restore.d.ts +1 -1
- package/dist/restore.d.ts.map +1 -1
- package/dist/review.d.ts +2 -2
- package/dist/review.d.ts.map +1 -1
- package/dist/route.d.ts +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/size.d.ts +6 -4
- package/dist/size.d.ts.map +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// dist/install.js
|
|
2
2
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3
3
|
import { createHash as createHash2 } from "node:crypto";
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5
5
|
import { homedir as homedir2 } from "node:os";
|
|
6
|
-
import { dirname as
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, posix as posix2, relative, resolve } from "node:path";
|
|
8
7
|
|
|
9
8
|
// ../core/dist/guard-record.js
|
|
10
9
|
import { z } from "zod";
|
|
@@ -268,14 +267,40 @@ function looksLikeSecret(text) {
|
|
|
268
267
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
269
268
|
var MCP_SERVER_KEY = "halfcycle";
|
|
270
269
|
|
|
270
|
+
// ../core/dist/release-version.js
|
|
271
|
+
var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
272
|
+
|
|
273
|
+
// ../core/dist/release-tag.js
|
|
274
|
+
var PRE_RELEASE_VERSION_PATTERN = /^(\d+\.\d+\.\d+)-rc\.\d+$/;
|
|
275
|
+
var PACKAGE_COMMAND = "npx halfcycle";
|
|
276
|
+
var COMMAND_FORMS = /npx halfcycle(?:@latest|(?![@\p{L}\p{N}-]))/gu;
|
|
277
|
+
function isPreReleaseVersion(value) {
|
|
278
|
+
return typeof value === "string" && PRE_RELEASE_VERSION_PATTERN.test(value);
|
|
279
|
+
}
|
|
280
|
+
function releaseTagOfVersion(version) {
|
|
281
|
+
return isPreReleaseVersion(version) ? "next" : "latest";
|
|
282
|
+
}
|
|
283
|
+
function halfcycleCommand(version) {
|
|
284
|
+
return `${PACKAGE_COMMAND}@${releaseTagOfVersion(version)}`;
|
|
285
|
+
}
|
|
286
|
+
function bareHalfcycleCommand(version) {
|
|
287
|
+
return releaseTagOfVersion(version) === "next" ? `${PACKAGE_COMMAND}@next` : PACKAGE_COMMAND;
|
|
288
|
+
}
|
|
289
|
+
function rewriteHalfcycleCommands(text, tag) {
|
|
290
|
+
if (tag === "latest")
|
|
291
|
+
return text;
|
|
292
|
+
return text.replace(COMMAND_FORMS, `${PACKAGE_COMMAND}@${tag}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
271
295
|
// ../core/dist/shell-quote.js
|
|
272
296
|
function quoteForShell(value) {
|
|
273
297
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
274
298
|
}
|
|
275
|
-
var HALFCYCLE_COMMAND =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
var HALFCYCLE_COMMAND = halfcycleCommand();
|
|
300
|
+
function doctorLine(version) {
|
|
301
|
+
return `Run \`${bareHalfcycleCommand(version)} doctor\` to check set up, sign-in, connection, checks and version.`;
|
|
302
|
+
}
|
|
303
|
+
var DOCTOR_LINE = doctorLine();
|
|
279
304
|
|
|
280
305
|
// ../core/dist/github-address.js
|
|
281
306
|
var GITHUB_REPOSITORY_ADDRESS = /^https:\/\/github\.com\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/(?!\.{1,2}$)[A-Za-z0-9._-]{1,100}$/;
|
|
@@ -293,8 +318,14 @@ var REMOTE_FORMS = [
|
|
|
293
318
|
];
|
|
294
319
|
|
|
295
320
|
// ../core/dist/sign-in.js
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
function signInCommand(version) {
|
|
322
|
+
return `"${bareHalfcycleCommand(version)}"`;
|
|
323
|
+
}
|
|
324
|
+
var SIGN_IN_COMMAND = signInCommand();
|
|
325
|
+
function hookRemedy(version) {
|
|
326
|
+
return `Run ${signInCommand(version)} in the project folder to sign in.`;
|
|
327
|
+
}
|
|
328
|
+
var HOOK_REMEDY = hookRemedy();
|
|
298
329
|
|
|
299
330
|
// ../core/dist/task-plan.js
|
|
300
331
|
import { z as z2 } from "zod";
|
|
@@ -408,7 +439,7 @@ function codexHookCommand(script, args) {
|
|
|
408
439
|
const invocation = `cd "$(git rev-parse --show-toplevel)" && bash ./${script}`;
|
|
409
440
|
return args === void 0 ? invocation : `${invocation} ${args}`;
|
|
410
441
|
}
|
|
411
|
-
var CODEX_WINDOWS_NOT_SET_UP =
|
|
442
|
+
var CODEX_WINDOWS_NOT_SET_UP = `Halfcycle checks did not run because Halfcycle is not set up in this folder or any folder above it; run ${bareHalfcycleCommand()} in the project folder.`;
|
|
412
443
|
var CODEX_WINDOWS_NO_GIT_BASH = "Halfcycle checks did not run because Git for Windows was not found; install Git for Windows, then open a new session.";
|
|
413
444
|
function codexHookCommandWindows(script, args) {
|
|
414
445
|
const tick = (text) => "`" + text + "`";
|
|
@@ -3135,8 +3166,14 @@ var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
|
|
|
3135
3166
|
var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
|
|
3136
3167
|
var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
|
|
3137
3168
|
var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
|
|
3138
|
-
|
|
3139
|
-
|
|
3169
|
+
function noCredentialSentence(version) {
|
|
3170
|
+
return `Halfcycle is not connected: this machine has no working Halfcycle credential for this project. Run "${bareHalfcycleCommand(version)}" in the project folder to sign in again.`;
|
|
3171
|
+
}
|
|
3172
|
+
var NO_CREDENTIAL_SENTENCE = noCredentialSentence();
|
|
3173
|
+
function notThisAccountSentence(version) {
|
|
3174
|
+
return `Halfcycle is not connected: this project belongs to another Halfcycle account. To switch, run "${bareHalfcycleCommand(version)}" in this project and ${SWITCH_ACCOUNT_ANSWER}.`;
|
|
3175
|
+
}
|
|
3176
|
+
var NOT_THIS_ACCOUNT_SENTENCE = notThisAccountSentence();
|
|
3140
3177
|
var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
|
|
3141
3178
|
var NO_NODE_SENTENCE = "Halfcycle is not connected: Node is not installed, or not on the path this session uses, so nothing here is checked. Install Node, then reopen.";
|
|
3142
3179
|
var PLACEHOLDER_SENTENCES = Object.freeze({
|
|
@@ -3572,9 +3609,9 @@ var phaseModelsFileSchema = z26.object({
|
|
|
3572
3609
|
}).strict();
|
|
3573
3610
|
|
|
3574
3611
|
// dist/engagement-credential.js
|
|
3575
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3612
|
+
import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
3576
3613
|
import { platform as platform2 } from "node:os";
|
|
3577
|
-
import { join as
|
|
3614
|
+
import { join as join3 } from "node:path";
|
|
3578
3615
|
|
|
3579
3616
|
// dist/account-credential.js
|
|
3580
3617
|
import { homedir, platform } from "node:os";
|
|
@@ -3583,6 +3620,46 @@ function halfcycleHome(home) {
|
|
|
3583
3620
|
return join(home ?? homedir(), HALFCYCLE_DIR_NAME);
|
|
3584
3621
|
}
|
|
3585
3622
|
|
|
3623
|
+
// dist/own-version.js
|
|
3624
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3625
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
3626
|
+
import { fileURLToPath } from "node:url";
|
|
3627
|
+
var moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
3628
|
+
function findBundleRoot(startDir) {
|
|
3629
|
+
let candidate = startDir;
|
|
3630
|
+
for (let i = 0; i < 4; i++) {
|
|
3631
|
+
const manifest = join2(candidate, ".claude-plugin", "plugin.json");
|
|
3632
|
+
try {
|
|
3633
|
+
if (existsSync(manifest))
|
|
3634
|
+
return candidate;
|
|
3635
|
+
} catch {
|
|
3636
|
+
}
|
|
3637
|
+
candidate = dirname2(candidate);
|
|
3638
|
+
}
|
|
3639
|
+
return join2(startDir, "..");
|
|
3640
|
+
}
|
|
3641
|
+
var BUNDLE_ROOT = findBundleRoot(moduleDir);
|
|
3642
|
+
function bundleVersion() {
|
|
3643
|
+
try {
|
|
3644
|
+
const pkg = JSON.parse(readFileSync(join2(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
3645
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3646
|
+
} catch {
|
|
3647
|
+
return void 0;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
var cachedVersion;
|
|
3651
|
+
function ownVersion() {
|
|
3652
|
+
if (cachedVersion === void 0)
|
|
3653
|
+
cachedVersion = { value: bundleVersion() };
|
|
3654
|
+
return cachedVersion.value;
|
|
3655
|
+
}
|
|
3656
|
+
function ownCommand() {
|
|
3657
|
+
return halfcycleCommand(ownVersion());
|
|
3658
|
+
}
|
|
3659
|
+
function ownBareCommand() {
|
|
3660
|
+
return bareHalfcycleCommand(ownVersion());
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3586
3663
|
// dist/engagement-credential.js
|
|
3587
3664
|
var ENGAGEMENTS_DIR = ENGAGEMENTS_DIR_NAME;
|
|
3588
3665
|
var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
@@ -3590,7 +3667,7 @@ var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
|
3590
3667
|
var InvalidEngagementIdError = class extends Error {
|
|
3591
3668
|
constructor(engagementId) {
|
|
3592
3669
|
const shown = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
|
|
3593
|
-
super(`${shown} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "
|
|
3670
|
+
super(`${shown} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "${ownBareCommand()}" again: this repository is then set up as a new Halfcycle project.`);
|
|
3594
3671
|
this.name = "InvalidEngagementIdError";
|
|
3595
3672
|
}
|
|
3596
3673
|
};
|
|
@@ -3600,10 +3677,10 @@ function assertEngagementId(engagementId) {
|
|
|
3600
3677
|
}
|
|
3601
3678
|
function engagementStateDir(engagementId, home) {
|
|
3602
3679
|
assertEngagementId(engagementId);
|
|
3603
|
-
return
|
|
3680
|
+
return join3(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
3604
3681
|
}
|
|
3605
3682
|
function engagementEnvPath(engagementId, home) {
|
|
3606
|
-
return
|
|
3683
|
+
return join3(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
3607
3684
|
}
|
|
3608
3685
|
var GUARD_ENV_KEYS = [
|
|
3609
3686
|
"GUARD_SERVICE_URL",
|
|
@@ -3625,7 +3702,7 @@ function reconcileEnvText2(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
|
3625
3702
|
}
|
|
3626
3703
|
function readEngagementEnv(engagementId, home) {
|
|
3627
3704
|
try {
|
|
3628
|
-
return parseEnvText(
|
|
3705
|
+
return parseEnvText(readFileSync2(engagementEnvPath(engagementId, home), "utf-8"));
|
|
3629
3706
|
} catch {
|
|
3630
3707
|
return null;
|
|
3631
3708
|
}
|
|
@@ -3635,7 +3712,7 @@ function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KE
|
|
|
3635
3712
|
const dir = engagementStateDir(engagementId, home);
|
|
3636
3713
|
let existing;
|
|
3637
3714
|
try {
|
|
3638
|
-
existing =
|
|
3715
|
+
existing = readFileSync2(path, "utf-8");
|
|
3639
3716
|
} catch {
|
|
3640
3717
|
existing = null;
|
|
3641
3718
|
}
|
|
@@ -3666,7 +3743,7 @@ function engagementResolutionShell() {
|
|
|
3666
3743
|
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
3667
3744
|
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
3668
3745
|
# $HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
3669
|
-
# written owner-only by
|
|
3746
|
+
# written owner-only by \`${bareHalfcycleCommand()}\`. What this repository holds is the
|
|
3670
3747
|
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
3671
3748
|
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
3672
3749
|
# desktop session's PATH, so the id is cut out with sed \u2014 after flattening the
|
|
@@ -3720,8 +3797,8 @@ halfcycle_env_file() {
|
|
|
3720
3797
|
|
|
3721
3798
|
// dist/identity.js
|
|
3722
3799
|
import { execFileSync } from "node:child_process";
|
|
3723
|
-
import { existsSync, linkSync, mkdirSync as mkdirSync2, readFileSync as
|
|
3724
|
-
import { dirname as
|
|
3800
|
+
import { existsSync as existsSync2, linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3801
|
+
import { dirname as dirname3, isAbsolute, join as join4 } from "node:path";
|
|
3725
3802
|
import { randomUUID } from "node:crypto";
|
|
3726
3803
|
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
3727
3804
|
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
@@ -3834,13 +3911,13 @@ function readIgnoredPaths(targetRepoRoot, paths) {
|
|
|
3834
3911
|
const toplevel = git(targetRepoRoot, ["rev-parse", "--show-toplevel"]);
|
|
3835
3912
|
if (toplevel === null)
|
|
3836
3913
|
return null;
|
|
3837
|
-
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source:
|
|
3914
|
+
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join4(toplevel, entry.source) });
|
|
3838
3915
|
}
|
|
3839
3916
|
function mintOrReadIdentity(targetRepoRoot) {
|
|
3840
|
-
const path =
|
|
3841
|
-
if (
|
|
3917
|
+
const path = join4(targetRepoRoot, ".halfcycle", "project.json");
|
|
3918
|
+
if (existsSync2(path)) {
|
|
3842
3919
|
repairRemoteCredential(targetRepoRoot);
|
|
3843
|
-
const onDisk = JSON.parse(
|
|
3920
|
+
const onDisk = JSON.parse(readFileSync3(path, "utf-8"));
|
|
3844
3921
|
const identity = {
|
|
3845
3922
|
projectId: onDisk.projectId ?? "",
|
|
3846
3923
|
rootCommit: onDisk.rootCommit ?? null,
|
|
@@ -3865,10 +3942,10 @@ function repairRemoteCredential(targetRepoRoot) {
|
|
|
3865
3942
|
replaceProjectIdentityFile(targetRepoRoot, repaired);
|
|
3866
3943
|
}
|
|
3867
3944
|
function plannedRemoteRepair(targetRepoRoot) {
|
|
3868
|
-
const path =
|
|
3945
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3869
3946
|
let onDisk;
|
|
3870
3947
|
try {
|
|
3871
|
-
const parsed = JSON.parse(
|
|
3948
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
3872
3949
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
3873
3950
|
return null;
|
|
3874
3951
|
onDisk = parsed;
|
|
@@ -3887,7 +3964,7 @@ function plannedRemoteRepair(targetRepoRoot) {
|
|
|
3887
3964
|
});
|
|
3888
3965
|
}
|
|
3889
3966
|
function replaceProjectIdentityFile(targetRepoRoot, text) {
|
|
3890
|
-
const path =
|
|
3967
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3891
3968
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3892
3969
|
try {
|
|
3893
3970
|
writeFileSync2(temporary, text, { encoding: "utf-8", flag: "wx" });
|
|
@@ -3898,8 +3975,8 @@ function replaceProjectIdentityFile(targetRepoRoot, text) {
|
|
|
3898
3975
|
}
|
|
3899
3976
|
}
|
|
3900
3977
|
function planProjectIdentity(targetRepoRoot) {
|
|
3901
|
-
const path =
|
|
3902
|
-
if (!
|
|
3978
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3979
|
+
if (!existsSync2(path)) {
|
|
3903
3980
|
return { action: "add", after: serializeProjectIdentity(mintIdentity(targetRepoRoot)) };
|
|
3904
3981
|
}
|
|
3905
3982
|
const repaired = plannedRemoteRepair(targetRepoRoot);
|
|
@@ -3907,7 +3984,7 @@ function planProjectIdentity(targetRepoRoot) {
|
|
|
3907
3984
|
return { action: "change", after: repaired };
|
|
3908
3985
|
let current;
|
|
3909
3986
|
try {
|
|
3910
|
-
current =
|
|
3987
|
+
current = readFileSync3(path, "utf-8");
|
|
3911
3988
|
} catch {
|
|
3912
3989
|
current = null;
|
|
3913
3990
|
}
|
|
@@ -3917,8 +3994,8 @@ function serializeProjectIdentity(identity) {
|
|
|
3917
3994
|
return JSON.stringify(identity, null, 2) + "\n";
|
|
3918
3995
|
}
|
|
3919
3996
|
function createProjectIdentityFile(targetRepoRoot, text) {
|
|
3920
|
-
const path =
|
|
3921
|
-
mkdirSync2(
|
|
3997
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3998
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
3922
3999
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3923
4000
|
try {
|
|
3924
4001
|
writeFileSync2(temporary, text ?? serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
@@ -3937,8 +4014,8 @@ function createProjectIdentityFile(targetRepoRoot, text) {
|
|
|
3937
4014
|
}
|
|
3938
4015
|
|
|
3939
4016
|
// dist/install-plan.js
|
|
3940
|
-
import { existsSync as
|
|
3941
|
-
import { join as
|
|
4017
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
4018
|
+
import { join as join6, posix } from "node:path";
|
|
3942
4019
|
|
|
3943
4020
|
// dist/install-manifest.js
|
|
3944
4021
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
@@ -4289,8 +4366,8 @@ function union(a, b) {
|
|
|
4289
4366
|
}
|
|
4290
4367
|
|
|
4291
4368
|
// dist/scan.js
|
|
4292
|
-
import { existsSync as
|
|
4293
|
-
import { join as
|
|
4369
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4370
|
+
import { join as join5 } from "node:path";
|
|
4294
4371
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
4295
4372
|
var HALFCYCLE_STATE_NOTE = [
|
|
4296
4373
|
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
@@ -4313,7 +4390,7 @@ var LAYER_ARTEFACT_PATHS = {
|
|
|
4313
4390
|
function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4314
4391
|
const layers = {};
|
|
4315
4392
|
for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
|
|
4316
|
-
const found = paths.filter((p) =>
|
|
4393
|
+
const found = paths.filter((p) => existsSync3(join5(targetRepo, p)));
|
|
4317
4394
|
if (found.length > 0) {
|
|
4318
4395
|
layers[layer] = {
|
|
4319
4396
|
value: "asserted",
|
|
@@ -4333,17 +4410,17 @@ function runBootstrapScan(targetRepo) {
|
|
|
4333
4410
|
return { state: planned.state, ran: planned.text !== null };
|
|
4334
4411
|
}
|
|
4335
4412
|
function planBootstrapScan(targetRepo) {
|
|
4336
|
-
const statePath =
|
|
4337
|
-
if (
|
|
4338
|
-
const existing = JSON.parse(
|
|
4413
|
+
const statePath = join5(targetRepo, BOOTSTRAP_STATE_REL);
|
|
4414
|
+
if (existsSync3(statePath)) {
|
|
4415
|
+
const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
|
|
4339
4416
|
return { state: existing, text: null };
|
|
4340
4417
|
}
|
|
4341
4418
|
const state = scanLayers(targetRepo);
|
|
4342
4419
|
return { state, text: JSON.stringify(state, null, 2) + "\n" };
|
|
4343
4420
|
}
|
|
4344
4421
|
function writeBootstrapState(targetRepo, text) {
|
|
4345
|
-
mkdirSync3(
|
|
4346
|
-
writeFileSync3(
|
|
4422
|
+
mkdirSync3(join5(targetRepo, ".halfcycle"), { recursive: true });
|
|
4423
|
+
writeFileSync3(join5(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
|
|
4347
4424
|
}
|
|
4348
4425
|
function mergeWithRecords(state, layersWithRealRecords) {
|
|
4349
4426
|
const merged = {};
|
|
@@ -4508,12 +4585,12 @@ function planInstall(options) {
|
|
|
4508
4585
|
throw new Error("[halfcycle] a plan for a project needs its kind as well as its id.");
|
|
4509
4586
|
}
|
|
4510
4587
|
}
|
|
4511
|
-
if (!
|
|
4588
|
+
if (!existsSync4(targetRepo)) {
|
|
4512
4589
|
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
4513
4590
|
}
|
|
4514
4591
|
const writeSet = closedWriteSet();
|
|
4515
4592
|
const previous = readPreviousManifest(targetRepo);
|
|
4516
|
-
const dirsBefore = new Set([...writeSet.dirs].filter((dir) =>
|
|
4593
|
+
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync4(join6(targetRepo, dir))));
|
|
4517
4594
|
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
4518
4595
|
const codexSkills = planCodexSkills(targetRepo, previous, writeSet);
|
|
4519
4596
|
const vendored = planVendoredBinary(targetRepo);
|
|
@@ -4535,7 +4612,7 @@ function planInstall(options) {
|
|
|
4535
4612
|
const gitattributes = planGitattributes(targetRepo);
|
|
4536
4613
|
const identity = planProjectIdentity(targetRepo);
|
|
4537
4614
|
if (identity.after === null)
|
|
4538
|
-
|
|
4615
|
+
readFileSync5(join6(targetRepo, PROJECT_IDENTITY_REL));
|
|
4539
4616
|
const credentialPlan = !preview && credential !== void 0 ? planEngagementCredential(credential, engagementId, home) : null;
|
|
4540
4617
|
const credentialWrite = credentialPlan === null ? null : { outcome: credentialPlan.outcome, existed: credentialPlan.existed, path: engagementEnvPath(engagementId, home) };
|
|
4541
4618
|
const storedAfter = credentialPlan?.storedAfter ?? (preview ? Object.fromEntries(ENGAGEMENT_ENV_KEYS.map((key) => [key, "supplied at sign-in"])) : readEngagementEnv(engagementId, home) ?? {});
|
|
@@ -4668,7 +4745,7 @@ function entriesOf(steps, record2) {
|
|
|
4668
4745
|
}
|
|
4669
4746
|
};
|
|
4670
4747
|
const pending = (rel) => {
|
|
4671
|
-
const there =
|
|
4748
|
+
const there = existsSync4(join6(targetRepo, rel));
|
|
4672
4749
|
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
4673
4750
|
};
|
|
4674
4751
|
steps.commands.forEach(fromFile);
|
|
@@ -4716,7 +4793,7 @@ function entriesOf(steps, record2) {
|
|
|
4716
4793
|
action: "leave",
|
|
4717
4794
|
kind: "codex-server",
|
|
4718
4795
|
bytes: null,
|
|
4719
|
-
...
|
|
4796
|
+
...existsSync4(join6(targetRepo, CODEX_CONFIG_REL2)) ? { yours: true } : {}
|
|
4720
4797
|
});
|
|
4721
4798
|
break;
|
|
4722
4799
|
}
|
|
@@ -4795,10 +4872,10 @@ function applyInstallPlan(plan) {
|
|
|
4795
4872
|
putPlannedCodexHooks(targetRepo, steps.codexHooks);
|
|
4796
4873
|
putPlannedCodexConfig(targetRepo, steps.codexConfig);
|
|
4797
4874
|
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
4798
|
-
const gitignorePath =
|
|
4799
|
-
const gitignoreAfter =
|
|
4875
|
+
const gitignorePath = join6(targetRepo, GITIGNORE_REL);
|
|
4876
|
+
const gitignoreAfter = existsSync4(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
|
|
4800
4877
|
const gitattributes = putPlannedGitattributes(targetRepo, steps.gitattributes);
|
|
4801
|
-
const gitattributesAfter = readTextOrNull(
|
|
4878
|
+
const gitattributesAfter = readTextOrNull(join6(targetRepo, GITATTRIBUTES_REL));
|
|
4802
4879
|
writeProjectIdentity(targetRepo, steps.identity);
|
|
4803
4880
|
const credential = steps.credential;
|
|
4804
4881
|
const credentialOutcome = steps.credentialWrite !== null && credential !== void 0 ? writeEngagementCredential(credential, engagementId, steps.home) : null;
|
|
@@ -4819,9 +4896,9 @@ function applyInstallPlan(plan) {
|
|
|
4819
4896
|
gitattributesAfter,
|
|
4820
4897
|
credential: credentialOutcome,
|
|
4821
4898
|
legacy,
|
|
4822
|
-
bytesAfter: (rel) =>
|
|
4823
|
-
dirExistsAfter: (dir) =>
|
|
4824
|
-
stateExistsAfter:
|
|
4899
|
+
bytesAfter: (rel) => readFileSync5(join6(targetRepo, rel)),
|
|
4900
|
+
dirExistsAfter: (dir) => existsSync4(join6(targetRepo, dir)),
|
|
4901
|
+
stateExistsAfter: existsSync4(join6(targetRepo, BOOTSTRAP_STATE_REL))
|
|
4825
4902
|
});
|
|
4826
4903
|
const record2 = planInstallRecord(targetRepo, manifestText);
|
|
4827
4904
|
putPlannedFile(targetRepo, record2);
|
|
@@ -5172,8 +5249,8 @@ function crewProfileRel(agent, callsign) {
|
|
|
5172
5249
|
}
|
|
5173
5250
|
function crewProfileTarget(targetRepo, rel) {
|
|
5174
5251
|
const folder = rel.startsWith(`${CLAUDE_AGENTS_DIR}/`) ? CLAUDE_AGENTS_DIR : CODEX_AGENTS_DIR;
|
|
5175
|
-
const target = resolve(
|
|
5176
|
-
if (
|
|
5252
|
+
const target = resolve(join7(targetRepo, rel));
|
|
5253
|
+
if (dirname4(target) !== resolve(targetRepo, folder)) {
|
|
5177
5254
|
throw new Error(`[halfcycle] crew profile "${rel}" does not sit directly in ${folder}.`);
|
|
5178
5255
|
}
|
|
5179
5256
|
return target;
|
|
@@ -5256,41 +5333,25 @@ function renderCrewProfiles(roster = CREW_ROSTER) {
|
|
|
5256
5333
|
])
|
|
5257
5334
|
];
|
|
5258
5335
|
}
|
|
5259
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
5260
|
-
var __dirname = dirname3(__filename);
|
|
5261
|
-
function findBundleRoot(startDir) {
|
|
5262
|
-
let candidate = startDir;
|
|
5263
|
-
for (let i = 0; i < 4; i++) {
|
|
5264
|
-
const manifest = join6(candidate, ".claude-plugin", "plugin.json");
|
|
5265
|
-
try {
|
|
5266
|
-
if (existsSync4(manifest))
|
|
5267
|
-
return candidate;
|
|
5268
|
-
} catch {
|
|
5269
|
-
}
|
|
5270
|
-
candidate = dirname3(candidate);
|
|
5271
|
-
}
|
|
5272
|
-
return join6(startDir, "..");
|
|
5273
|
-
}
|
|
5274
|
-
var BUNDLE_ROOT = findBundleRoot(__dirname);
|
|
5275
5336
|
function resolveVendoredBinary() {
|
|
5276
5337
|
const candidates = [
|
|
5277
|
-
|
|
5278
|
-
|
|
5338
|
+
join7(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
|
|
5339
|
+
join7(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
|
|
5279
5340
|
];
|
|
5280
5341
|
for (const candidate of candidates) {
|
|
5281
|
-
if (
|
|
5342
|
+
if (existsSync5(candidate))
|
|
5282
5343
|
return candidate;
|
|
5283
5344
|
}
|
|
5284
5345
|
throw new Error(`[halfcycle] Cannot find the self-contained guard binary bin.bundle.mjs. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
5285
5346
|
}
|
|
5286
5347
|
function resolvePreFilterShell() {
|
|
5287
5348
|
const candidates = [
|
|
5288
|
-
|
|
5289
|
-
|
|
5349
|
+
join7(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
|
|
5350
|
+
join7(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
|
|
5290
5351
|
];
|
|
5291
5352
|
for (const candidate of candidates) {
|
|
5292
|
-
if (
|
|
5293
|
-
return
|
|
5353
|
+
if (existsSync5(candidate))
|
|
5354
|
+
return readFileSync6(candidate, "utf-8");
|
|
5294
5355
|
}
|
|
5295
5356
|
throw new Error(`[halfcycle] Cannot find ${PRE_FILTER_SHELL_FILENAME}. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
5296
5357
|
}
|
|
@@ -5329,7 +5390,7 @@ function isAllowlisted(targetRelPath) {
|
|
|
5329
5390
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
5330
5391
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
5331
5392
|
function capturedIndexScaffold() {
|
|
5332
|
-
return
|
|
5393
|
+
return join7(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
|
|
5333
5394
|
}
|
|
5334
5395
|
var GITIGNORE_REL = ".gitignore";
|
|
5335
5396
|
var GITATTRIBUTES_REL = ".gitattributes";
|
|
@@ -5404,14 +5465,15 @@ function closedWriteSet() {
|
|
|
5404
5465
|
function sha256OfWhatThisVersionWrites(commands) {
|
|
5405
5466
|
const out = /* @__PURE__ */ new Map();
|
|
5406
5467
|
const fromFile = (rel, src) => {
|
|
5407
|
-
if (
|
|
5408
|
-
out.set(rel, fileSha256(
|
|
5468
|
+
if (existsSync5(src))
|
|
5469
|
+
out.set(rel, fileSha256(readFileSync6(src)));
|
|
5409
5470
|
};
|
|
5410
5471
|
for (const cmd of commands) {
|
|
5411
|
-
const srcFile =
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
out.set(
|
|
5472
|
+
const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
5473
|
+
if (existsSync5(srcFile)) {
|
|
5474
|
+
const text = commandFileText(readFileSync6(srcFile, "utf-8"), ownVersion());
|
|
5475
|
+
out.set(commandStubRel(cmd.name), fileSha256(Buffer.from(text, "utf-8")));
|
|
5476
|
+
out.set(skillRel(cmd.name), fileSha256(Buffer.from(codexSkillContent(cmd.name, text), "utf-8")));
|
|
5415
5477
|
}
|
|
5416
5478
|
}
|
|
5417
5479
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
@@ -5428,7 +5490,7 @@ function bytesAreOurs(path, bytes, recorded, writeSet) {
|
|
|
5428
5490
|
}
|
|
5429
5491
|
function readPreviousManifest(targetRepo) {
|
|
5430
5492
|
try {
|
|
5431
|
-
return parseManifest(
|
|
5493
|
+
return parseManifest(readFileSync6(join7(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
|
|
5432
5494
|
} catch {
|
|
5433
5495
|
return null;
|
|
5434
5496
|
}
|
|
@@ -5532,8 +5594,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
5532
5594
|
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
5533
5595
|
let plain = dirVerdicts.get(dir);
|
|
5534
5596
|
if (plain === void 0) {
|
|
5535
|
-
const abs =
|
|
5536
|
-
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(
|
|
5597
|
+
const abs = join7(targetRepo, dir);
|
|
5598
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join7(abs, ".git")) === null;
|
|
5537
5599
|
dirVerdicts.set(dir, plain);
|
|
5538
5600
|
}
|
|
5539
5601
|
if (!plain)
|
|
@@ -5547,7 +5609,7 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
5547
5609
|
...merged,
|
|
5548
5610
|
INSTALL_MANIFEST_REL
|
|
5549
5611
|
])
|
|
5550
|
-
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(
|
|
5612
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join7(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
5551
5613
|
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
5552
5614
|
if (ignored === null)
|
|
5553
5615
|
return null;
|
|
@@ -5570,7 +5632,7 @@ function lstatOrNull(path) {
|
|
|
5570
5632
|
}
|
|
5571
5633
|
function readFileOrNull(path) {
|
|
5572
5634
|
try {
|
|
5573
|
-
return
|
|
5635
|
+
return readFileSync6(path, "utf-8");
|
|
5574
5636
|
} catch {
|
|
5575
5637
|
return null;
|
|
5576
5638
|
}
|
|
@@ -5579,8 +5641,8 @@ function isPlainObject(value) {
|
|
|
5579
5641
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5580
5642
|
}
|
|
5581
5643
|
function readPluginManifest() {
|
|
5582
|
-
const manifestPath =
|
|
5583
|
-
const raw =
|
|
5644
|
+
const manifestPath = join7(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
|
|
5645
|
+
const raw = readFileSync6(manifestPath, "utf-8");
|
|
5584
5646
|
return JSON.parse(raw);
|
|
5585
5647
|
}
|
|
5586
5648
|
function parseCommandStub(raw, name) {
|
|
@@ -5623,6 +5685,9 @@ function codexSkillBody(name, sourceBody) {
|
|
|
5623
5685
|
}
|
|
5624
5686
|
return transformed;
|
|
5625
5687
|
}
|
|
5688
|
+
function commandFileText(source, version) {
|
|
5689
|
+
return rewriteHalfcycleCommands(source, releaseTagOfVersion(version));
|
|
5690
|
+
}
|
|
5626
5691
|
function codexSkillContent(name, sourceRaw) {
|
|
5627
5692
|
const { description, body } = parseCommandStub(sourceRaw, name);
|
|
5628
5693
|
const transformed = codexSkillBody(name, body);
|
|
@@ -5635,14 +5700,14 @@ ${transformed}`;
|
|
|
5635
5700
|
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
5636
5701
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
5637
5702
|
return (manifest.commands ?? []).map((cmd) => {
|
|
5638
|
-
const srcFile =
|
|
5639
|
-
if (!
|
|
5703
|
+
const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
5704
|
+
if (!existsSync5(srcFile)) {
|
|
5640
5705
|
throw new Error(`[halfcycle] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
|
|
5641
5706
|
}
|
|
5642
5707
|
const rel = commandStubRel(cmd.name);
|
|
5643
|
-
const content =
|
|
5708
|
+
const content = commandFileText(readFileSync6(srcFile, "utf-8"), ownVersion());
|
|
5644
5709
|
const recorded = recordedFiles.get(rel);
|
|
5645
|
-
const planned = decideCollisionSafe(
|
|
5710
|
+
const planned = decideCollisionSafe(join7(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
5646
5711
|
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
5647
5712
|
});
|
|
5648
5713
|
}
|
|
@@ -5652,8 +5717,8 @@ function putPlannedFile(targetRepoRoot, planned) {
|
|
|
5652
5717
|
if (!isAllowlisted(planned.rel)) {
|
|
5653
5718
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${planned.rel}". Only the method surface and scaffolding paths are writable.`);
|
|
5654
5719
|
}
|
|
5655
|
-
const target =
|
|
5656
|
-
mkdirSync4(
|
|
5720
|
+
const target = join7(targetRepoRoot, planned.rel);
|
|
5721
|
+
mkdirSync4(dirname4(target), { recursive: true });
|
|
5657
5722
|
writeFileSync4(target, planned.text, "utf-8");
|
|
5658
5723
|
}
|
|
5659
5724
|
function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
|
|
@@ -5663,7 +5728,7 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
|
|
|
5663
5728
|
}
|
|
5664
5729
|
if (readFileOrNull(targetAbsPath) === content)
|
|
5665
5730
|
return;
|
|
5666
|
-
mkdirSync4(
|
|
5731
|
+
mkdirSync4(dirname4(targetAbsPath), { recursive: true });
|
|
5667
5732
|
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
5668
5733
|
writtenPaths.push(rel);
|
|
5669
5734
|
}
|
|
@@ -5672,7 +5737,7 @@ function decideCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
|
|
|
5672
5737
|
if (!isAllowlisted(rel)) {
|
|
5673
5738
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
5674
5739
|
}
|
|
5675
|
-
const current =
|
|
5740
|
+
const current = existsSync5(targetAbsPath) ? readFileSync6(targetAbsPath) : null;
|
|
5676
5741
|
const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
|
|
5677
5742
|
if (current === null)
|
|
5678
5743
|
return { rel, text, outcome: "written", replacedExisting: false };
|
|
@@ -5695,7 +5760,7 @@ function decideOwned(targetAbsPath, targetRepoRoot, content) {
|
|
|
5695
5760
|
const text = inLineEndingOf(current, content);
|
|
5696
5761
|
if (current === text)
|
|
5697
5762
|
return { rel, text, outcome: "skipped", replacedExisting: false };
|
|
5698
|
-
return { rel, text, outcome: "written", replacedExisting:
|
|
5763
|
+
return { rel, text, outcome: "written", replacedExisting: existsSync5(targetAbsPath) };
|
|
5699
5764
|
}
|
|
5700
5765
|
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
5701
5766
|
function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
@@ -5703,9 +5768,9 @@ function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedM
|
|
|
5703
5768
|
if (!isAllowlisted(rel)) {
|
|
5704
5769
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
5705
5770
|
}
|
|
5706
|
-
if (!
|
|
5771
|
+
if (!existsSync5(targetAbsPath))
|
|
5707
5772
|
return { rel, text: content, outcome: "written", replacedExisting: false };
|
|
5708
|
-
const current =
|
|
5773
|
+
const current = readFileSync6(targetAbsPath, "utf-8");
|
|
5709
5774
|
if (current === content)
|
|
5710
5775
|
return { rel, text: content, outcome: "skipped", replacedExisting: false };
|
|
5711
5776
|
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
@@ -5735,8 +5800,8 @@ var DENY_PATTERNS = [
|
|
|
5735
5800
|
];
|
|
5736
5801
|
var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
|
|
5737
5802
|
function readExistingSettings(targetRepo) {
|
|
5738
|
-
const settingsPath =
|
|
5739
|
-
return
|
|
5803
|
+
const settingsPath = join7(targetRepo, SETTINGS_REL);
|
|
5804
|
+
return existsSync5(settingsPath) ? JSON.parse(readFileSync6(settingsPath, "utf-8")) : void 0;
|
|
5740
5805
|
}
|
|
5741
5806
|
var SettingsParseRefused = class extends Error {
|
|
5742
5807
|
constructor(message) {
|
|
@@ -5745,14 +5810,14 @@ var SettingsParseRefused = class extends Error {
|
|
|
5745
5810
|
}
|
|
5746
5811
|
};
|
|
5747
5812
|
function settingsParseRefusal(targetRepo, err) {
|
|
5748
|
-
const file =
|
|
5813
|
+
const file = join7(targetRepo, SETTINGS_REL);
|
|
5749
5814
|
const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
|
|
5750
5815
|
let where = "";
|
|
5751
5816
|
if (offset !== void 0) {
|
|
5752
|
-
const before =
|
|
5817
|
+
const before = readFileSync6(file, "utf-8").slice(0, Number(offset)).split("\n");
|
|
5753
5818
|
where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
|
|
5754
5819
|
}
|
|
5755
|
-
return `${file} could not be read as JSON${where} \u2014 fix it and run "
|
|
5820
|
+
return `${file} could not be read as JSON${where} \u2014 fix it and run "${ownBareCommand()}" again.`;
|
|
5756
5821
|
}
|
|
5757
5822
|
var SETTINGS_SCHEMA = "https://json.schemastore.org/claude-code-settings.json";
|
|
5758
5823
|
var HALFCYCLE_STATUS_LINE = Object.freeze({
|
|
@@ -5760,18 +5825,18 @@ var HALFCYCLE_STATUS_LINE = Object.freeze({
|
|
|
5760
5825
|
command: 'bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard-runner.sh statusline'
|
|
5761
5826
|
});
|
|
5762
5827
|
function statusLineSetElsewhere(targetRepo, home = homedir2(), env = process.env) {
|
|
5763
|
-
const places = [
|
|
5828
|
+
const places = [join7(targetRepo, ".claude", "settings.local.json"), join7(home, ".claude", "settings.json")];
|
|
5764
5829
|
const configDir = env["CLAUDE_CONFIG_DIR"];
|
|
5765
5830
|
if (configDir !== void 0 && configDir !== "") {
|
|
5766
|
-
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ?
|
|
5767
|
-
places.push(
|
|
5831
|
+
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join7(home, configDir.slice(2)) : configDir;
|
|
5832
|
+
places.push(join7(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
|
|
5768
5833
|
}
|
|
5769
5834
|
return places.some((path) => {
|
|
5770
|
-
if (!
|
|
5835
|
+
if (!existsSync5(path))
|
|
5771
5836
|
return false;
|
|
5772
5837
|
let value;
|
|
5773
5838
|
try {
|
|
5774
|
-
value = JSON.parse(
|
|
5839
|
+
value = JSON.parse(readFileSync6(path, "utf-8"));
|
|
5775
5840
|
} catch {
|
|
5776
5841
|
return true;
|
|
5777
5842
|
}
|
|
@@ -6238,12 +6303,12 @@ function isTrackedAndClean(targetRepo, relPath) {
|
|
|
6238
6303
|
return true;
|
|
6239
6304
|
}
|
|
6240
6305
|
function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
6241
|
-
const path =
|
|
6242
|
-
if (!
|
|
6306
|
+
const path = join7(targetRepo, rel);
|
|
6307
|
+
if (!existsSync5(path))
|
|
6243
6308
|
return "absent";
|
|
6244
6309
|
let raw;
|
|
6245
6310
|
try {
|
|
6246
|
-
raw =
|
|
6311
|
+
raw = readFileSync6(path, "utf-8");
|
|
6247
6312
|
} catch {
|
|
6248
6313
|
return "failed";
|
|
6249
6314
|
}
|
|
@@ -6257,7 +6322,7 @@ function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashe
|
|
|
6257
6322
|
}
|
|
6258
6323
|
function removePlannedRetiredFile(targetRepo, rel) {
|
|
6259
6324
|
try {
|
|
6260
|
-
rmSync2(
|
|
6325
|
+
rmSync2(join7(targetRepo, rel));
|
|
6261
6326
|
} catch {
|
|
6262
6327
|
return "failed";
|
|
6263
6328
|
}
|
|
@@ -6278,7 +6343,7 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
|
6278
6343
|
`const u=p.dirname(d);`,
|
|
6279
6344
|
`if(u===d)break;`,
|
|
6280
6345
|
`d=u}`,
|
|
6281
|
-
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run
|
|
6346
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run ${ownBareCommand()} in the project folder');`,
|
|
6282
6347
|
`process.exit(1)`
|
|
6283
6348
|
].join(""))}"`;
|
|
6284
6349
|
var CODEX_ADDRESS_PATTERN = /^https?:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._-]+)*\/mcp$/;
|
|
@@ -6302,7 +6367,7 @@ function codexMcpHeadersHelperCommand(address) {
|
|
|
6302
6367
|
`if(u===d)break;`,
|
|
6303
6368
|
`d=u}`,
|
|
6304
6369
|
`if(b===''){`,
|
|
6305
|
-
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run
|
|
6370
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run ${ownBareCommand()} in the project folder');`,
|
|
6306
6371
|
`n()}else{`,
|
|
6307
6372
|
`const r=c.spawnSync(process.execPath,[b,'mcp-headers','--url','${address}'],{stdio:['ignore','pipe','inherit']});`,
|
|
6308
6373
|
`if(r.status===0&&r.stdout&&r.stdout.length){o=1;process.stdout.write(r.stdout)}else{n()}}`,
|
|
@@ -6628,9 +6693,9 @@ function gitignoreCovers(content) {
|
|
|
6628
6693
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
6629
6694
|
}
|
|
6630
6695
|
function planGitignore(targetRepo) {
|
|
6631
|
-
const gitignorePath =
|
|
6696
|
+
const gitignorePath = join7(targetRepo, GITIGNORE_REL);
|
|
6632
6697
|
const header = GITIGNORE_HEADER;
|
|
6633
|
-
const before =
|
|
6698
|
+
const before = existsSync5(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
|
|
6634
6699
|
if (before === null) {
|
|
6635
6700
|
return { before, after: [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n", outcome: "written" };
|
|
6636
6701
|
}
|
|
@@ -6646,7 +6711,7 @@ function putPlannedGitignore(targetRepo, planned) {
|
|
|
6646
6711
|
if (planned.outcome !== "written" || planned.after === null)
|
|
6647
6712
|
return planned.outcome;
|
|
6648
6713
|
try {
|
|
6649
|
-
writeFileSync4(
|
|
6714
|
+
writeFileSync4(join7(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
|
|
6650
6715
|
return "written";
|
|
6651
6716
|
} catch {
|
|
6652
6717
|
return "failed";
|
|
@@ -6655,7 +6720,7 @@ function putPlannedGitignore(targetRepo, planned) {
|
|
|
6655
6720
|
var GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
|
|
6656
6721
|
function readTextOrNull(path) {
|
|
6657
6722
|
try {
|
|
6658
|
-
return
|
|
6723
|
+
return readFileSync6(path, "utf-8");
|
|
6659
6724
|
} catch {
|
|
6660
6725
|
return null;
|
|
6661
6726
|
}
|
|
@@ -6665,12 +6730,12 @@ function lfRules(paths) {
|
|
|
6665
6730
|
}
|
|
6666
6731
|
var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
|
|
6667
6732
|
function planGitattributes(targetRepo) {
|
|
6668
|
-
const path =
|
|
6733
|
+
const path = join7(targetRepo, GITATTRIBUTES_REL);
|
|
6669
6734
|
const rules = REQUIRED_GITATTRIBUTES_RULES;
|
|
6670
6735
|
const before = readTextOrNull(path);
|
|
6671
6736
|
if (lstatOrNull(path)?.isSymbolicLink() === true)
|
|
6672
6737
|
return { before, after: before, outcome: "failed" };
|
|
6673
|
-
if (!
|
|
6738
|
+
if (!existsSync5(path)) {
|
|
6674
6739
|
return { before, after: [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", outcome: "written" };
|
|
6675
6740
|
}
|
|
6676
6741
|
if (before === null)
|
|
@@ -6688,7 +6753,7 @@ function putPlannedGitattributes(targetRepo, planned) {
|
|
|
6688
6753
|
if (planned.outcome !== "written" || planned.after === null)
|
|
6689
6754
|
return planned.outcome;
|
|
6690
6755
|
try {
|
|
6691
|
-
writeAllowlisted(
|
|
6756
|
+
writeAllowlisted(join7(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
|
|
6692
6757
|
return "written";
|
|
6693
6758
|
} catch {
|
|
6694
6759
|
return "failed";
|
|
@@ -6722,7 +6787,7 @@ function planBundlePin(targetRepoRoot, version, engagementId, engagementType, cr
|
|
|
6722
6787
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
6723
6788
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
6724
6789
|
};
|
|
6725
|
-
const current = readFileOrNull(
|
|
6790
|
+
const current = readFileOrNull(join7(targetRepoRoot, BUNDLE_PIN_REL));
|
|
6726
6791
|
const text = inLineEndingOf(current, JSON.stringify(pin, null, 2) + "\n");
|
|
6727
6792
|
return {
|
|
6728
6793
|
rel: BUNDLE_PIN_REL,
|
|
@@ -6753,7 +6818,7 @@ function planBundlePinPreview(targetRepoRoot, version, home) {
|
|
|
6753
6818
|
return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0);
|
|
6754
6819
|
}
|
|
6755
6820
|
function writeBundlePin(targetRepoRoot, planned, writtenPaths) {
|
|
6756
|
-
const pinPath =
|
|
6821
|
+
const pinPath = join7(targetRepoRoot, BUNDLE_PIN_REL);
|
|
6757
6822
|
writeAllowlisted(pinPath, targetRepoRoot, planned.text, writtenPaths);
|
|
6758
6823
|
}
|
|
6759
6824
|
function renderCrewRoster() {
|
|
@@ -6768,10 +6833,10 @@ function pinCreatedByAccountId(pin) {
|
|
|
6768
6833
|
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
6769
6834
|
}
|
|
6770
6835
|
function readBundlePin(targetRepoRoot) {
|
|
6771
|
-
const pinPath =
|
|
6772
|
-
if (!
|
|
6836
|
+
const pinPath = join7(targetRepoRoot, BUNDLE_PIN_REL);
|
|
6837
|
+
if (!existsSync5(pinPath))
|
|
6773
6838
|
return null;
|
|
6774
|
-
return JSON.parse(
|
|
6839
|
+
return JSON.parse(readFileSync6(pinPath, "utf-8"));
|
|
6775
6840
|
}
|
|
6776
6841
|
function readPinnedEngagement(targetRepoRoot, home) {
|
|
6777
6842
|
const pin = readBundlePin(targetRepoRoot);
|
|
@@ -6794,7 +6859,7 @@ function readPinnedEngagement(targetRepoRoot, home) {
|
|
|
6794
6859
|
}
|
|
6795
6860
|
function readLegacyEnvLocal(targetRepoRoot) {
|
|
6796
6861
|
try {
|
|
6797
|
-
return parseEnvText(
|
|
6862
|
+
return parseEnvText(readFileSync6(join7(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
|
|
6798
6863
|
} catch {
|
|
6799
6864
|
return null;
|
|
6800
6865
|
}
|
|
@@ -6843,7 +6908,7 @@ function writeEngagementCredential(credential, engagementId, home) {
|
|
|
6843
6908
|
function planEngagementCredential(credential, engagementId, home) {
|
|
6844
6909
|
let existing;
|
|
6845
6910
|
try {
|
|
6846
|
-
existing =
|
|
6911
|
+
existing = readFileSync6(engagementEnvPath(engagementId, home), "utf-8");
|
|
6847
6912
|
} catch {
|
|
6848
6913
|
existing = null;
|
|
6849
6914
|
}
|
|
@@ -6886,12 +6951,12 @@ function engagementCredentialValues(credential, engagementId) {
|
|
|
6886
6951
|
}
|
|
6887
6952
|
var LEGACY_ENV_LOCAL_REL = ".env.local";
|
|
6888
6953
|
function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
6889
|
-
const path =
|
|
6890
|
-
if (!
|
|
6954
|
+
const path = join7(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
6955
|
+
if (!existsSync5(path))
|
|
6891
6956
|
return { outcome: "absent", unbacked: [], missingForAdoption: [] };
|
|
6892
6957
|
let raw;
|
|
6893
6958
|
try {
|
|
6894
|
-
raw =
|
|
6959
|
+
raw = readFileSync6(path, "utf-8");
|
|
6895
6960
|
} catch {
|
|
6896
6961
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
6897
6962
|
}
|
|
@@ -6917,7 +6982,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
6917
6982
|
return { outcome: "stripped", unbacked: [], missingForAdoption: [], keptText: kept.join("\n") };
|
|
6918
6983
|
}
|
|
6919
6984
|
function applyLegacyMigration(targetRepo, decided) {
|
|
6920
|
-
const path =
|
|
6985
|
+
const path = join7(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
6921
6986
|
try {
|
|
6922
6987
|
if (decided.outcome === "removed") {
|
|
6923
6988
|
rmSync2(path);
|
|
@@ -6943,23 +7008,23 @@ function planCodexSkills(targetRepo, previous, writeSet) {
|
|
|
6943
7008
|
const manifest = readPluginManifest();
|
|
6944
7009
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6945
7010
|
return (manifest.commands ?? []).map((cmd) => {
|
|
6946
|
-
const srcFile =
|
|
6947
|
-
if (!
|
|
7011
|
+
const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
7012
|
+
if (!existsSync5(srcFile)) {
|
|
6948
7013
|
throw new Error(`[halfcycle] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
|
|
6949
7014
|
}
|
|
6950
7015
|
const rel = skillRel(cmd.name);
|
|
6951
|
-
const content = codexSkillContent(cmd.name,
|
|
7016
|
+
const content = codexSkillContent(cmd.name, commandFileText(readFileSync6(srcFile, "utf-8"), ownVersion()));
|
|
6952
7017
|
const recorded = recordedFiles.get(rel);
|
|
6953
|
-
const planned = decideCollisionSafe(
|
|
7018
|
+
const planned = decideCollisionSafe(join7(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
6954
7019
|
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
6955
7020
|
});
|
|
6956
7021
|
}
|
|
6957
7022
|
function planVendoredBinary(targetRepo) {
|
|
6958
|
-
return decideOwned(
|
|
7023
|
+
return decideOwned(join7(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
|
|
6959
7024
|
}
|
|
6960
7025
|
function planSettings(targetRepo, previous, home, env) {
|
|
6961
|
-
const settingsPath =
|
|
6962
|
-
const preexisted =
|
|
7026
|
+
const settingsPath = join7(targetRepo, SETTINGS_REL);
|
|
7027
|
+
const preexisted = existsSync5(settingsPath);
|
|
6963
7028
|
const generated = JSON.parse(generateSettingsJson());
|
|
6964
7029
|
let existing;
|
|
6965
7030
|
try {
|
|
@@ -6983,11 +7048,11 @@ function planSettings(targetRepo, previous, home, env) {
|
|
|
6983
7048
|
return { file, preexisted, existing, generated, merged, retiredOurs };
|
|
6984
7049
|
}
|
|
6985
7050
|
function planHookScripts(targetRepo) {
|
|
6986
|
-
return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(
|
|
7051
|
+
return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join7(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
|
|
6987
7052
|
}
|
|
6988
7053
|
function planMcpRegistration(targetRepo, credential) {
|
|
6989
|
-
const mcpPath =
|
|
6990
|
-
const existing =
|
|
7054
|
+
const mcpPath = join7(targetRepo, MCP_REGISTRATION_REL);
|
|
7055
|
+
const existing = existsSync5(mcpPath) ? readFileSync6(mcpPath, "utf-8") : null;
|
|
6991
7056
|
if (credential === void 0)
|
|
6992
7057
|
return { kind: "none", existing };
|
|
6993
7058
|
if (credential === "pending")
|
|
@@ -6999,11 +7064,11 @@ function planMcpRegistration(targetRepo, credential) {
|
|
|
6999
7064
|
function putPlannedMcp(targetRepo, planned) {
|
|
7000
7065
|
if (planned.kind !== "write" || planned.outcome === "skipped")
|
|
7001
7066
|
return;
|
|
7002
|
-
writeAllowlisted(
|
|
7067
|
+
writeAllowlisted(join7(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
7003
7068
|
}
|
|
7004
7069
|
function fileInFolderPlace(root, rel) {
|
|
7005
7070
|
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
7006
|
-
const found = lstatOrNull(
|
|
7071
|
+
const found = lstatOrNull(join7(root, dir));
|
|
7007
7072
|
if (found !== null && !found.isDirectory())
|
|
7008
7073
|
return true;
|
|
7009
7074
|
}
|
|
@@ -7012,7 +7077,7 @@ function fileInFolderPlace(root, rel) {
|
|
|
7012
7077
|
function symlinkOnPath(root, rel) {
|
|
7013
7078
|
let current = root;
|
|
7014
7079
|
for (const segment of rel.split("/")) {
|
|
7015
|
-
current =
|
|
7080
|
+
current = join7(current, segment);
|
|
7016
7081
|
try {
|
|
7017
7082
|
if (lstatSync(current).isSymbolicLink())
|
|
7018
7083
|
return true;
|
|
@@ -7033,14 +7098,14 @@ function planCodexHooks(targetRepo) {
|
|
|
7033
7098
|
});
|
|
7034
7099
|
if (symlinkOnPath(targetRepo, CODEX_HOOKS_REL2))
|
|
7035
7100
|
return leave("link", null);
|
|
7036
|
-
const folder = lstatOrNull(
|
|
7101
|
+
const folder = lstatOrNull(join7(targetRepo, posix2.dirname(CODEX_HOOKS_REL2)));
|
|
7037
7102
|
if (folder !== null && !folder.isDirectory())
|
|
7038
7103
|
return leave("unreadable", null);
|
|
7039
|
-
const path =
|
|
7104
|
+
const path = join7(targetRepo, CODEX_HOOKS_REL2);
|
|
7040
7105
|
let before = null;
|
|
7041
7106
|
if (lstatOrNull(path) !== null) {
|
|
7042
7107
|
try {
|
|
7043
|
-
before =
|
|
7108
|
+
before = readFileSync6(path, "utf-8");
|
|
7044
7109
|
} catch {
|
|
7045
7110
|
return leave("unreadable", null);
|
|
7046
7111
|
}
|
|
@@ -7074,13 +7139,13 @@ function planCodexHooks(targetRepo) {
|
|
|
7074
7139
|
function putPlannedCodexHooks(targetRepo, planned) {
|
|
7075
7140
|
if (planned.outcome !== "written" || planned.text === null)
|
|
7076
7141
|
return;
|
|
7077
|
-
writeAllowlisted(
|
|
7142
|
+
writeAllowlisted(join7(targetRepo, CODEX_HOOKS_REL2), targetRepo, planned.text, []);
|
|
7078
7143
|
}
|
|
7079
7144
|
function readCodexConfig(targetRepo) {
|
|
7080
|
-
const folder = lstatOrNull(
|
|
7145
|
+
const folder = lstatOrNull(join7(targetRepo, posix2.dirname(CODEX_CONFIG_REL2)));
|
|
7081
7146
|
if (folder !== null && !folder.isDirectory())
|
|
7082
7147
|
return "unreadable";
|
|
7083
|
-
const path =
|
|
7148
|
+
const path = join7(targetRepo, CODEX_CONFIG_REL2);
|
|
7084
7149
|
const stat = lstatOrNull(path);
|
|
7085
7150
|
if (stat === null)
|
|
7086
7151
|
return { text: null };
|
|
@@ -7088,7 +7153,7 @@ function readCodexConfig(targetRepo) {
|
|
|
7088
7153
|
return "unreadable";
|
|
7089
7154
|
let bytes;
|
|
7090
7155
|
try {
|
|
7091
|
-
bytes =
|
|
7156
|
+
bytes = readFileSync6(path);
|
|
7092
7157
|
} catch {
|
|
7093
7158
|
return "unreadable";
|
|
7094
7159
|
}
|
|
@@ -7134,10 +7199,10 @@ function planCodexConfig(targetRepo, credential) {
|
|
|
7134
7199
|
function putPlannedCodexConfig(targetRepo, planned) {
|
|
7135
7200
|
if (planned.outcome !== "written" || planned.text === null)
|
|
7136
7201
|
return;
|
|
7137
|
-
writeAllowlisted(
|
|
7202
|
+
writeAllowlisted(join7(targetRepo, CODEX_CONFIG_REL2), targetRepo, planned.text, []);
|
|
7138
7203
|
}
|
|
7139
7204
|
function planCrewRoster(targetRepo) {
|
|
7140
|
-
return decideOwned(
|
|
7205
|
+
return decideOwned(join7(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
7141
7206
|
}
|
|
7142
7207
|
function planCrewProfiles(targetRepo, previous, writeSet) {
|
|
7143
7208
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
@@ -7152,7 +7217,7 @@ function planCrewProfiles(targetRepo, previous, writeSet) {
|
|
|
7152
7217
|
});
|
|
7153
7218
|
}
|
|
7154
7219
|
function planInstallRecord(targetRepo, text) {
|
|
7155
|
-
return decideOwned(
|
|
7220
|
+
return decideOwned(join7(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
7156
7221
|
}
|
|
7157
7222
|
function accountInstall(steps, actual) {
|
|
7158
7223
|
const { targetRepo, writeSet, pin } = steps;
|
|
@@ -7526,8 +7591,8 @@ async function requestEngagementValues(method, url, requestBody, credential, sha
|
|
|
7526
7591
|
}
|
|
7527
7592
|
|
|
7528
7593
|
// dist/setup/manifest.js
|
|
7529
|
-
import { existsSync as
|
|
7530
|
-
import { dirname as
|
|
7594
|
+
import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync7, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7595
|
+
import { dirname as dirname5, join as join8, posix as posix3 } from "node:path";
|
|
7531
7596
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
7532
7597
|
var DECLARED_DIR_PREFIX = "docs";
|
|
7533
7598
|
var RESERVED_METHOD_PATH = "docs/method";
|
|
@@ -7612,7 +7677,7 @@ function firstSymlinkOnPath(targetRepo, path) {
|
|
|
7612
7677
|
const segments = path.split("/").filter((s) => s.length > 0);
|
|
7613
7678
|
let walked = targetRepo;
|
|
7614
7679
|
for (let i = 0; i < segments.length; i++) {
|
|
7615
|
-
walked =
|
|
7680
|
+
walked = join8(walked, segments[i]);
|
|
7616
7681
|
try {
|
|
7617
7682
|
if (lstatSync2(walked).isSymbolicLink())
|
|
7618
7683
|
return segments.slice(0, i + 1).join("/");
|
|
@@ -7630,30 +7695,30 @@ function symlinkEntry(path, link, isDirectoryRow) {
|
|
|
7630
7695
|
};
|
|
7631
7696
|
}
|
|
7632
7697
|
function applyFileRow(targetRepo, path, content) {
|
|
7633
|
-
const abs =
|
|
7698
|
+
const abs = join8(targetRepo, path);
|
|
7634
7699
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
7635
7700
|
if (link !== null)
|
|
7636
7701
|
return symlinkEntry(path, link, false);
|
|
7637
|
-
if (
|
|
7638
|
-
const current =
|
|
7702
|
+
if (existsSync6(abs)) {
|
|
7703
|
+
const current = readFileSync7(abs, "utf-8");
|
|
7639
7704
|
return current === content ? { path, outcome: "already-present", note: "already present and correct \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this file \u2014 left exactly as it was" };
|
|
7640
7705
|
}
|
|
7641
|
-
mkdirSync5(
|
|
7706
|
+
mkdirSync5(dirname5(abs), { recursive: true });
|
|
7642
7707
|
writeFileSync5(abs, content, "utf-8");
|
|
7643
7708
|
return { path, outcome: "created", note: "created" };
|
|
7644
7709
|
}
|
|
7645
7710
|
function applySectionRow(targetRepo, path, heading, body) {
|
|
7646
|
-
const abs =
|
|
7711
|
+
const abs = join8(targetRepo, path);
|
|
7647
7712
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
7648
7713
|
if (link !== null)
|
|
7649
7714
|
return symlinkEntry(path, link, false);
|
|
7650
7715
|
const section = renderSection(heading, body);
|
|
7651
|
-
if (!
|
|
7652
|
-
mkdirSync5(
|
|
7716
|
+
if (!existsSync6(abs)) {
|
|
7717
|
+
mkdirSync5(dirname5(abs), { recursive: true });
|
|
7653
7718
|
writeFileSync5(abs, section, "utf-8");
|
|
7654
7719
|
return { path, outcome: "created", note: `created, with a "${heading}" section` };
|
|
7655
7720
|
}
|
|
7656
|
-
const existing =
|
|
7721
|
+
const existing = readFileSync7(abs, "utf-8");
|
|
7657
7722
|
if (hasSection(existing, heading)) {
|
|
7658
7723
|
return {
|
|
7659
7724
|
path,
|
|
@@ -7670,11 +7735,11 @@ function applySectionRow(targetRepo, path, heading, body) {
|
|
|
7670
7735
|
};
|
|
7671
7736
|
}
|
|
7672
7737
|
function applyDirectoryRow(targetRepo, path) {
|
|
7673
|
-
const abs =
|
|
7738
|
+
const abs = join8(targetRepo, path);
|
|
7674
7739
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
7675
7740
|
if (link !== null)
|
|
7676
7741
|
return symlinkEntry(path, link, true);
|
|
7677
|
-
if (
|
|
7742
|
+
if (existsSync6(abs)) {
|
|
7678
7743
|
const kind = statSync(abs).isDirectory() ? "directory" : "file";
|
|
7679
7744
|
return kind === "directory" ? { path: `${path}/`, outcome: "already-present", note: "already there \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this path \u2014 left exactly as it was" };
|
|
7680
7745
|
}
|
|
@@ -7682,7 +7747,7 @@ function applyDirectoryRow(targetRepo, path) {
|
|
|
7682
7747
|
return { path: `${path}/`, outcome: "created", note: "created" };
|
|
7683
7748
|
}
|
|
7684
7749
|
function applySetupWrites(targetRepo, rows) {
|
|
7685
|
-
if (!
|
|
7750
|
+
if (!existsSync6(targetRepo)) {
|
|
7686
7751
|
throw new Error(`[halfcycle setup] The project directory does not exist: ${targetRepo}`);
|
|
7687
7752
|
}
|
|
7688
7753
|
assertNoConflictingDuplicates(rows);
|
|
@@ -7793,8 +7858,8 @@ function trackerHomeRow(home = FLAT_FILE_TRACKER_HOME) {
|
|
|
7793
7858
|
}
|
|
7794
7859
|
|
|
7795
7860
|
// dist/setup/discovery.js
|
|
7796
|
-
import { existsSync as
|
|
7797
|
-
import { join as
|
|
7861
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync, statSync as statSync2 } from "node:fs";
|
|
7862
|
+
import { join as join9 } from "node:path";
|
|
7798
7863
|
function renderAnchor(anchor) {
|
|
7799
7864
|
return anchor.kind === "file" ? `${anchor.path}:${anchor.line}` : `$ ${anchor.command}`;
|
|
7800
7865
|
}
|
|
@@ -7818,8 +7883,8 @@ function lineOf(text, needle) {
|
|
|
7818
7883
|
return null;
|
|
7819
7884
|
}
|
|
7820
7885
|
function readOrNull(repo, rel) {
|
|
7821
|
-
const abs =
|
|
7822
|
-
return
|
|
7886
|
+
const abs = join9(repo, rel);
|
|
7887
|
+
return existsSync7(abs) && statSync2(abs).isFile() ? readFileSync8(abs, "utf-8") : null;
|
|
7823
7888
|
}
|
|
7824
7889
|
function anchoredIn(repo, rel, needle, value) {
|
|
7825
7890
|
const text = readOrNull(repo, rel);
|
|
@@ -7831,8 +7896,8 @@ function anchoredIn(repo, rel, needle, value) {
|
|
|
7831
7896
|
return { value, anchor: { kind: "file", path: rel, line } };
|
|
7832
7897
|
}
|
|
7833
7898
|
function presenceAnchor(repo, rel) {
|
|
7834
|
-
const abs =
|
|
7835
|
-
if (!
|
|
7899
|
+
const abs = join9(repo, rel);
|
|
7900
|
+
if (!existsSync7(abs))
|
|
7836
7901
|
return null;
|
|
7837
7902
|
return statSync2(abs).isDirectory() ? { kind: "command", command: `ls -d ${rel}`, output: `${rel}/` } : { kind: "file", path: rel, line: 1 };
|
|
7838
7903
|
}
|
|
@@ -7873,8 +7938,8 @@ function firstMatch(items, f) {
|
|
|
7873
7938
|
}
|
|
7874
7939
|
var CONTEXT_INDEX_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
7875
7940
|
function ciWorkflows(repo) {
|
|
7876
|
-
const dir =
|
|
7877
|
-
if (!
|
|
7941
|
+
const dir = join9(repo, ".github", "workflows");
|
|
7942
|
+
if (!existsSync7(dir) || !statSync2(dir).isDirectory())
|
|
7878
7943
|
return [];
|
|
7879
7944
|
return readdirSync(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).sort();
|
|
7880
7945
|
}
|
|
@@ -7882,21 +7947,21 @@ function existingLayerDocuments(repo) {
|
|
|
7882
7947
|
const found = [];
|
|
7883
7948
|
for (const paths of Object.values(LAYER_ARTEFACT_PATHS)) {
|
|
7884
7949
|
for (const p of paths ?? []) {
|
|
7885
|
-
if (
|
|
7950
|
+
if (existsSync7(join9(repo, p)))
|
|
7886
7951
|
found.push(p);
|
|
7887
7952
|
}
|
|
7888
7953
|
}
|
|
7889
7954
|
return found.sort();
|
|
7890
7955
|
}
|
|
7891
7956
|
function discoverRepository(targetRepo) {
|
|
7892
|
-
if (!
|
|
7957
|
+
if (!existsSync7(targetRepo)) {
|
|
7893
7958
|
throw new Error(`[halfcycle setup] The project directory does not exist: ${targetRepo}`);
|
|
7894
7959
|
}
|
|
7895
7960
|
const findings = [];
|
|
7896
7961
|
const questions = [];
|
|
7897
7962
|
settleOrAsk("project-language", firstMatch(MANIFESTS, ([file, needle, language]) => anchoredIn(targetRepo, file, needle, language)), "What is this project written in?", findings, questions);
|
|
7898
7963
|
settleOrAsk("package-manager", firstMatch(LOCKFILES, ([file, manager]) => anchoredByPresence(targetRepo, file, manager)), "Which package manager does this project use?", findings, questions);
|
|
7899
|
-
settleOrAsk("repo-structure", firstMatch(WORKSPACE_MARKERS, ([file, needle, shape]) => anchoredIn(targetRepo, file, needle, shape)) ?? (
|
|
7964
|
+
settleOrAsk("repo-structure", firstMatch(WORKSPACE_MARKERS, ([file, needle, shape]) => anchoredIn(targetRepo, file, needle, shape)) ?? (existsSync7(join9(targetRepo, "package.json")) || existsSync7(join9(targetRepo, "go.mod")) || existsSync7(join9(targetRepo, "pyproject.toml")) ? anchoredByPresence(targetRepo, "package.json", "a single-package repository") ?? anchoredByPresence(targetRepo, "go.mod", "a single-package repository") ?? anchoredByPresence(targetRepo, "pyproject.toml", "a single-package repository") : null), "Is this one repository for one product, or one of several?", findings, questions);
|
|
7900
7965
|
const workflows = ciWorkflows(targetRepo);
|
|
7901
7966
|
settleOrAsk("ci-strategy", workflows.length > 0 ? {
|
|
7902
7967
|
value: `GitHub Actions \u2014 ${workflows.length === 1 ? "one workflow" : `${workflows.length} workflows`}: ${workflows.join(", ")}`,
|
|
@@ -8566,11 +8631,11 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
8566
8631
|
}
|
|
8567
8632
|
|
|
8568
8633
|
// dist/build-record/write.js
|
|
8569
|
-
import { existsSync as
|
|
8570
|
-
import { basename, join as
|
|
8634
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "node:fs";
|
|
8635
|
+
import { basename, join as join10, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
|
|
8571
8636
|
|
|
8572
8637
|
// dist/build-record/pair-io.js
|
|
8573
|
-
import { closeSync, openSync, readFileSync as
|
|
8638
|
+
import { closeSync, openSync, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
|
|
8574
8639
|
function createExclusiveOnDisk(path, data) {
|
|
8575
8640
|
const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
8576
8641
|
const fd = openSync(path, "wx");
|
|
@@ -8601,7 +8666,7 @@ function isEexist(err) {
|
|
|
8601
8666
|
}
|
|
8602
8667
|
function readIfPresent(path) {
|
|
8603
8668
|
try {
|
|
8604
|
-
return
|
|
8669
|
+
return readFileSync9(path, "utf-8");
|
|
8605
8670
|
} catch {
|
|
8606
8671
|
return null;
|
|
8607
8672
|
}
|
|
@@ -8633,7 +8698,7 @@ function replaceOne(io, tmp, finalPath, text) {
|
|
|
8633
8698
|
}
|
|
8634
8699
|
|
|
8635
8700
|
// dist/build-record/write.js
|
|
8636
|
-
var BUILD_RECORD_DIR =
|
|
8701
|
+
var BUILD_RECORD_DIR = join10("docs", "build-records");
|
|
8637
8702
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
8638
8703
|
const rel = relative2(outDir, resolve2(candidate));
|
|
8639
8704
|
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename(rel);
|
|
@@ -8671,8 +8736,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
8671
8736
|
}
|
|
8672
8737
|
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
8673
8738
|
const segment = renderPhaseSegment(phase);
|
|
8674
|
-
const jsonPath =
|
|
8675
|
-
const mdPath =
|
|
8739
|
+
const jsonPath = join10(outDir, `${segment}.json`);
|
|
8740
|
+
const mdPath = join10(outDir, `${segment}.md`);
|
|
8676
8741
|
const jsonTmp = `${jsonPath}.tmp`;
|
|
8677
8742
|
const mdTmp = `${mdPath}.tmp`;
|
|
8678
8743
|
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
@@ -8686,7 +8751,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
8686
8751
|
}
|
|
8687
8752
|
const existingJson = readIfPresent(jsonPath);
|
|
8688
8753
|
if (existingJson === null) {
|
|
8689
|
-
return { kind: "in-the-way", path:
|
|
8754
|
+
return { kind: "in-the-way", path: existsSync8(jsonPath) ? jsonPath : mdPath };
|
|
8690
8755
|
}
|
|
8691
8756
|
if (existingJson === jsonText) {
|
|
8692
8757
|
replaceOne(io, mdTmp, mdPath, mdText);
|
|
@@ -8748,20 +8813,20 @@ function isMissingRoute(method, status, body) {
|
|
|
8748
8813
|
}
|
|
8749
8814
|
|
|
8750
8815
|
// dist/build-record/write-change.js
|
|
8751
|
-
import { basename as basename2, isAbsolute as isAbsolute4, join as
|
|
8752
|
-
var CHANGE_RECORD_DIR =
|
|
8816
|
+
import { basename as basename2, isAbsolute as isAbsolute4, join as join11, relative as relative3, resolve as resolve3 } from "node:path";
|
|
8817
|
+
var CHANGE_RECORD_DIR = join11("docs", "build-records", "changes");
|
|
8753
8818
|
|
|
8754
8819
|
// dist/change.js
|
|
8755
|
-
var CHANGE_USAGE = `${
|
|
8756
|
-
${
|
|
8757
|
-
${
|
|
8820
|
+
var CHANGE_USAGE = `${ownCommand()} change sort --request "<one line>" --touches <path>\u2026 [--contract <from>:<to>]\u2026 [--stored-data] [--standing-rule] [--new-part] [--repo <root>]
|
|
8821
|
+
${ownCommand()} change up <mini|full> (--change <id> | --phase <phase>) [--repo <root>]
|
|
8822
|
+
${ownCommand()} change done <id> [--repo <root>]`;
|
|
8758
8823
|
|
|
8759
8824
|
// dist/plan.js
|
|
8760
|
-
var PLAN_USAGE = `${
|
|
8825
|
+
var PLAN_USAGE = `${ownCommand()} plan <phase> [--show] [--repo <root>]`;
|
|
8761
8826
|
|
|
8762
8827
|
// dist/review.js
|
|
8763
|
-
var REVIEW_USAGE = `${
|
|
8764
|
-
var RV9_LINE = `halfcycle review: this project is not signed in on this computer. ${
|
|
8828
|
+
var REVIEW_USAGE = `${ownCommand()} review <phase> (--show | --answer <part> "<answer>") [--repo <root>]`;
|
|
8829
|
+
var RV9_LINE = `halfcycle review: this project is not signed in on this computer. ${hookRemedy(ownVersion())}`;
|
|
8765
8830
|
|
|
8766
8831
|
// dist/open-phase.js
|
|
8767
8832
|
var OPEN_FROM_SIZE_REFUSALS = Object.freeze({
|
|
@@ -8877,16 +8942,16 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
|
|
|
8877
8942
|
};
|
|
8878
8943
|
}
|
|
8879
8944
|
function refetchCommand(phase) {
|
|
8880
|
-
return `${
|
|
8945
|
+
return `${ownCommand()} build-record ${quoteForShell(phase)}`;
|
|
8881
8946
|
}
|
|
8882
8947
|
|
|
8883
8948
|
// dist/check-tasks.js
|
|
8884
|
-
var CHECK_TASKS_USAGE = `${
|
|
8949
|
+
var CHECK_TASKS_USAGE = `${ownCommand()} check-tasks <phase> [--with-task-list] [--repo <root>]`;
|
|
8885
8950
|
var MISSING_PLAN_LINE = `there is no ${TASK_PLAN_FILE} in this project yet. Run the task-graph step to write it, then run this again.`;
|
|
8886
8951
|
var SPEC_FOLDER = LAYER_ARTEFACT_PATHS.l4[0];
|
|
8887
8952
|
|
|
8888
8953
|
// dist/ready.js
|
|
8889
|
-
var READY_USAGE = `${
|
|
8954
|
+
var READY_USAGE = `${ownCommand()} ready <phase> [--cap <n>] [--repo <root>]`;
|
|
8890
8955
|
var STATES_TEXT = buildTaskStateSchema.options.join(", ");
|
|
8891
8956
|
var READY_LINES = {
|
|
8892
8957
|
start: (id, title) => `Ready to start: ${named(id, title)}`,
|