halfcycle 0.4.14 → 0.4.15
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 +2525 -541
- package/bin/pre-tool-use-filter.sh +41 -5
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/consistency-check.md +1 -1
- package/commands/context-update.md +1 -1
- package/commands/halfcycle-design.md +1 -1
- package/commands/halfcycle-setup.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/commands/task-graph.md +1 -1
- package/commands/write-spec.md +1 -1
- package/dist/bin.js +0 -0
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/claude-plan.d.ts +24 -5
- package/dist/claude-plan.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 +2242 -565
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/folder-refusals.d.ts +27 -0
- package/dist/folder-refusals.d.ts.map +1 -0
- package/dist/identity.d.ts +31 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +1276 -386
- package/dist/index.js.map +3 -3
- package/dist/install-plan.d.ts +112 -0
- package/dist/install-plan.d.ts.map +1 -0
- package/dist/install.d.ts +535 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/plan.d.ts +16 -3
- package/dist/plan.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts +28 -0
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/preview.d.ts +40 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/restore.d.ts +58 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/review.d.ts.map +1 -1
- package/dist/scan.d.ts +11 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/setup/discovery.d.ts.map +1 -1
- package/dist/setup/rows.d.ts +10 -6
- package/dist/setup/rows.d.ts.map +1 -1
- package/package.json +12 -12
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/bin.js
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { homedir as homedir4 } from "node:os";
|
|
4
|
+
import { existsSync as existsSync12, statSync as statSync2, writeSync as writeSync3 } from "node:fs";
|
|
6
5
|
|
|
7
6
|
// dist/install.js
|
|
8
7
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
9
8
|
import { createHash as createHash2 } from "node:crypto";
|
|
10
|
-
import { existsSync as
|
|
9
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
11
10
|
import { homedir as homedir2 } from "node:os";
|
|
12
|
-
import { dirname as dirname3, isAbsolute as isAbsolute2, join as
|
|
11
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, posix as posix2, relative, resolve } from "node:path";
|
|
13
12
|
import { fileURLToPath } from "node:url";
|
|
14
13
|
|
|
15
14
|
// ../core/dist/guard-record.js
|
|
@@ -139,6 +138,7 @@ function parseAccountStoreText(raw) {
|
|
|
139
138
|
|
|
140
139
|
// ../core/dist/hook-wiring.js
|
|
141
140
|
var SETTINGS_REL = ".claude/settings.json";
|
|
141
|
+
var LOCAL_SETTINGS_REL = ".claude/settings.local.json";
|
|
142
142
|
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
143
143
|
var PRE_FILTER_SHELL_FILENAME = "pre-tool-use-filter.sh";
|
|
144
144
|
var PRE_FILTER_FUNCTION = "halfcycle_pt_skip";
|
|
@@ -146,9 +146,10 @@ var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
|
146
146
|
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
147
147
|
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
148
148
|
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
149
|
+
var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
|
|
149
150
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
150
151
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
151
|
-
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent)$";
|
|
152
|
+
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
|
|
152
153
|
function hookCommand(script, verb) {
|
|
153
154
|
const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
|
|
154
155
|
return verb === void 0 ? invocation : `${invocation} ${verb}`;
|
|
@@ -182,6 +183,13 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
182
183
|
command: hookCommand(USER_PROMPT_SCOPE_REL),
|
|
183
184
|
script: USER_PROMPT_SCOPE_REL
|
|
184
185
|
},
|
|
186
|
+
{
|
|
187
|
+
event: "UserPromptSubmit",
|
|
188
|
+
matcher: null,
|
|
189
|
+
timeout: 10,
|
|
190
|
+
command: hookCommand(USER_PROMPT_FINDINGS_REL),
|
|
191
|
+
script: USER_PROMPT_FINDINGS_REL
|
|
192
|
+
},
|
|
185
193
|
{
|
|
186
194
|
event: "PreToolUse",
|
|
187
195
|
matcher: PRE_TOOL_USE_MATCHER,
|
|
@@ -631,9 +639,9 @@ function waitsFor(tasks) {
|
|
|
631
639
|
const n = tasks.length;
|
|
632
640
|
const positionsOf = /* @__PURE__ */ new Map();
|
|
633
641
|
tasks.forEach((task, i) => {
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
636
|
-
|
|
642
|
+
const list2 = positionsOf.get(task.id);
|
|
643
|
+
if (list2)
|
|
644
|
+
list2.push(i);
|
|
637
645
|
else
|
|
638
646
|
positionsOf.set(task.id, [i]);
|
|
639
647
|
});
|
|
@@ -759,17 +767,17 @@ function hasBit(bits, i) {
|
|
|
759
767
|
function isComparedKind(task) {
|
|
760
768
|
return task.kind === "build" || task.kind === "spec";
|
|
761
769
|
}
|
|
762
|
-
function firstDifference(plan,
|
|
770
|
+
function firstDifference(plan, list2) {
|
|
763
771
|
const planIds = new Set(plan.map((task) => task.id));
|
|
764
|
-
const listIds = new Set(
|
|
772
|
+
const listIds = new Set(list2.map((task) => task.id));
|
|
765
773
|
for (const task of plan)
|
|
766
774
|
if (!listIds.has(task.id))
|
|
767
775
|
return `"${task.id}"`;
|
|
768
|
-
for (const task of
|
|
776
|
+
for (const task of list2)
|
|
769
777
|
if (!planIds.has(task.id))
|
|
770
778
|
return `"${task.id}"`;
|
|
771
779
|
const listDeps = /* @__PURE__ */ new Map();
|
|
772
|
-
for (const task of
|
|
780
|
+
for (const task of list2) {
|
|
773
781
|
const deps = listDeps.get(task.id) ?? /* @__PURE__ */ new Set();
|
|
774
782
|
for (const dep of task.dependsOn)
|
|
775
783
|
deps.add(dep);
|
|
@@ -788,7 +796,7 @@ function firstDifference(plan, list) {
|
|
|
788
796
|
if (!theirs.has(dep))
|
|
789
797
|
return `"${task.id}" \u2192 "${dep}"`;
|
|
790
798
|
}
|
|
791
|
-
for (const task of
|
|
799
|
+
for (const task of list2) {
|
|
792
800
|
const ours = planDeps.get(task.id);
|
|
793
801
|
for (const dep of task.dependsOn)
|
|
794
802
|
if (!ours.has(dep))
|
|
@@ -1005,10 +1013,10 @@ function checkTaskPlan(plan, request) {
|
|
|
1005
1013
|
}
|
|
1006
1014
|
}
|
|
1007
1015
|
lines.push(...placementLines(tasks, before));
|
|
1008
|
-
const
|
|
1009
|
-
if (!
|
|
1016
|
+
const refused3 = lines.some((line) => line.refuses);
|
|
1017
|
+
if (!refused3)
|
|
1010
1018
|
lines.push({ code: "P", text: taskPlanLineText.P(), refuses: false });
|
|
1011
|
-
return { lines, refused:
|
|
1019
|
+
return { lines, refused: refused3 };
|
|
1012
1020
|
}
|
|
1013
1021
|
|
|
1014
1022
|
// ../events/dist/result.js
|
|
@@ -1688,15 +1696,15 @@ function crossFieldProblems(raw) {
|
|
|
1688
1696
|
ids.add(part.id);
|
|
1689
1697
|
}
|
|
1690
1698
|
if (typeof part.lane === "string" && typeof part.slot === "number") {
|
|
1691
|
-
const
|
|
1692
|
-
const earlier = placed.get(
|
|
1699
|
+
const place2 = `${part.lane}\0${part.slot}`;
|
|
1700
|
+
const earlier = placed.get(place2);
|
|
1693
1701
|
if (earlier !== void 0) {
|
|
1694
1702
|
problems.push({
|
|
1695
1703
|
path: `parts.${i}.slot`,
|
|
1696
1704
|
message: `${quoted(parts[earlier].id)} already sits in lane ${shown(part.lane)} at slot ${part.slot}`
|
|
1697
1705
|
});
|
|
1698
1706
|
} else {
|
|
1699
|
-
placed.set(
|
|
1707
|
+
placed.set(place2, i);
|
|
1700
1708
|
}
|
|
1701
1709
|
}
|
|
1702
1710
|
if (typeof part.outside === "boolean" && Array.isArray(part.owns)) {
|
|
@@ -2181,8 +2189,9 @@ var SCOPE_PART_STATUSES = ["approved", "waiting"];
|
|
|
2181
2189
|
var scopePartStatusSchema = z10.enum(SCOPE_PART_STATUSES);
|
|
2182
2190
|
var SCOPE_ACTS = ["approve", "send-back"];
|
|
2183
2191
|
var scopeActSchema = z10.enum(SCOPE_ACTS);
|
|
2184
|
-
var SCOPE_GIVEN_IN = ["console", "chat"];
|
|
2192
|
+
var SCOPE_GIVEN_IN = ["console", "chat", "autonomy"];
|
|
2185
2193
|
var scopeGivenInSchema = z10.enum(SCOPE_GIVEN_IN);
|
|
2194
|
+
var APPROVED_IN_ADVANCE_SENTENCE = "approved in advance by your autonomy setting";
|
|
2186
2195
|
var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
|
|
2187
2196
|
var scopeNoteWireSchema = z10.string().max(SCOPE_NOTE_MAX);
|
|
2188
2197
|
var SCOPE_PART_VERDICTS = ["ok", "change"];
|
|
@@ -2213,7 +2222,8 @@ var phasePlanRecordedSchema = z10.object({
|
|
|
2213
2222
|
typedApprovalOffered: z10.boolean(),
|
|
2214
2223
|
reportedAt: utcIso8601,
|
|
2215
2224
|
carriedFrom: z10.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional(),
|
|
2216
|
-
movedUp: planMovedUpSchema.optional()
|
|
2225
|
+
movedUp: planMovedUpSchema.optional(),
|
|
2226
|
+
givenIn: scopeGivenInSchema.optional()
|
|
2217
2227
|
}).strict();
|
|
2218
2228
|
var phasePlanRefusedSchema = z10.object({
|
|
2219
2229
|
outcome: z10.literal("refused"),
|
|
@@ -4029,6 +4039,15 @@ var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-pla
|
|
|
4029
4039
|
var reviewPartChangeSchema = z20.enum(REVIEW_PART_CHANGES);
|
|
4030
4040
|
var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
|
|
4031
4041
|
var reviewDecisionSchema = z20.enum(REVIEW_DECISIONS);
|
|
4042
|
+
var reviewGivenInSchema = scopeGivenInSchema.extract(["autonomy"]);
|
|
4043
|
+
var DECIDED_BY_AUTONOMY_SENTENCE = "decided by your autonomy setting";
|
|
4044
|
+
function checkReviewGivenIn(v, ctx) {
|
|
4045
|
+
if (v.givenIn === void 0)
|
|
4046
|
+
return;
|
|
4047
|
+
if (v.decision !== "accepted" && v.decision !== "carried") {
|
|
4048
|
+
ctx.addIssue({ code: "custom", path: ["givenIn"], message: "is present only on a part accepted or carried" });
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4032
4051
|
function actOf(decision) {
|
|
4033
4052
|
if (decision === "accepted")
|
|
4034
4053
|
return "accept";
|
|
@@ -4111,6 +4130,25 @@ var reviewRefusedSchema = z20.object({
|
|
|
4111
4130
|
function bareReviewOutcome(outcome) {
|
|
4112
4131
|
return z20.object({ outcome: z20.literal(outcome) }).strict();
|
|
4113
4132
|
}
|
|
4133
|
+
var reviewClosedByAutonomySchema = z20.object({
|
|
4134
|
+
outcome: z20.literal("closed-by-autonomy"),
|
|
4135
|
+
accepted: z20.number().int().min(0).max(REVIEW_PARTS_MAX),
|
|
4136
|
+
carried: z20.number().int().min(0).max(REVIEW_PARTS_MAX)
|
|
4137
|
+
}).strict().refine((v) => v.accepted + v.carried <= REVIEW_PARTS_MAX, {
|
|
4138
|
+
message: "cannot exceed the parts a round holds",
|
|
4139
|
+
path: ["carried"]
|
|
4140
|
+
});
|
|
4141
|
+
var AUTONOMY_WAIT_REASONS = ["unchecked-claims", "built-without-approval"];
|
|
4142
|
+
var autonomyWaitReasonSchema = z20.enum(AUTONOMY_WAIT_REASONS);
|
|
4143
|
+
var reviewAutonomyWaitsSchema = z20.object({
|
|
4144
|
+
outcome: z20.literal("autonomy-waits"),
|
|
4145
|
+
reason: autonomyWaitReasonSchema,
|
|
4146
|
+
count: z20.number().int().min(1),
|
|
4147
|
+
parts: z20.array(z20.object({ part: systemPartIdSchema, name: systemPartSchema.shape.name }).strict()).min(1).max(REVIEW_PARTS_MAX)
|
|
4148
|
+
}).strict().refine((v) => v.reason !== "built-without-approval" || v.count === v.parts.length, {
|
|
4149
|
+
message: "counts the parts it names",
|
|
4150
|
+
path: ["count"]
|
|
4151
|
+
});
|
|
4114
4152
|
var openReviewResultSchema = z20.discriminatedUnion("outcome", [
|
|
4115
4153
|
reviewOpenedSchema,
|
|
4116
4154
|
reviewTasksOpenSchema,
|
|
@@ -4119,7 +4157,9 @@ var openReviewResultSchema = z20.discriminatedUnion("outcome", [
|
|
|
4119
4157
|
reviewOutstandingSchema,
|
|
4120
4158
|
reviewCleanOverFindingsSchema,
|
|
4121
4159
|
reviewRefusedSchema,
|
|
4122
|
-
bareReviewOutcome("unavailable")
|
|
4160
|
+
bareReviewOutcome("unavailable"),
|
|
4161
|
+
reviewClosedByAutonomySchema,
|
|
4162
|
+
reviewAutonomyWaitsSchema
|
|
4123
4163
|
]);
|
|
4124
4164
|
var reviewPhaseNotOpenSchema = z20.object({
|
|
4125
4165
|
statusCode: z20.literal(409),
|
|
@@ -4233,6 +4273,7 @@ var reviewViewPartSchema = z20.object({
|
|
|
4233
4273
|
decidable: z20.boolean(),
|
|
4234
4274
|
waiting: z20.boolean(),
|
|
4235
4275
|
decision: reviewDecisionSchema.optional(),
|
|
4276
|
+
givenIn: reviewGivenInSchema.optional(),
|
|
4236
4277
|
reason: reviewNoteSchema.optional(),
|
|
4237
4278
|
carriedTo: reviewCarriedToSchema.optional(),
|
|
4238
4279
|
sentBack: z20.array(reviewSentBackSchema),
|
|
@@ -4260,6 +4301,7 @@ var reviewViewPartSchema = z20.object({
|
|
|
4260
4301
|
if (v.change === "not-in-plan" && !v.built) {
|
|
4261
4302
|
ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
|
|
4262
4303
|
}
|
|
4304
|
+
checkReviewGivenIn(v, ctx);
|
|
4263
4305
|
});
|
|
4264
4306
|
var reviewPhaseNumberSchema = z20.number().int().min(1).nullable();
|
|
4265
4307
|
var phaseReviewNoneSchema = z20.object({
|
|
@@ -4354,6 +4396,7 @@ var recordReviewPartSchema = z21.object({
|
|
|
4354
4396
|
built: z21.boolean(),
|
|
4355
4397
|
builtWithoutApproval: z21.boolean(),
|
|
4356
4398
|
decision: reviewDecisionSchema,
|
|
4399
|
+
givenIn: reviewGivenInSchema.optional(),
|
|
4357
4400
|
reason: reviewNoteSchema.optional(),
|
|
4358
4401
|
carriedTo: reviewCarriedToSchema.optional(),
|
|
4359
4402
|
sentBack: z21.array(reviewSentBackSchema),
|
|
@@ -4367,6 +4410,7 @@ var recordReviewPartSchema = z21.object({
|
|
|
4367
4410
|
if (v.carriedTo !== void 0 !== (v.decision === "carried")) {
|
|
4368
4411
|
ctx.addIssue({ code: "custom", path: ["carriedTo"], message: "is present exactly on a carry" });
|
|
4369
4412
|
}
|
|
4413
|
+
checkReviewGivenIn(v, ctx);
|
|
4370
4414
|
});
|
|
4371
4415
|
var SPINE_ORDER = boardGateNameSchema.options;
|
|
4372
4416
|
var recordReviewGatesSchema = z21.array(recordReviewGateSchema).superRefine((gates, ctx) => {
|
|
@@ -4899,7 +4943,12 @@ var ownerWaitDeclareRequestSchema = z31.object({
|
|
|
4899
4943
|
var OWNER_WAIT_DECLARE_REFUSALS = ["no-open-phase", "unknown-item", "say-if-undoable"];
|
|
4900
4944
|
var ownerWaitDeclareResultSchema = z31.discriminatedUnion("outcome", [
|
|
4901
4945
|
z31.object({ outcome: z31.literal("recorded"), id: z31.string().uuid(), say: z31.string().min(1) }).strict(),
|
|
4902
|
-
z31.object({
|
|
4946
|
+
z31.object({
|
|
4947
|
+
outcome: z31.literal("go-ahead"),
|
|
4948
|
+
id: z31.string().uuid(),
|
|
4949
|
+
callId: z31.number().int().positive(),
|
|
4950
|
+
say: z31.string().min(1)
|
|
4951
|
+
}).strict(),
|
|
4903
4952
|
z31.object({ outcome: z31.literal("refused"), reason: z31.enum(OWNER_WAIT_DECLARE_REFUSALS) }).strict()
|
|
4904
4953
|
]);
|
|
4905
4954
|
var ownerWaitAddedItemSchema = z31.object({
|
|
@@ -5101,6 +5150,201 @@ function paceAllowedNowOf(k) {
|
|
|
5101
5150
|
}
|
|
5102
5151
|
var PACE_AGENTS_TOO_MANY = `Run at most ${String(PACE_AGENTS_MAX)} agents at once.`;
|
|
5103
5152
|
|
|
5153
|
+
// ../events/dist/autonomy.js
|
|
5154
|
+
import { z as z33 } from "zod";
|
|
5155
|
+
var AUTONOMY_CHANGED_IN = ["console", "chat"];
|
|
5156
|
+
var autonomyChangedInSchema = z33.enum(AUTONOMY_CHANGED_IN);
|
|
5157
|
+
var autonomySettingsSchema = z33.object({
|
|
5158
|
+
on: z33.boolean(),
|
|
5159
|
+
// whether builds run on their own
|
|
5160
|
+
mayPublish: z33.boolean(),
|
|
5161
|
+
// whether builds may publish or release without waiting
|
|
5162
|
+
maySpend: z33.boolean(),
|
|
5163
|
+
// whether builds may spend money or sign up without waiting
|
|
5164
|
+
changedAt: utcIso8601.nullable(),
|
|
5165
|
+
// when the setting last changed; null if never
|
|
5166
|
+
changedIn: autonomyChangedInSchema.nullable()
|
|
5167
|
+
// where it last changed; null if never
|
|
5168
|
+
}).strict().refine((v) => v.changedAt === null === (v.changedIn === null), {
|
|
5169
|
+
message: "says when and where it changed, or neither",
|
|
5170
|
+
path: ["changedIn"]
|
|
5171
|
+
});
|
|
5172
|
+
var autonomyChangeRequestSchema = z33.object({
|
|
5173
|
+
on: z33.boolean(),
|
|
5174
|
+
// the switch
|
|
5175
|
+
mayPublish: z33.boolean(),
|
|
5176
|
+
// the publish box
|
|
5177
|
+
maySpend: z33.boolean()
|
|
5178
|
+
// the spend box
|
|
5179
|
+
}).strict();
|
|
5180
|
+
var AUTONOMY_CHAT_ACTS = ["on", "off"];
|
|
5181
|
+
var autonomyChatActSchema = z33.enum(AUTONOMY_CHAT_ACTS);
|
|
5182
|
+
var autonomyChatRequestSchema = z33.object({
|
|
5183
|
+
act: autonomyChatActSchema
|
|
5184
|
+
// what the owner typed
|
|
5185
|
+
}).strict();
|
|
5186
|
+
var autonomyChatResultSchema = z33.discriminatedUnion("outcome", [
|
|
5187
|
+
z33.object({
|
|
5188
|
+
outcome: z33.literal("recorded"),
|
|
5189
|
+
on: z33.boolean()
|
|
5190
|
+
// whether builds now run on their own
|
|
5191
|
+
}).strict(),
|
|
5192
|
+
z33.object({ outcome: z33.literal("not-owner") }).strict(),
|
|
5193
|
+
z33.object({ outcome: z33.literal("no-owner") }).strict(),
|
|
5194
|
+
z33.object({ outcome: z33.literal("not-offered") }).strict(),
|
|
5195
|
+
z33.object({ outcome: z33.literal("unavailable") }).strict()
|
|
5196
|
+
]);
|
|
5197
|
+
var autonomyStateSchema = z33.object({
|
|
5198
|
+
on: z33.boolean(),
|
|
5199
|
+
// whether builds run on their own
|
|
5200
|
+
mayPublish: z33.boolean(),
|
|
5201
|
+
// whether builds may publish or release without waiting
|
|
5202
|
+
maySpend: z33.boolean()
|
|
5203
|
+
// whether builds may spend money or sign up without waiting
|
|
5204
|
+
}).strict();
|
|
5205
|
+
var autonomyStateFileSchema = autonomyStateSchema.extend({
|
|
5206
|
+
readAt: utcIso8601
|
|
5207
|
+
// when this copy was read from Halfcycle
|
|
5208
|
+
}).strict();
|
|
5209
|
+
var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
5210
|
+
var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
|
|
5211
|
+
var autonomyActivityKindSchema = z33.enum(AUTONOMY_ACTIVITY_KINDS);
|
|
5212
|
+
var autonomyActivityRowSchema = z33.object({
|
|
5213
|
+
kind: autonomyActivityKindSchema,
|
|
5214
|
+
// what happened
|
|
5215
|
+
phase: scopePhaseSchema,
|
|
5216
|
+
// the phase it happened in
|
|
5217
|
+
question: ownerWaitText.optional(),
|
|
5218
|
+
// the agent's question, for a decision
|
|
5219
|
+
chose: ownerWaitText.optional(),
|
|
5220
|
+
// what the agent chose or recommends, for a decision
|
|
5221
|
+
undoable: ownerWaitUndoableSchema.optional(),
|
|
5222
|
+
// whether the agent says it can be undone
|
|
5223
|
+
how: statementText(STATEMENT_MAX).optional(),
|
|
5224
|
+
// how to undo it, when it can be
|
|
5225
|
+
at: utcIso8601
|
|
5226
|
+
// when it happened
|
|
5227
|
+
}).strict().superRefine((row, ctx) => {
|
|
5228
|
+
const decision = row.kind === "went-ahead" || row.kind === "waiting";
|
|
5229
|
+
if (decision && row.question === void 0) {
|
|
5230
|
+
ctx.addIssue({ code: "custom", message: "a decision says its question", path: ["question"] });
|
|
5231
|
+
}
|
|
5232
|
+
if (!decision) {
|
|
5233
|
+
for (const key of ["question", "chose", "undoable", "how"]) {
|
|
5234
|
+
if (row[key] !== void 0) {
|
|
5235
|
+
ctx.addIssue({ code: "custom", message: "only a decision carries this", path: [key] });
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
if (row.kind === "went-ahead") {
|
|
5240
|
+
if (row.chose === void 0) {
|
|
5241
|
+
ctx.addIssue({ code: "custom", message: "a decision that went ahead says what was chosen", path: ["chose"] });
|
|
5242
|
+
}
|
|
5243
|
+
if (row.undoable === void 0) {
|
|
5244
|
+
ctx.addIssue({
|
|
5245
|
+
code: "custom",
|
|
5246
|
+
message: "a decision that went ahead says whether it can be undone",
|
|
5247
|
+
path: ["undoable"]
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
|
|
5252
|
+
ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
|
|
5253
|
+
}
|
|
5254
|
+
if (row.kind === "went-ahead" && row.undoable === "yes" !== (row.how !== void 0)) {
|
|
5255
|
+
ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
|
|
5256
|
+
}
|
|
5257
|
+
});
|
|
5258
|
+
var autonomyActivitySchema = z33.object({
|
|
5259
|
+
rows: z33.array(autonomyActivityRowSchema)
|
|
5260
|
+
// newest first
|
|
5261
|
+
}).strict();
|
|
5262
|
+
var savedIdSchema = z33.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
|
|
5263
|
+
var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
|
|
5264
|
+
function savedRefOf(id) {
|
|
5265
|
+
return `${SAVED_REF_PREFIX}${id}`;
|
|
5266
|
+
}
|
|
5267
|
+
var SAVED_KEEP_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5268
|
+
var SAVED_DIR_NAME = "saved";
|
|
5269
|
+
var SAVED_FILE_NAME = "saved.json";
|
|
5270
|
+
var SAVED_COPIES_DIR_NAME = "files";
|
|
5271
|
+
var SAVED_REPOSITORY_FILE_NAME = "repository";
|
|
5272
|
+
var SAVED_HALF_WRITTEN_GRACE_MS = 6e4;
|
|
5273
|
+
function savedPruneAnswer(save, here) {
|
|
5274
|
+
const old = (t) => !Number.isFinite(t) || here.now - t > SAVED_KEEP_MS;
|
|
5275
|
+
if (save.refMadeAt !== null) {
|
|
5276
|
+
if (!old(save.refMadeAt))
|
|
5277
|
+
return "keep";
|
|
5278
|
+
return save.folderChangedAt === null ? "remove-ref" : "remove-ref-then-folder";
|
|
5279
|
+
}
|
|
5280
|
+
if (save.folderChangedAt === null || !Number.isFinite(save.folderChangedAt))
|
|
5281
|
+
return "keep";
|
|
5282
|
+
const ours = save.folderRepository === null || here.repository !== null && save.folderRepository === here.repository;
|
|
5283
|
+
const wait = ours ? SAVED_HALF_WRITTEN_GRACE_MS : SAVED_KEEP_MS;
|
|
5284
|
+
return here.now - save.folderChangedAt > wait ? "remove-folder" : "keep";
|
|
5285
|
+
}
|
|
5286
|
+
function isProjectRelativePath(p) {
|
|
5287
|
+
if (p.length === 0 || hasControlCharacter(p))
|
|
5288
|
+
return false;
|
|
5289
|
+
if (p.startsWith("/") || p.startsWith("\\") || /^[A-Za-z]:/.test(p))
|
|
5290
|
+
return false;
|
|
5291
|
+
return p.split(/[\\/]/).every((segment) => segment !== "" && windowsNameOf(segment) !== "" && !isGitFolderSegment(segment));
|
|
5292
|
+
}
|
|
5293
|
+
function windowsNameOf(segment) {
|
|
5294
|
+
return segment.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff]/g, "").split(":")[0].replace(/[. ]+$/, "").toLowerCase();
|
|
5295
|
+
}
|
|
5296
|
+
function isGitFolderSegment(segment) {
|
|
5297
|
+
const t = windowsNameOf(segment);
|
|
5298
|
+
return t === ".git" || /^git~\d+$/.test(t);
|
|
5299
|
+
}
|
|
5300
|
+
var SAVED_PATH_KEPT_IN = ["commit", "copy"];
|
|
5301
|
+
var savedPathSchema = z33.object({
|
|
5302
|
+
path: z33.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
|
|
5303
|
+
// the file, relative to the project
|
|
5304
|
+
keptIn: z33.enum(SAVED_PATH_KEPT_IN)
|
|
5305
|
+
// in the save's commit, or copied beside this file
|
|
5306
|
+
}).strict();
|
|
5307
|
+
var savedCommitSchema = z33.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
|
|
5308
|
+
var savedBranchSchema = z33.object({
|
|
5309
|
+
name: z33.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
|
|
5310
|
+
message: "must be a branch name"
|
|
5311
|
+
}),
|
|
5312
|
+
// the branch's name
|
|
5313
|
+
commit: savedCommitSchema
|
|
5314
|
+
// the commit it pointed at
|
|
5315
|
+
}).strict();
|
|
5316
|
+
var savedSchema = z33.object({
|
|
5317
|
+
id: savedIdSchema,
|
|
5318
|
+
// what restore is typed with
|
|
5319
|
+
savedAt: utcIso8601,
|
|
5320
|
+
// when the copy was made
|
|
5321
|
+
ref: z33.string().min(1),
|
|
5322
|
+
// where the saved commit is kept
|
|
5323
|
+
what: z33.string().min(1).max(2e3),
|
|
5324
|
+
// what the action would have changed, in words
|
|
5325
|
+
paths: z33.array(savedPathSchema),
|
|
5326
|
+
// every saved file
|
|
5327
|
+
branches: z33.array(savedBranchSchema)
|
|
5328
|
+
// every saved line of work
|
|
5329
|
+
}).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
|
|
5330
|
+
function autonomyA13(accepted, carried) {
|
|
5331
|
+
return `Your autonomy setting accepted the ${String(accepted)} built parts and carried ${String(carried)} unbuilt parts forward. The phase is closed.`;
|
|
5332
|
+
}
|
|
5333
|
+
function autonomyA14(count, parts) {
|
|
5334
|
+
return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests and decide those parts. See the console's review.`;
|
|
5335
|
+
}
|
|
5336
|
+
function autonomyA15(count) {
|
|
5337
|
+
return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
|
|
5338
|
+
}
|
|
5339
|
+
function namesOf(names) {
|
|
5340
|
+
if (names.length <= 1)
|
|
5341
|
+
return names[0] ?? "";
|
|
5342
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1] ?? ""}`;
|
|
5343
|
+
}
|
|
5344
|
+
function autonomyC8(agent, question2, chose) {
|
|
5345
|
+
return `${agent} decided "${question2}" without asking you, under your autonomy setting: chose "${chose}". It is on the list of things that wait for you, so it was sent to you as it was made.`;
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5104
5348
|
// ../events/dist/work-words.js
|
|
5105
5349
|
var CALLS_HEADING = "Calls made without asking you";
|
|
5106
5350
|
var CALLS_EXPLAINED = "These are the decisions the agents recorded taking on their own, each against a stated default. Halfcycle lists what they recorded; it cannot see a call an agent did not record.";
|
|
@@ -5473,45 +5717,64 @@ function mintIdentity(targetRepoRoot) {
|
|
|
5473
5717
|
note: PROJECT_IDENTITY_NOTE
|
|
5474
5718
|
};
|
|
5475
5719
|
}
|
|
5476
|
-
function
|
|
5477
|
-
const path = join3(targetRepoRoot,
|
|
5720
|
+
function plannedRemoteRepair(targetRepoRoot) {
|
|
5721
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
5478
5722
|
let onDisk;
|
|
5479
5723
|
try {
|
|
5480
5724
|
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
5481
5725
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
5482
|
-
return;
|
|
5726
|
+
return null;
|
|
5483
5727
|
onDisk = parsed;
|
|
5484
5728
|
} catch {
|
|
5485
|
-
return;
|
|
5729
|
+
return null;
|
|
5486
5730
|
}
|
|
5487
5731
|
const storedRemote = onDisk.remote ?? null;
|
|
5488
5732
|
const remote = redactRemoteCredential(storedRemote);
|
|
5489
5733
|
if (remote === storedRemote)
|
|
5490
|
-
return;
|
|
5491
|
-
|
|
5734
|
+
return null;
|
|
5735
|
+
return serializeProjectIdentity({
|
|
5492
5736
|
projectId: onDisk.projectId ?? "",
|
|
5493
5737
|
rootCommit: onDisk.rootCommit ?? null,
|
|
5494
5738
|
remote,
|
|
5495
5739
|
note: PROJECT_IDENTITY_NOTE
|
|
5496
|
-
};
|
|
5740
|
+
});
|
|
5741
|
+
}
|
|
5742
|
+
function replaceProjectIdentityFile(targetRepoRoot, text) {
|
|
5743
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
5497
5744
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
5498
5745
|
try {
|
|
5499
|
-
writeFileSync3(temporary,
|
|
5746
|
+
writeFileSync3(temporary, text, { encoding: "utf-8", flag: "wx" });
|
|
5500
5747
|
renameSync(temporary, path);
|
|
5501
5748
|
} catch {
|
|
5502
5749
|
} finally {
|
|
5503
5750
|
rmSync(temporary, { force: true });
|
|
5504
5751
|
}
|
|
5505
5752
|
}
|
|
5753
|
+
function planProjectIdentity(targetRepoRoot) {
|
|
5754
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
5755
|
+
if (!existsSync(path)) {
|
|
5756
|
+
return { action: "add", after: serializeProjectIdentity(mintIdentity(targetRepoRoot)) };
|
|
5757
|
+
}
|
|
5758
|
+
const repaired = plannedRemoteRepair(targetRepoRoot);
|
|
5759
|
+
if (repaired !== null)
|
|
5760
|
+
return { action: "change", after: repaired };
|
|
5761
|
+
let current;
|
|
5762
|
+
try {
|
|
5763
|
+
current = readFileSync3(path, "utf-8");
|
|
5764
|
+
} catch {
|
|
5765
|
+
current = null;
|
|
5766
|
+
}
|
|
5767
|
+
return { action: "leave", after: current };
|
|
5768
|
+
}
|
|
5506
5769
|
function serializeProjectIdentity(identity) {
|
|
5507
5770
|
return JSON.stringify(identity, null, 2) + "\n";
|
|
5508
5771
|
}
|
|
5509
|
-
function createProjectIdentityFile(targetRepoRoot) {
|
|
5772
|
+
function createProjectIdentityFile(targetRepoRoot, text) {
|
|
5510
5773
|
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
5511
5774
|
mkdirSync3(dirname2(path), { recursive: true });
|
|
5512
5775
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
5513
5776
|
try {
|
|
5514
|
-
writeFileSync3(temporary, serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
5777
|
+
writeFileSync3(temporary, text ?? serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
5515
5778
|
encoding: "utf-8",
|
|
5516
5779
|
flag: "wx"
|
|
5517
5780
|
});
|
|
@@ -5526,6 +5789,10 @@ function createProjectIdentityFile(targetRepoRoot) {
|
|
|
5526
5789
|
}
|
|
5527
5790
|
}
|
|
5528
5791
|
|
|
5792
|
+
// dist/install-plan.js
|
|
5793
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
5794
|
+
import { join as join5, posix } from "node:path";
|
|
5795
|
+
|
|
5529
5796
|
// dist/install-manifest.js
|
|
5530
5797
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
5531
5798
|
import { createHash } from "node:crypto";
|
|
@@ -5589,10 +5856,10 @@ function stringArray(value, field) {
|
|
|
5589
5856
|
return value;
|
|
5590
5857
|
}
|
|
5591
5858
|
function hashArray(value, field) {
|
|
5592
|
-
const
|
|
5593
|
-
if (!
|
|
5859
|
+
const list2 = stringArray(value, field);
|
|
5860
|
+
if (!list2.every((v) => HEX64.test(v)))
|
|
5594
5861
|
throw new UnreadableManifestError(`"${field}" holds a value that is not a hash`);
|
|
5595
|
-
return
|
|
5862
|
+
return list2;
|
|
5596
5863
|
}
|
|
5597
5864
|
function bool(value, field) {
|
|
5598
5865
|
if (typeof value !== "boolean")
|
|
@@ -5824,6 +6091,448 @@ function union(a, b) {
|
|
|
5824
6091
|
return [.../* @__PURE__ */ new Set([...a, ...b])];
|
|
5825
6092
|
}
|
|
5826
6093
|
|
|
6094
|
+
// dist/scan.js
|
|
6095
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6096
|
+
import { join as join4 } from "node:path";
|
|
6097
|
+
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
6098
|
+
var HALFCYCLE_STATE_NOTE = [
|
|
6099
|
+
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
6100
|
+
"record of what this project has done, and it is never updated as work happens.",
|
|
6101
|
+
'An empty or missing "layers" means the install-time scan found no pre-existing',
|
|
6102
|
+
"artefacts on disk \u2014 it does NOT mean nothing has been done since, and it is not",
|
|
6103
|
+
"evidence that any step is unfinished or any approval is outstanding.",
|
|
6104
|
+
"The authoritative position lives in the Halfcycle service: call the",
|
|
6105
|
+
"halfcycle_resolve tool to ask where this project stands. Do not answer that",
|
|
6106
|
+
"question from this file."
|
|
6107
|
+
].join(" ");
|
|
6108
|
+
var LAYER_ARTEFACT_PATHS = {
|
|
6109
|
+
l0: ["ENGAGEMENT.md"],
|
|
6110
|
+
l1: ["docs/product-spec.md"],
|
|
6111
|
+
l2: ["docs/architecture-overview.md"],
|
|
6112
|
+
l3: ["docs/invariants.md"],
|
|
6113
|
+
l4: ["docs/features"],
|
|
6114
|
+
l5: ["docs/phases"]
|
|
6115
|
+
};
|
|
6116
|
+
function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6117
|
+
const layers = {};
|
|
6118
|
+
for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
|
|
6119
|
+
const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
|
|
6120
|
+
if (found.length > 0) {
|
|
6121
|
+
layers[layer] = {
|
|
6122
|
+
value: "asserted",
|
|
6123
|
+
source: "bootstrap-scan",
|
|
6124
|
+
foundPaths: found,
|
|
6125
|
+
scannedAt
|
|
6126
|
+
};
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
return { format: HALFCYCLE_STATE_FORMAT, note: HALFCYCLE_STATE_NOTE, layers };
|
|
6130
|
+
}
|
|
6131
|
+
var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
|
|
6132
|
+
function planBootstrapScan(targetRepo) {
|
|
6133
|
+
const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
|
|
6134
|
+
if (existsSync2(statePath)) {
|
|
6135
|
+
const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
|
|
6136
|
+
return { state: existing, text: null };
|
|
6137
|
+
}
|
|
6138
|
+
const state = scanLayers(targetRepo);
|
|
6139
|
+
return { state, text: JSON.stringify(state, null, 2) + "\n" };
|
|
6140
|
+
}
|
|
6141
|
+
function writeBootstrapState(targetRepo, text) {
|
|
6142
|
+
mkdirSync4(join4(targetRepo, ".halfcycle"), { recursive: true });
|
|
6143
|
+
writeFileSync4(join4(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
|
|
6144
|
+
}
|
|
6145
|
+
|
|
6146
|
+
// dist/install-plan.js
|
|
6147
|
+
var PENDING = /* @__PURE__ */ Symbol("pending");
|
|
6148
|
+
var YOURS = "a file of yours with the same name; Halfcycle will not touch it";
|
|
6149
|
+
var SAME = "already as Halfcycle would write it";
|
|
6150
|
+
var PREVIEW_WORDS = Object.freeze({
|
|
6151
|
+
command: {
|
|
6152
|
+
add: "a command your coding agent can run to take the next Halfcycle step",
|
|
6153
|
+
change: "a newer version of this Halfcycle command",
|
|
6154
|
+
leave: SAME,
|
|
6155
|
+
yours: YOURS
|
|
6156
|
+
},
|
|
6157
|
+
"check-script": {
|
|
6158
|
+
add: "a check Halfcycle runs before and after your agent's actions",
|
|
6159
|
+
change: "a newer version of this Halfcycle check",
|
|
6160
|
+
leave: SAME,
|
|
6161
|
+
yours: YOURS
|
|
6162
|
+
},
|
|
6163
|
+
settings: {
|
|
6164
|
+
add: "your coding agent's settings, with the checks Halfcycle runs",
|
|
6165
|
+
change: "adds the lines Halfcycle needs; your own lines stay as they are",
|
|
6166
|
+
leave: "already has the lines Halfcycle needs",
|
|
6167
|
+
yours: YOURS
|
|
6168
|
+
},
|
|
6169
|
+
"server-entry": {
|
|
6170
|
+
add: "tells your coding agent how to reach Halfcycle's service",
|
|
6171
|
+
change: "adds Halfcycle's entry; your own entries stay as they are",
|
|
6172
|
+
leave: "already has Halfcycle's entry",
|
|
6173
|
+
yours: YOURS
|
|
6174
|
+
},
|
|
6175
|
+
"ignore-list": {
|
|
6176
|
+
add: "keeps Halfcycle's files for this computer only out of git",
|
|
6177
|
+
change: "adds the lines Halfcycle needs; your own lines stay as they are",
|
|
6178
|
+
leave: "already has the lines Halfcycle needs",
|
|
6179
|
+
yours: YOURS
|
|
6180
|
+
},
|
|
6181
|
+
"line-endings": {
|
|
6182
|
+
add: "keeps Halfcycle's checks working in a Windows copy of this project",
|
|
6183
|
+
change: "adds the lines Halfcycle needs; your own lines stay as they are",
|
|
6184
|
+
leave: "already has the lines Halfcycle needs",
|
|
6185
|
+
yours: YOURS
|
|
6186
|
+
},
|
|
6187
|
+
"halfcycle-folder": {
|
|
6188
|
+
add: "a file in Halfcycle's own folder",
|
|
6189
|
+
change: "a newer version of this file in Halfcycle's own folder",
|
|
6190
|
+
leave: SAME,
|
|
6191
|
+
yours: YOURS
|
|
6192
|
+
},
|
|
6193
|
+
runner: {
|
|
6194
|
+
add: "the program Halfcycle's checks run",
|
|
6195
|
+
change: "a newer version of the program Halfcycle's checks run",
|
|
6196
|
+
leave: SAME,
|
|
6197
|
+
yours: YOURS
|
|
6198
|
+
},
|
|
6199
|
+
"captured-index": {
|
|
6200
|
+
add: "the list of real examples your project's tests are checked against",
|
|
6201
|
+
change: "a newer version of this list",
|
|
6202
|
+
leave: SAME,
|
|
6203
|
+
yours: YOURS
|
|
6204
|
+
},
|
|
6205
|
+
"old-sign-in": {
|
|
6206
|
+
add: "an older Halfcycle sign-in",
|
|
6207
|
+
change: "takes out an older Halfcycle sign-in; your own lines stay as they are",
|
|
6208
|
+
leave: "an older Halfcycle sign-in that cannot be moved yet; left as it is",
|
|
6209
|
+
yours: YOURS
|
|
6210
|
+
}
|
|
6211
|
+
});
|
|
6212
|
+
var kindTable;
|
|
6213
|
+
function kinds() {
|
|
6214
|
+
kindTable ??= /* @__PURE__ */ new Map([
|
|
6215
|
+
[".claude/hooks/guard-runner.sh", "check-script"],
|
|
6216
|
+
[".claude/hooks/user-prompt-reminder.sh", "check-script"],
|
|
6217
|
+
[".claude/hooks/user-prompt-scope.sh", "check-script"],
|
|
6218
|
+
[".claude/hooks/user-prompt-findings.sh", "check-script"],
|
|
6219
|
+
[".claude/hooks/session-end-marker.sh", "check-script"],
|
|
6220
|
+
[".claude/hooks/session-start-marker.sh", "check-script"],
|
|
6221
|
+
[SETTINGS_REL, "settings"],
|
|
6222
|
+
[MCP_REGISTRATION_REL, "server-entry"],
|
|
6223
|
+
[GITIGNORE_REL, "ignore-list"],
|
|
6224
|
+
[GITATTRIBUTES_REL, "line-endings"],
|
|
6225
|
+
[VENDORED_BIN_REL, "runner"],
|
|
6226
|
+
[CREW_ROSTER_REL, "halfcycle-folder"],
|
|
6227
|
+
[BUNDLE_PIN_REL, "halfcycle-folder"],
|
|
6228
|
+
[PROJECT_IDENTITY_REL, "halfcycle-folder"],
|
|
6229
|
+
[BOOTSTRAP_STATE_REL, "halfcycle-folder"],
|
|
6230
|
+
[INSTALL_MANIFEST_REL, "halfcycle-folder"],
|
|
6231
|
+
[RETIRED_MCP_HEADERS_HELPER_REL, "halfcycle-folder"],
|
|
6232
|
+
[LEGACY_CI_STANZA_REL, "halfcycle-folder"],
|
|
6233
|
+
[CAPTURED_INDEX_REL, "captured-index"],
|
|
6234
|
+
[LEGACY_ENV_LOCAL_REL, "old-sign-in"]
|
|
6235
|
+
]);
|
|
6236
|
+
return kindTable;
|
|
6237
|
+
}
|
|
6238
|
+
function previewKindOf(rel) {
|
|
6239
|
+
if (/^\.claude\/commands\/[^/]+\.md$/.test(rel))
|
|
6240
|
+
return "command";
|
|
6241
|
+
return kinds().get(rel);
|
|
6242
|
+
}
|
|
6243
|
+
function kindOf(rel) {
|
|
6244
|
+
const kind = previewKindOf(rel);
|
|
6245
|
+
if (kind === void 0)
|
|
6246
|
+
throw new Error(`[halfcycle] the installer plans ${rel}, which has no preview words.`);
|
|
6247
|
+
return kind;
|
|
6248
|
+
}
|
|
6249
|
+
var STEPS = /* @__PURE__ */ new WeakMap();
|
|
6250
|
+
function planInstall(options) {
|
|
6251
|
+
const { targetRepo, engagementId, engagementType, credential, home, env, createdByAccountId } = options;
|
|
6252
|
+
const preview = engagementId === void 0;
|
|
6253
|
+
if (!preview) {
|
|
6254
|
+
assertEngagementId(engagementId);
|
|
6255
|
+
if (engagementType === void 0) {
|
|
6256
|
+
throw new Error("[halfcycle] a plan for a project needs its kind as well as its id.");
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
if (!existsSync3(targetRepo)) {
|
|
6260
|
+
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
6261
|
+
}
|
|
6262
|
+
const writeSet = closedWriteSet();
|
|
6263
|
+
const previous = readPreviousManifest(targetRepo);
|
|
6264
|
+
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
6265
|
+
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
6266
|
+
const vendored = planVendoredBinary(targetRepo);
|
|
6267
|
+
let settings;
|
|
6268
|
+
try {
|
|
6269
|
+
settings = planSettings(targetRepo, previous, home, env);
|
|
6270
|
+
} catch (err) {
|
|
6271
|
+
if (err instanceof SettingsParseRefused)
|
|
6272
|
+
return Object.freeze({ entries: [], outside: [], refusal: err.message });
|
|
6273
|
+
throw err;
|
|
6274
|
+
}
|
|
6275
|
+
const hooks = planHookScripts(targetRepo);
|
|
6276
|
+
const ciStanza = planLegacyCiStanzaRemoval(targetRepo);
|
|
6277
|
+
const mcp = planMcpRegistration(targetRepo, preview ? "pending" : credential);
|
|
6278
|
+
const mcpHelper = mcp.kind === "none" ? null : planMcpHeadersHelperRemoval(targetRepo);
|
|
6279
|
+
const gitignore = planGitignore(targetRepo);
|
|
6280
|
+
const gitattributes = planGitattributes(targetRepo);
|
|
6281
|
+
const identity = planProjectIdentity(targetRepo);
|
|
6282
|
+
if (identity.after === null)
|
|
6283
|
+
readFileSync5(join5(targetRepo, PROJECT_IDENTITY_REL));
|
|
6284
|
+
const credentialPlan = !preview && credential !== void 0 ? planEngagementCredential(credential, engagementId, home) : null;
|
|
6285
|
+
const credentialWrite = credentialPlan === null ? null : { outcome: credentialPlan.outcome, existed: credentialPlan.existed, path: engagementEnvPath(engagementId, home) };
|
|
6286
|
+
const storedAfter = credentialPlan?.storedAfter ?? (preview ? Object.fromEntries(ENGAGEMENT_ENV_KEYS.map((key) => [key, "supplied at sign-in"])) : readEngagementEnv(engagementId, home) ?? {});
|
|
6287
|
+
const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
|
|
6288
|
+
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
|
|
6289
|
+
const crew = planCrewRoster(targetRepo);
|
|
6290
|
+
const scan = { text: planBootstrapScan(targetRepo).text };
|
|
6291
|
+
const steps = {
|
|
6292
|
+
targetRepo,
|
|
6293
|
+
version,
|
|
6294
|
+
engagementId,
|
|
6295
|
+
engagementType,
|
|
6296
|
+
credential: preview ? void 0 : credential,
|
|
6297
|
+
home,
|
|
6298
|
+
writeSet,
|
|
6299
|
+
previous,
|
|
6300
|
+
dirsBefore,
|
|
6301
|
+
commands,
|
|
6302
|
+
vendored,
|
|
6303
|
+
settings,
|
|
6304
|
+
hooks,
|
|
6305
|
+
ciStanza,
|
|
6306
|
+
mcp,
|
|
6307
|
+
mcpHelper,
|
|
6308
|
+
gitignore,
|
|
6309
|
+
gitattributes,
|
|
6310
|
+
identity,
|
|
6311
|
+
credentialWrite,
|
|
6312
|
+
legacy,
|
|
6313
|
+
pin,
|
|
6314
|
+
crew,
|
|
6315
|
+
scan
|
|
6316
|
+
};
|
|
6317
|
+
const record2 = preview ? null : planInstallRecord(targetRepo, accountInstall(steps, predicted(steps)).manifestText);
|
|
6318
|
+
const plan = Object.freeze({
|
|
6319
|
+
entries: Object.freeze(entriesOf(steps, record2)),
|
|
6320
|
+
outside: Object.freeze(outsideOf(steps))
|
|
6321
|
+
});
|
|
6322
|
+
STEPS.set(plan, steps);
|
|
6323
|
+
return plan;
|
|
6324
|
+
}
|
|
6325
|
+
function predicted(steps) {
|
|
6326
|
+
const after = /* @__PURE__ */ new Map();
|
|
6327
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6328
|
+
const lands = (rel, text, writes) => {
|
|
6329
|
+
after.set(rel, text);
|
|
6330
|
+
if (!writes)
|
|
6331
|
+
return;
|
|
6332
|
+
for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir))
|
|
6333
|
+
dirs.add(dir);
|
|
6334
|
+
};
|
|
6335
|
+
const file = (planned) => {
|
|
6336
|
+
if (planned.outcome !== "collided")
|
|
6337
|
+
lands(planned.rel, planned.text, planned.outcome === "written");
|
|
6338
|
+
};
|
|
6339
|
+
steps.commands.forEach(file);
|
|
6340
|
+
file(steps.vendored);
|
|
6341
|
+
file(steps.settings.file);
|
|
6342
|
+
steps.hooks.forEach(file);
|
|
6343
|
+
if (steps.mcp.kind === "write")
|
|
6344
|
+
lands(MCP_REGISTRATION_REL, steps.mcp.text, steps.mcp.outcome !== "skipped");
|
|
6345
|
+
if (steps.identity.after !== null)
|
|
6346
|
+
lands(PROJECT_IDENTITY_REL, steps.identity.after, steps.identity.action === "add");
|
|
6347
|
+
if (steps.pin !== null)
|
|
6348
|
+
file(steps.pin);
|
|
6349
|
+
file(steps.crew);
|
|
6350
|
+
if (steps.scan.text !== null)
|
|
6351
|
+
lands(BOOTSTRAP_STATE_REL, steps.scan.text, true);
|
|
6352
|
+
const mergeAfter = (planned) => planned.outcome === "written" ? planned.after : planned.before;
|
|
6353
|
+
return {
|
|
6354
|
+
ciStanza: steps.ciStanza,
|
|
6355
|
+
mcpHelper: steps.mcpHelper,
|
|
6356
|
+
gitignore: steps.gitignore.outcome,
|
|
6357
|
+
gitignoreAfter: mergeAfter(steps.gitignore),
|
|
6358
|
+
gitattributes: steps.gitattributes.outcome,
|
|
6359
|
+
gitattributesAfter: mergeAfter(steps.gitattributes),
|
|
6360
|
+
credential: steps.credentialWrite?.outcome ?? null,
|
|
6361
|
+
legacy: steps.legacy,
|
|
6362
|
+
bytesAfter: (rel) => {
|
|
6363
|
+
const text = after.get(rel);
|
|
6364
|
+
if (text === void 0)
|
|
6365
|
+
throw new Error(`[halfcycle] the plan has no bytes for ${rel}.`);
|
|
6366
|
+
return Buffer.from(text, "utf-8");
|
|
6367
|
+
},
|
|
6368
|
+
dirExistsAfter: (dir) => steps.dirsBefore.has(dir) || dirs.has(dir),
|
|
6369
|
+
stateExistsAfter: true
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6372
|
+
function entriesOf(steps, record2) {
|
|
6373
|
+
const { targetRepo } = steps;
|
|
6374
|
+
const entries = [];
|
|
6375
|
+
const fromFile = (planned) => {
|
|
6376
|
+
const kind = kindOf(planned.rel);
|
|
6377
|
+
if (planned.outcome === "written") {
|
|
6378
|
+
entries.push({ path: planned.rel, action: planned.replacedExisting ? "change" : "add", kind, bytes: planned.text });
|
|
6379
|
+
} else if (planned.outcome === "collided") {
|
|
6380
|
+
entries.push({ path: planned.rel, action: "leave", kind, bytes: null, yours: true });
|
|
6381
|
+
} else {
|
|
6382
|
+
entries.push({ path: planned.rel, action: "leave", kind, bytes: null });
|
|
6383
|
+
}
|
|
6384
|
+
};
|
|
6385
|
+
const retired = (rel, decided) => {
|
|
6386
|
+
if (decided === null || decided === "absent")
|
|
6387
|
+
return;
|
|
6388
|
+
if (decided === "removed")
|
|
6389
|
+
entries.push({ path: rel, action: "remove", kind: kindOf(rel), bytes: null });
|
|
6390
|
+
else if (decided === "kept-foreign")
|
|
6391
|
+
entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null, yours: true });
|
|
6392
|
+
else
|
|
6393
|
+
entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null });
|
|
6394
|
+
};
|
|
6395
|
+
const merge = (rel, planned) => {
|
|
6396
|
+
if (planned.outcome === "written" && planned.after !== null) {
|
|
6397
|
+
entries.push({ path: rel, action: planned.before === null ? "add" : "change", kind: kindOf(rel), bytes: planned.after });
|
|
6398
|
+
} else if (planned.before !== null || planned.outcome === "failed") {
|
|
6399
|
+
entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null });
|
|
6400
|
+
}
|
|
6401
|
+
};
|
|
6402
|
+
const pending = (rel) => {
|
|
6403
|
+
const there = existsSync3(join5(targetRepo, rel));
|
|
6404
|
+
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
6405
|
+
};
|
|
6406
|
+
steps.commands.forEach(fromFile);
|
|
6407
|
+
fromFile(steps.vendored);
|
|
6408
|
+
const settings = steps.settings;
|
|
6409
|
+
entries.push(settings.file.outcome === "written" ? { path: SETTINGS_REL, action: settings.preexisted ? "change" : "add", kind: "settings", bytes: settings.file.text } : { path: SETTINGS_REL, action: "leave", kind: "settings", bytes: null });
|
|
6410
|
+
steps.hooks.forEach(fromFile);
|
|
6411
|
+
retired(LEGACY_CI_STANZA_REL, steps.ciStanza);
|
|
6412
|
+
const mcp = steps.mcp;
|
|
6413
|
+
if (mcp.kind === "write") {
|
|
6414
|
+
const action = mcp.outcome === "written" ? "add" : mcp.outcome === "merged" ? "change" : "leave";
|
|
6415
|
+
entries.push({ path: MCP_REGISTRATION_REL, action, kind: "server-entry", bytes: action === "leave" ? null : mcp.text });
|
|
6416
|
+
} else if (mcp.kind === "pending") {
|
|
6417
|
+
pending(MCP_REGISTRATION_REL);
|
|
6418
|
+
} else {
|
|
6419
|
+
entries.push({ path: MCP_REGISTRATION_REL, action: "leave", kind: "server-entry", bytes: null });
|
|
6420
|
+
}
|
|
6421
|
+
retired(RETIRED_MCP_HEADERS_HELPER_REL, steps.mcpHelper);
|
|
6422
|
+
merge(GITIGNORE_REL, steps.gitignore);
|
|
6423
|
+
merge(GITATTRIBUTES_REL, steps.gitattributes);
|
|
6424
|
+
const identity = steps.identity;
|
|
6425
|
+
entries.push({
|
|
6426
|
+
path: PROJECT_IDENTITY_REL,
|
|
6427
|
+
action: identity.action,
|
|
6428
|
+
kind: "halfcycle-folder",
|
|
6429
|
+
bytes: identity.action === "leave" ? null : identity.after
|
|
6430
|
+
});
|
|
6431
|
+
const legacy = steps.legacy;
|
|
6432
|
+
if (legacy.outcome === "removed") {
|
|
6433
|
+
entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "remove", kind: "old-sign-in", bytes: null });
|
|
6434
|
+
} else if (legacy.outcome === "stripped") {
|
|
6435
|
+
entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "change", kind: "old-sign-in", bytes: legacy.keptText ?? null });
|
|
6436
|
+
} else if (legacy.outcome !== "absent") {
|
|
6437
|
+
entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "leave", kind: "old-sign-in", bytes: null });
|
|
6438
|
+
}
|
|
6439
|
+
if (steps.pin === null)
|
|
6440
|
+
pending(BUNDLE_PIN_REL);
|
|
6441
|
+
else
|
|
6442
|
+
fromFile(steps.pin);
|
|
6443
|
+
fromFile(steps.crew);
|
|
6444
|
+
entries.push(steps.scan.text !== null ? { path: BOOTSTRAP_STATE_REL, action: "add", kind: "halfcycle-folder", bytes: steps.scan.text } : { path: BOOTSTRAP_STATE_REL, action: "leave", kind: "halfcycle-folder", bytes: null });
|
|
6445
|
+
if (record2 === null)
|
|
6446
|
+
pending(INSTALL_MANIFEST_REL);
|
|
6447
|
+
else
|
|
6448
|
+
fromFile(record2);
|
|
6449
|
+
return entries;
|
|
6450
|
+
}
|
|
6451
|
+
function outsideOf(steps) {
|
|
6452
|
+
if (steps.engagementId === void 0) {
|
|
6453
|
+
const outside = [{ kind: "sign-in", path: null, action: "pending" }];
|
|
6454
|
+
if (!projectPinned(steps.targetRepo))
|
|
6455
|
+
outside.push({ kind: "new-project" });
|
|
6456
|
+
return outside;
|
|
6457
|
+
}
|
|
6458
|
+
const write = steps.credentialWrite;
|
|
6459
|
+
if (write === null)
|
|
6460
|
+
return [];
|
|
6461
|
+
const action = write.outcome === "skipped" ? "leave" : write.existed ? "change" : "add";
|
|
6462
|
+
return [{ kind: "sign-in", path: write.path, action }];
|
|
6463
|
+
}
|
|
6464
|
+
function projectPinned(targetRepo) {
|
|
6465
|
+
try {
|
|
6466
|
+
const id = readBundlePin(targetRepo)?.engagementId;
|
|
6467
|
+
return typeof id === "string" && id.trim() !== "";
|
|
6468
|
+
} catch {
|
|
6469
|
+
return false;
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
function applyInstallPlan(plan) {
|
|
6473
|
+
if (plan.refusal !== void 0)
|
|
6474
|
+
throw new SettingsParseRefused(plan.refusal);
|
|
6475
|
+
const steps = STEPS.get(plan);
|
|
6476
|
+
if (steps === void 0)
|
|
6477
|
+
throw new Error("[halfcycle] only a plan made by planInstall can be applied.");
|
|
6478
|
+
const { targetRepo, engagementId, pin } = steps;
|
|
6479
|
+
if (engagementId === void 0 || pin === null || steps.mcp.kind === "pending") {
|
|
6480
|
+
throw new Error("[halfcycle] a preview cannot be applied: it is still waiting for the service.");
|
|
6481
|
+
}
|
|
6482
|
+
for (const command of steps.commands)
|
|
6483
|
+
putPlannedFile(targetRepo, command);
|
|
6484
|
+
putPlannedFile(targetRepo, steps.vendored);
|
|
6485
|
+
putPlannedFile(targetRepo, steps.settings.file);
|
|
6486
|
+
for (const hook of steps.hooks)
|
|
6487
|
+
putPlannedFile(targetRepo, hook);
|
|
6488
|
+
const ciStanza = steps.ciStanza === "removed" ? removePlannedRetiredFile(targetRepo, LEGACY_CI_STANZA_REL) : steps.ciStanza;
|
|
6489
|
+
putPlannedMcp(targetRepo, steps.mcp);
|
|
6490
|
+
const mcpHelper = steps.mcpHelper === "removed" ? removePlannedRetiredFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL) : steps.mcpHelper;
|
|
6491
|
+
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
6492
|
+
const gitignorePath = join5(targetRepo, GITIGNORE_REL);
|
|
6493
|
+
const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
|
|
6494
|
+
const gitattributes = putPlannedGitattributes(targetRepo, steps.gitattributes);
|
|
6495
|
+
const gitattributesAfter = readTextOrNull(join5(targetRepo, GITATTRIBUTES_REL));
|
|
6496
|
+
writeProjectIdentity(targetRepo, steps.identity);
|
|
6497
|
+
const credential = steps.credential;
|
|
6498
|
+
const credentialOutcome = steps.credentialWrite !== null && credential !== void 0 ? writeEngagementCredential(credential, engagementId, steps.home) : null;
|
|
6499
|
+
const stored = readEngagementEnv(engagementId, steps.home) ?? {};
|
|
6500
|
+
const legacy = applyLegacyMigration(targetRepo, migrateLegacyEnvLocal(targetRepo, stored));
|
|
6501
|
+
writeBundlePin(targetRepo, pin, []);
|
|
6502
|
+
putPlannedFile(targetRepo, steps.crew);
|
|
6503
|
+
if (steps.scan.text !== null)
|
|
6504
|
+
writeBootstrapState(targetRepo, steps.scan.text);
|
|
6505
|
+
const { report, installManifest, manifestText } = accountInstall(steps, {
|
|
6506
|
+
ciStanza,
|
|
6507
|
+
mcpHelper,
|
|
6508
|
+
gitignore,
|
|
6509
|
+
gitignoreAfter,
|
|
6510
|
+
gitattributes,
|
|
6511
|
+
gitattributesAfter,
|
|
6512
|
+
credential: credentialOutcome,
|
|
6513
|
+
legacy,
|
|
6514
|
+
bytesAfter: (rel) => readFileSync5(join5(targetRepo, rel)),
|
|
6515
|
+
dirExistsAfter: (dir) => existsSync3(join5(targetRepo, dir)),
|
|
6516
|
+
stateExistsAfter: existsSync3(join5(targetRepo, BOOTSTRAP_STATE_REL))
|
|
6517
|
+
});
|
|
6518
|
+
const record2 = planInstallRecord(targetRepo, manifestText);
|
|
6519
|
+
putPlannedFile(targetRepo, record2);
|
|
6520
|
+
recordInstallRecord(report, record2);
|
|
6521
|
+
const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
|
|
6522
|
+
return {
|
|
6523
|
+
version: steps.version,
|
|
6524
|
+
writtenPaths: report.writtenPaths,
|
|
6525
|
+
skippedPaths: report.skippedPaths,
|
|
6526
|
+
mergedPaths: report.mergedPaths,
|
|
6527
|
+
collidedPaths: report.collidedPaths,
|
|
6528
|
+
editedCollidedPaths: report.editedCollidedPaths,
|
|
6529
|
+
replacedPaths: report.replacedPaths,
|
|
6530
|
+
bootstrapScanRan: steps.scan.text !== null,
|
|
6531
|
+
legacyCredentialKept: legacy.outcome === "kept" ? { keys: legacy.unbacked, missing: legacy.missingForAdoption } : null,
|
|
6532
|
+
notShared
|
|
6533
|
+
};
|
|
6534
|
+
}
|
|
6535
|
+
|
|
5827
6536
|
// dist/mcp-endpoint.js
|
|
5828
6537
|
var MCP_ENDPOINT_PATH = "/mcp";
|
|
5829
6538
|
function mcpEndpointUrl(origin) {
|
|
@@ -5903,56 +6612,6 @@ function mergePermissions(existing, generated, retiredOurs) {
|
|
|
5903
6612
|
return result;
|
|
5904
6613
|
}
|
|
5905
6614
|
|
|
5906
|
-
// dist/scan.js
|
|
5907
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5908
|
-
import { join as join4 } from "node:path";
|
|
5909
|
-
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
5910
|
-
var HALFCYCLE_STATE_NOTE = [
|
|
5911
|
-
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
5912
|
-
"record of what this project has done, and it is never updated as work happens.",
|
|
5913
|
-
'An empty or missing "layers" means the install-time scan found no pre-existing',
|
|
5914
|
-
"artefacts on disk \u2014 it does NOT mean nothing has been done since, and it is not",
|
|
5915
|
-
"evidence that any step is unfinished or any approval is outstanding.",
|
|
5916
|
-
"The authoritative position lives in the Halfcycle service: call the",
|
|
5917
|
-
"halfcycle_resolve tool to ask where this project stands. Do not answer that",
|
|
5918
|
-
"question from this file."
|
|
5919
|
-
].join(" ");
|
|
5920
|
-
var LAYER_ARTEFACT_PATHS = {
|
|
5921
|
-
l0: ["ENGAGEMENT.md"],
|
|
5922
|
-
l1: ["docs/product-spec.md"],
|
|
5923
|
-
l2: ["docs/architecture-overview.md"],
|
|
5924
|
-
l3: ["docs/invariants.md"],
|
|
5925
|
-
l4: ["docs/features"],
|
|
5926
|
-
l5: ["docs/phases"]
|
|
5927
|
-
};
|
|
5928
|
-
function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5929
|
-
const layers = {};
|
|
5930
|
-
for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
|
|
5931
|
-
const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
|
|
5932
|
-
if (found.length > 0) {
|
|
5933
|
-
layers[layer] = {
|
|
5934
|
-
value: "asserted",
|
|
5935
|
-
source: "bootstrap-scan",
|
|
5936
|
-
foundPaths: found,
|
|
5937
|
-
scannedAt
|
|
5938
|
-
};
|
|
5939
|
-
}
|
|
5940
|
-
}
|
|
5941
|
-
return { format: HALFCYCLE_STATE_FORMAT, note: HALFCYCLE_STATE_NOTE, layers };
|
|
5942
|
-
}
|
|
5943
|
-
var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
|
|
5944
|
-
function runBootstrapScan(targetRepo) {
|
|
5945
|
-
const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
|
|
5946
|
-
if (existsSync2(statePath)) {
|
|
5947
|
-
const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
|
|
5948
|
-
return { state: existing, ran: false };
|
|
5949
|
-
}
|
|
5950
|
-
const state = scanLayers(targetRepo);
|
|
5951
|
-
mkdirSync4(join4(targetRepo, ".halfcycle"), { recursive: true });
|
|
5952
|
-
writeFileSync4(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
5953
|
-
return { state, ran: true };
|
|
5954
|
-
}
|
|
5955
|
-
|
|
5956
6615
|
// dist/install.js
|
|
5957
6616
|
function renderNotShared(finding) {
|
|
5958
6617
|
const rule = `rule "${finding.pattern}" in ${finding.source}, line ${finding.line}`;
|
|
@@ -5967,36 +6626,36 @@ var __dirname = dirname3(__filename);
|
|
|
5967
6626
|
function findBundleRoot(startDir) {
|
|
5968
6627
|
let candidate = startDir;
|
|
5969
6628
|
for (let i = 0; i < 4; i++) {
|
|
5970
|
-
const manifest =
|
|
6629
|
+
const manifest = join6(candidate, ".claude-plugin", "plugin.json");
|
|
5971
6630
|
try {
|
|
5972
|
-
if (
|
|
6631
|
+
if (existsSync4(manifest))
|
|
5973
6632
|
return candidate;
|
|
5974
6633
|
} catch {
|
|
5975
6634
|
}
|
|
5976
6635
|
candidate = dirname3(candidate);
|
|
5977
6636
|
}
|
|
5978
|
-
return
|
|
6637
|
+
return join6(startDir, "..");
|
|
5979
6638
|
}
|
|
5980
6639
|
var BUNDLE_ROOT = findBundleRoot(__dirname);
|
|
5981
6640
|
function resolveVendoredBinary() {
|
|
5982
6641
|
const candidates = [
|
|
5983
|
-
|
|
5984
|
-
|
|
6642
|
+
join6(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
|
|
6643
|
+
join6(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
|
|
5985
6644
|
];
|
|
5986
6645
|
for (const candidate of candidates) {
|
|
5987
|
-
if (
|
|
6646
|
+
if (existsSync4(candidate))
|
|
5988
6647
|
return candidate;
|
|
5989
6648
|
}
|
|
5990
6649
|
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(", ")}.`);
|
|
5991
6650
|
}
|
|
5992
6651
|
function resolvePreFilterShell() {
|
|
5993
6652
|
const candidates = [
|
|
5994
|
-
|
|
5995
|
-
|
|
6653
|
+
join6(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
|
|
6654
|
+
join6(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
|
|
5996
6655
|
];
|
|
5997
6656
|
for (const candidate of candidates) {
|
|
5998
|
-
if (
|
|
5999
|
-
return
|
|
6657
|
+
if (existsSync4(candidate))
|
|
6658
|
+
return readFileSync6(candidate, "utf-8");
|
|
6000
6659
|
}
|
|
6001
6660
|
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(", ")}.`);
|
|
6002
6661
|
}
|
|
@@ -6025,7 +6684,7 @@ function isAllowlisted(targetRelPath) {
|
|
|
6025
6684
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
6026
6685
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
6027
6686
|
function capturedIndexScaffold() {
|
|
6028
|
-
return
|
|
6687
|
+
return join6(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
|
|
6029
6688
|
}
|
|
6030
6689
|
var GITIGNORE_REL = ".gitignore";
|
|
6031
6690
|
var GITATTRIBUTES_REL = ".gitattributes";
|
|
@@ -6063,7 +6722,7 @@ function closedWriteSet() {
|
|
|
6063
6722
|
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
6064
6723
|
const dirs = /* @__PURE__ */ new Set();
|
|
6065
6724
|
for (const path of [...files, ...merged]) {
|
|
6066
|
-
for (let dir =
|
|
6725
|
+
for (let dir = posix2.dirname(path); dir !== "."; dir = posix2.dirname(dir))
|
|
6067
6726
|
dirs.add(dir);
|
|
6068
6727
|
}
|
|
6069
6728
|
const ownedDirs = new Set([...dirs].filter((dir) => `${dir}/`.startsWith(INSTALLER_OWNED_DIR)));
|
|
@@ -6087,11 +6746,11 @@ function closedWriteSet() {
|
|
|
6087
6746
|
function sha256OfWhatThisVersionWrites(commands) {
|
|
6088
6747
|
const out = /* @__PURE__ */ new Map();
|
|
6089
6748
|
const fromFile = (rel, src) => {
|
|
6090
|
-
if (
|
|
6091
|
-
out.set(rel, fileSha256(
|
|
6749
|
+
if (existsSync4(src))
|
|
6750
|
+
out.set(rel, fileSha256(readFileSync6(src)));
|
|
6092
6751
|
};
|
|
6093
6752
|
for (const cmd of commands)
|
|
6094
|
-
fromFile(commandStubRel(cmd.name),
|
|
6753
|
+
fromFile(commandStubRel(cmd.name), join6(BUNDLE_ROOT, ".claude-plugin", cmd.path));
|
|
6095
6754
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
6096
6755
|
out.set(rel, fileSha256(content));
|
|
6097
6756
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
@@ -6104,17 +6763,17 @@ function bytesAreOurs(path, bytes, recorded, writeSet) {
|
|
|
6104
6763
|
}
|
|
6105
6764
|
function readPreviousManifest(targetRepo) {
|
|
6106
6765
|
try {
|
|
6107
|
-
return parseManifest(
|
|
6766
|
+
return parseManifest(readFileSync6(join6(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
|
|
6108
6767
|
} catch {
|
|
6109
6768
|
return null;
|
|
6110
6769
|
}
|
|
6111
6770
|
}
|
|
6112
|
-
function noteFile(draft,
|
|
6771
|
+
function noteFile(draft, rel, outcome, after) {
|
|
6113
6772
|
if (outcome === "collided") {
|
|
6114
6773
|
recordCollision(draft, rel);
|
|
6115
6774
|
return;
|
|
6116
6775
|
}
|
|
6117
|
-
recordInstalledFile(draft, rel,
|
|
6776
|
+
recordInstalledFile(draft, rel, after(rel));
|
|
6118
6777
|
}
|
|
6119
6778
|
function recordSettingsMerge(draft, existing, generated, preexisted, merged) {
|
|
6120
6779
|
const generatedHooks = generated.hooks ?? {};
|
|
@@ -6203,11 +6862,11 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
6203
6862
|
const collided = new Set(collidedPaths);
|
|
6204
6863
|
const dirVerdicts = /* @__PURE__ */ new Map();
|
|
6205
6864
|
const travelsThroughPlainDirs = (rel) => {
|
|
6206
|
-
for (let dir =
|
|
6865
|
+
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
6207
6866
|
let plain = dirVerdicts.get(dir);
|
|
6208
6867
|
if (plain === void 0) {
|
|
6209
|
-
const abs =
|
|
6210
|
-
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(
|
|
6868
|
+
const abs = join6(targetRepo, dir);
|
|
6869
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join6(abs, ".git")) === null;
|
|
6211
6870
|
dirVerdicts.set(dir, plain);
|
|
6212
6871
|
}
|
|
6213
6872
|
if (!plain)
|
|
@@ -6221,7 +6880,7 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
6221
6880
|
...merged,
|
|
6222
6881
|
INSTALL_MANIFEST_REL
|
|
6223
6882
|
])
|
|
6224
|
-
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(
|
|
6883
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join6(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
6225
6884
|
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
6226
6885
|
if (ignored === null)
|
|
6227
6886
|
return null;
|
|
@@ -6244,7 +6903,7 @@ function lstatOrNull(path) {
|
|
|
6244
6903
|
}
|
|
6245
6904
|
function readFileOrNull(path) {
|
|
6246
6905
|
try {
|
|
6247
|
-
return
|
|
6906
|
+
return readFileSync6(path, "utf-8");
|
|
6248
6907
|
} catch {
|
|
6249
6908
|
return null;
|
|
6250
6909
|
}
|
|
@@ -6253,28 +6912,33 @@ function isPlainObject2(value) {
|
|
|
6253
6912
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6254
6913
|
}
|
|
6255
6914
|
function readPluginManifest() {
|
|
6256
|
-
const manifestPath =
|
|
6257
|
-
const raw =
|
|
6915
|
+
const manifestPath = join6(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
|
|
6916
|
+
const raw = readFileSync6(manifestPath, "utf-8");
|
|
6258
6917
|
return JSON.parse(raw);
|
|
6259
6918
|
}
|
|
6260
|
-
function
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6919
|
+
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
6920
|
+
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6921
|
+
return (manifest.commands ?? []).map((cmd) => {
|
|
6922
|
+
const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
6923
|
+
if (!existsSync4(srcFile)) {
|
|
6264
6924
|
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.`);
|
|
6265
6925
|
}
|
|
6266
6926
|
const rel = commandStubRel(cmd.name);
|
|
6267
|
-
const
|
|
6268
|
-
const
|
|
6269
|
-
const
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6927
|
+
const content = readFileSync6(srcFile, "utf-8");
|
|
6928
|
+
const recorded = recordedFiles.get(rel);
|
|
6929
|
+
const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
6930
|
+
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
6931
|
+
});
|
|
6932
|
+
}
|
|
6933
|
+
function putPlannedFile(targetRepoRoot, planned) {
|
|
6934
|
+
if (planned.outcome !== "written")
|
|
6935
|
+
return;
|
|
6936
|
+
if (!isAllowlisted(planned.rel)) {
|
|
6937
|
+
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${planned.rel}". Only the method surface and scaffolding paths are writable.`);
|
|
6277
6938
|
}
|
|
6939
|
+
const target = join6(targetRepoRoot, planned.rel);
|
|
6940
|
+
mkdirSync5(dirname3(target), { recursive: true });
|
|
6941
|
+
writeFileSync5(target, planned.text, "utf-8");
|
|
6278
6942
|
}
|
|
6279
6943
|
function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
|
|
6280
6944
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
@@ -6287,71 +6951,66 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
|
|
|
6287
6951
|
writeFileSync5(targetAbsPath, content, "utf-8");
|
|
6288
6952
|
writtenPaths.push(rel);
|
|
6289
6953
|
}
|
|
6290
|
-
function
|
|
6954
|
+
function decideCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
|
|
6291
6955
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
6292
6956
|
if (!isAllowlisted(rel)) {
|
|
6293
6957
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
6294
6958
|
}
|
|
6295
|
-
|
|
6296
|
-
const current = existsSync3(targetAbsPath) ? readFileSync5(targetAbsPath) : null;
|
|
6959
|
+
const current = existsSync4(targetAbsPath) ? readFileSync6(targetAbsPath) : null;
|
|
6297
6960
|
const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
|
|
6298
|
-
if (current
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
replacedExisting
|
|
6304
|
-
}
|
|
6305
|
-
mkdirSync5(dirname3(targetAbsPath), { recursive: true });
|
|
6306
|
-
writeFileSync5(targetAbsPath, text, "utf-8");
|
|
6307
|
-
return { outcome: "written", replacedExisting };
|
|
6961
|
+
if (current === null)
|
|
6962
|
+
return { rel, text, outcome: "written", replacedExisting: false };
|
|
6963
|
+
if (current.toString("utf-8") === text)
|
|
6964
|
+
return { rel, text, outcome: "skipped", replacedExisting: false };
|
|
6965
|
+
if (!isOurs(current))
|
|
6966
|
+
return { rel, text, outcome: "collided", replacedExisting: false };
|
|
6967
|
+
return { rel, text, outcome: "written", replacedExisting: true };
|
|
6308
6968
|
}
|
|
6309
6969
|
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
6310
6970
|
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
6971
|
+
const planned = decideOwned(targetAbsPath, targetRepoRoot, content);
|
|
6972
|
+
putPlannedFile(targetRepoRoot, planned);
|
|
6973
|
+
return planned.outcome;
|
|
6974
|
+
}
|
|
6975
|
+
function decideOwned(targetAbsPath, targetRepoRoot, content) {
|
|
6311
6976
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
6312
6977
|
if (!isAllowlisted(rel)) {
|
|
6313
6978
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
6314
6979
|
}
|
|
6315
6980
|
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
6316
|
-
throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (
|
|
6981
|
+
throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (decideOwnedGenerated) rather than assumed from where it sits.`);
|
|
6317
6982
|
}
|
|
6318
6983
|
const current = readFileOrNull(targetAbsPath);
|
|
6319
6984
|
const text = inLineEndingOf(current, content);
|
|
6320
6985
|
if (current === text)
|
|
6321
|
-
return "skipped";
|
|
6322
|
-
|
|
6323
|
-
writeFileSync5(targetAbsPath, text, "utf-8");
|
|
6324
|
-
return "written";
|
|
6986
|
+
return { rel, text, outcome: "skipped", replacedExisting: false };
|
|
6987
|
+
return { rel, text, outcome: "written", replacedExisting: existsSync4(targetAbsPath) };
|
|
6325
6988
|
}
|
|
6326
6989
|
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
6327
|
-
function
|
|
6990
|
+
function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
6328
6991
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
6329
6992
|
if (!isAllowlisted(rel)) {
|
|
6330
6993
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
6331
6994
|
}
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
replacedExisting = true;
|
|
6995
|
+
if (!existsSync4(targetAbsPath))
|
|
6996
|
+
return { rel, text: content, outcome: "written", replacedExisting: false };
|
|
6997
|
+
const current = readFileSync6(targetAbsPath, "utf-8");
|
|
6998
|
+
if (current === content)
|
|
6999
|
+
return { rel, text: content, outcome: "skipped", replacedExisting: false };
|
|
7000
|
+
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
7001
|
+
if (!headerLine.startsWith(generatedMarker)) {
|
|
7002
|
+
return { rel, text: content, outcome: "collided", replacedExisting: false };
|
|
6341
7003
|
}
|
|
6342
|
-
|
|
6343
|
-
writeFileSync5(targetAbsPath, content, "utf-8");
|
|
6344
|
-
return { outcome: "written", replacedExisting };
|
|
7004
|
+
return { rel, text: content, outcome: "written", replacedExisting: true };
|
|
6345
7005
|
}
|
|
6346
|
-
function recordOwnedGenerated(report,
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
return outcome;
|
|
7006
|
+
function recordOwnedGenerated(report, planned) {
|
|
7007
|
+
record(report, planned.outcome, planned.rel);
|
|
7008
|
+
if (planned.replacedExisting)
|
|
7009
|
+
report.replacedPaths.push(planned.rel);
|
|
7010
|
+
return planned.outcome;
|
|
6352
7011
|
}
|
|
6353
7012
|
function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
|
|
6354
|
-
const existedBefore =
|
|
7013
|
+
const existedBefore = existsSync4(targetAbsPath);
|
|
6355
7014
|
const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
|
|
6356
7015
|
record(report, outcome, rel);
|
|
6357
7016
|
if (outcome === "written" && existedBefore)
|
|
@@ -6373,8 +7032,8 @@ var DENY_PATTERNS = [
|
|
|
6373
7032
|
];
|
|
6374
7033
|
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.";
|
|
6375
7034
|
function readExistingSettings(targetRepo) {
|
|
6376
|
-
const settingsPath =
|
|
6377
|
-
return
|
|
7035
|
+
const settingsPath = join6(targetRepo, SETTINGS_REL);
|
|
7036
|
+
return existsSync4(settingsPath) ? JSON.parse(readFileSync6(settingsPath, "utf-8")) : void 0;
|
|
6378
7037
|
}
|
|
6379
7038
|
var SettingsParseRefused = class extends Error {
|
|
6380
7039
|
constructor(message) {
|
|
@@ -6383,11 +7042,11 @@ var SettingsParseRefused = class extends Error {
|
|
|
6383
7042
|
}
|
|
6384
7043
|
};
|
|
6385
7044
|
function settingsParseRefusal(targetRepo, err) {
|
|
6386
|
-
const file =
|
|
7045
|
+
const file = join6(targetRepo, SETTINGS_REL);
|
|
6387
7046
|
const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
|
|
6388
7047
|
let where = "";
|
|
6389
7048
|
if (offset !== void 0) {
|
|
6390
|
-
const before =
|
|
7049
|
+
const before = readFileSync6(file, "utf-8").slice(0, Number(offset)).split("\n");
|
|
6391
7050
|
where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
|
|
6392
7051
|
}
|
|
6393
7052
|
return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
|
|
@@ -6398,18 +7057,18 @@ var HALFCYCLE_STATUS_LINE = Object.freeze({
|
|
|
6398
7057
|
command: 'bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard-runner.sh statusline'
|
|
6399
7058
|
});
|
|
6400
7059
|
function statusLineSetElsewhere(targetRepo, home = homedir2(), env = process.env) {
|
|
6401
|
-
const places = [
|
|
7060
|
+
const places = [join6(targetRepo, ".claude", "settings.local.json"), join6(home, ".claude", "settings.json")];
|
|
6402
7061
|
const configDir = env["CLAUDE_CONFIG_DIR"];
|
|
6403
7062
|
if (configDir !== void 0 && configDir !== "") {
|
|
6404
|
-
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ?
|
|
6405
|
-
places.push(
|
|
7063
|
+
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join6(home, configDir.slice(2)) : configDir;
|
|
7064
|
+
places.push(join6(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
|
|
6406
7065
|
}
|
|
6407
7066
|
return places.some((path) => {
|
|
6408
|
-
if (!
|
|
7067
|
+
if (!existsSync4(path))
|
|
6409
7068
|
return false;
|
|
6410
7069
|
let value;
|
|
6411
7070
|
try {
|
|
6412
|
-
value = JSON.parse(
|
|
7071
|
+
value = JSON.parse(readFileSync6(path, "utf-8"));
|
|
6413
7072
|
} catch {
|
|
6414
7073
|
return true;
|
|
6415
7074
|
}
|
|
@@ -6646,6 +7305,10 @@ ${GENERATED_SESSION_END_HEADER} the Halfcycle installer.
|
|
|
6646
7305
|
# written at session start. Deletion happens HERE and not at stop time: the stop
|
|
6647
7306
|
# hook runs at the end of every turn, and deleting the marker there would throw
|
|
6648
7307
|
# away the session's starting point for every later turn.
|
|
7308
|
+
#
|
|
7309
|
+
# Also removes the two small files this session may have left behind for its
|
|
7310
|
+
# checks to hand findings across turns, both named by this session's id alone,
|
|
7311
|
+
# with no fallback name to remove when there is none.
|
|
6649
7312
|
INPUT="$(cat 2>/dev/null || true)"
|
|
6650
7313
|
|
|
6651
7314
|
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
@@ -6666,6 +7329,8 @@ rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.eval" 2>/dev/null || true
|
|
|
6666
7329
|
if [ -n "$SESSION_ID" ]; then
|
|
6667
7330
|
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref" 2>/dev/null || true
|
|
6668
7331
|
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.eval" 2>/dev/null || true
|
|
7332
|
+
rm -f "\${MARKER_DIR}/halfcycle-findings-\${SESSION_ID}.json" 2>/dev/null || true
|
|
7333
|
+
rm -f "\${MARKER_DIR}/halfcycle-own-\${SESSION_ID}.jsonl" 2>/dev/null || true
|
|
6669
7334
|
fi
|
|
6670
7335
|
exit 0
|
|
6671
7336
|
`;
|
|
@@ -6725,19 +7390,20 @@ function generateUserPromptScopeHook() {
|
|
|
6725
7390
|
return `#!/usr/bin/env bash
|
|
6726
7391
|
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review.
|
|
6727
7392
|
#
|
|
6728
|
-
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
6729
|
-
# decide one part of a phase's close review,
|
|
6730
|
-
#
|
|
6731
|
-
#
|
|
6732
|
-
# "
|
|
7393
|
+
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
7394
|
+
# decide one part of a phase's close review, or turn builds running on their
|
|
7395
|
+
# own on or off, by typing one line as the whole prompt in their own Claude
|
|
7396
|
+
# Code session: "approve <phase>", "send back <phase>: <note>", "accept
|
|
7397
|
+
# <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
|
|
7398
|
+
# <words>", "drop <phase>/<part>: <words>", "autonomy on" or "autonomy off".
|
|
6733
7399
|
#
|
|
6734
7400
|
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
6735
7401
|
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
6736
7402
|
# any whitespace (typed, or escaped in the input), with "approve ", "send back ",
|
|
6737
|
-
# "accept ", "question ", "carry " or "
|
|
6738
|
-
# output, and Node never starts. A quote inside any other field arrives
|
|
6739
|
-
# so no other field (the folder the session is in, for one) can read as
|
|
6740
|
-
# start of the prompt.
|
|
7403
|
+
# "accept ", "question ", "carry ", "drop " or "autonomy ", in any case, it exits
|
|
7404
|
+
# 0 with no output, and Node never starts. A quote inside any other field arrives
|
|
7405
|
+
# escaped, so no other field (the folder the session is in, for one) can read as
|
|
7406
|
+
# the start of the prompt.
|
|
6741
7407
|
#
|
|
6742
7408
|
# Otherwise it finds the project root from its own location, never from the
|
|
6743
7409
|
# folder the session has moved to, and hands the same input to the guard
|
|
@@ -6758,7 +7424,7 @@ IFS= read -r -d '' HC_INPUT || true
|
|
|
6758
7424
|
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
6759
7425
|
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
6760
7426
|
HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
|
|
6761
|
-
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop) "; then
|
|
7427
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|autonomy) "; then
|
|
6762
7428
|
exit 0
|
|
6763
7429
|
fi
|
|
6764
7430
|
|
|
@@ -6771,16 +7437,62 @@ set +e
|
|
|
6771
7437
|
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
6772
7438
|
`;
|
|
6773
7439
|
}
|
|
6774
|
-
var
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
7440
|
+
var GENERATED_USER_PROMPT_FINDINGS_HEADER = "# Findings hand-over \u2014 generated by";
|
|
7441
|
+
function generateUserPromptFindingsHook() {
|
|
7442
|
+
return `#!/usr/bin/env bash
|
|
7443
|
+
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer.
|
|
7444
|
+
#
|
|
7445
|
+
# At the person's next message, before the agent starts, hands it the details of
|
|
7446
|
+
# whatever Halfcycle's checks found at an earlier turn end, once.
|
|
7447
|
+
#
|
|
7448
|
+
# AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
|
|
7449
|
+
# script reads its session id with a bash regex (no node) and exits 0 with no
|
|
7450
|
+
# output once it finds no file \u2014 node and grep never start.
|
|
7451
|
+
trap 'exit 0' ERR
|
|
7452
|
+
set -e
|
|
7453
|
+
|
|
7454
|
+
HC_INPUT=""
|
|
7455
|
+
IFS= read -r -d '' HC_INPUT || true
|
|
7456
|
+
|
|
7457
|
+
# The session id, read with a bash regex held in a variable (a quoted operand to
|
|
7458
|
+
# =~ is read as a literal string, not a pattern). An id holding anything outside
|
|
7459
|
+
# [A-Za-z0-9-] cannot close the match and leaves this empty, which is read below
|
|
7460
|
+
# exactly like no session id at all.
|
|
7461
|
+
HC_SESSION_RE='"session_id"[[:space:]]*:[[:space:]]*"([A-Za-z0-9-]+)"'
|
|
7462
|
+
HC_SESSION_ID=""
|
|
7463
|
+
if [[ "$HC_INPUT" =~ $HC_SESSION_RE ]]; then
|
|
7464
|
+
HC_SESSION_ID="\${BASH_REMATCH[1]}"
|
|
7465
|
+
fi
|
|
7466
|
+
if [ -z "$HC_SESSION_ID" ]; then
|
|
7467
|
+
exit 0
|
|
7468
|
+
fi
|
|
7469
|
+
|
|
7470
|
+
HC_FINDINGS_DIR="\${TMPDIR:-/tmp}"
|
|
7471
|
+
HC_FINDINGS_DIR="\${HC_FINDINGS_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
7472
|
+
HC_FINDINGS_FILE="\${HC_FINDINGS_DIR}/halfcycle-findings-\${HC_SESSION_ID}.json"
|
|
7473
|
+
if [ ! -f "$HC_FINDINGS_FILE" ]; then
|
|
7474
|
+
exit 0
|
|
7475
|
+
fi
|
|
7476
|
+
|
|
7477
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
7478
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
7479
|
+
export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
7480
|
+
|
|
7481
|
+
trap - ERR
|
|
7482
|
+
set +e
|
|
7483
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT"
|
|
7484
|
+
`;
|
|
7485
|
+
}
|
|
7486
|
+
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
7487
|
+
"b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
|
|
7488
|
+
"674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
|
|
7489
|
+
"d17bde608a6fec54a63cb934a28d88acc8babc205334b5ee84d4ed6ee219d340",
|
|
7490
|
+
"85e71d3f6551977b241a28468f121ee4d1c4edef40a2c4dc1aa6f2f737ebd037"
|
|
7491
|
+
];
|
|
7492
|
+
function isTrackedAndClean(targetRepo, relPath) {
|
|
7493
|
+
try {
|
|
7494
|
+
execFileSync2("git", ["-C", targetRepo, "ls-files", "--error-unmatch", "--", relPath], {
|
|
7495
|
+
stdio: "ignore",
|
|
6784
7496
|
env: gitEnvForNamedRepo()
|
|
6785
7497
|
});
|
|
6786
7498
|
} catch {
|
|
@@ -6797,15 +7509,19 @@ function isTrackedAndClean(targetRepo, relPath) {
|
|
|
6797
7509
|
return true;
|
|
6798
7510
|
}
|
|
6799
7511
|
function removeLegacyCiStanza(targetRepo) {
|
|
6800
|
-
return removeRetiredGeneratedFile(targetRepo,
|
|
7512
|
+
return removeRetiredGeneratedFile(targetRepo, LEGACY_CI_STANZA_REL, 0, CI_STANZA_KNOWN_HEADER_HASHES);
|
|
6801
7513
|
}
|
|
6802
7514
|
function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
6803
|
-
const
|
|
6804
|
-
|
|
7515
|
+
const decided = decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes);
|
|
7516
|
+
return decided === "removed" ? removePlannedRetiredFile(targetRepo, rel) : decided;
|
|
7517
|
+
}
|
|
7518
|
+
function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
7519
|
+
const path = join6(targetRepo, rel);
|
|
7520
|
+
if (!existsSync4(path))
|
|
6805
7521
|
return "absent";
|
|
6806
7522
|
let raw;
|
|
6807
7523
|
try {
|
|
6808
|
-
raw =
|
|
7524
|
+
raw = readFileSync6(path, "utf-8");
|
|
6809
7525
|
} catch {
|
|
6810
7526
|
return "failed";
|
|
6811
7527
|
}
|
|
@@ -6815,13 +7531,20 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
|
|
|
6815
7531
|
return "kept-foreign";
|
|
6816
7532
|
if (!isTrackedAndClean(targetRepo, rel))
|
|
6817
7533
|
return "kept-uncommitted";
|
|
7534
|
+
return "removed";
|
|
7535
|
+
}
|
|
7536
|
+
function removePlannedRetiredFile(targetRepo, rel) {
|
|
6818
7537
|
try {
|
|
6819
|
-
rmSync2(
|
|
7538
|
+
rmSync2(join6(targetRepo, rel));
|
|
6820
7539
|
} catch {
|
|
6821
7540
|
return "failed";
|
|
6822
7541
|
}
|
|
6823
7542
|
return "removed";
|
|
6824
7543
|
}
|
|
7544
|
+
var LEGACY_CI_STANZA_REL = ".halfcycle/ci-stanza.yml";
|
|
7545
|
+
function planLegacyCiStanzaRemoval(targetRepo) {
|
|
7546
|
+
return decideRetiredRemoval(targetRepo, LEGACY_CI_STANZA_REL, 0, CI_STANZA_KNOWN_HEADER_HASHES);
|
|
7547
|
+
}
|
|
6825
7548
|
var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
6826
7549
|
`const p=require('path'),f=require('fs'),c=require('child_process');`,
|
|
6827
7550
|
`let d=process.cwd();`,
|
|
@@ -6840,13 +7563,14 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
|
6840
7563
|
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
6841
7564
|
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
6842
7565
|
];
|
|
6843
|
-
function
|
|
6844
|
-
return
|
|
7566
|
+
function planMcpHeadersHelperRemoval(targetRepo) {
|
|
7567
|
+
return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
|
|
6845
7568
|
}
|
|
6846
7569
|
var OWNED_GENERATED_HEADERS = {
|
|
6847
7570
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
6848
7571
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
6849
7572
|
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
7573
|
+
".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
|
|
6850
7574
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
6851
7575
|
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
6852
7576
|
};
|
|
@@ -6856,6 +7580,7 @@ function hookScriptsThisVersionWrites() {
|
|
|
6856
7580
|
[".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
|
|
6857
7581
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
6858
7582
|
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
7583
|
+
[".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
|
|
6859
7584
|
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
6860
7585
|
];
|
|
6861
7586
|
}
|
|
@@ -6909,23 +7634,26 @@ function gitignoreCovers(content) {
|
|
|
6909
7634
|
const lines = new Set(content.split("\n").map((l) => l.trim()));
|
|
6910
7635
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
6911
7636
|
}
|
|
6912
|
-
function
|
|
6913
|
-
const gitignorePath =
|
|
7637
|
+
function planGitignore(targetRepo) {
|
|
7638
|
+
const gitignorePath = join6(targetRepo, GITIGNORE_REL);
|
|
6914
7639
|
const header = GITIGNORE_HEADER;
|
|
7640
|
+
const before = existsSync4(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
|
|
7641
|
+
if (before === null) {
|
|
7642
|
+
return { before, after: [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n", outcome: "written" };
|
|
7643
|
+
}
|
|
7644
|
+
if (gitignoreCovers(before))
|
|
7645
|
+
return { before, after: before, outcome: "skipped" };
|
|
7646
|
+
const existingLines = new Set(before.split("\n").map((l) => l.trim()));
|
|
7647
|
+
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
7648
|
+
const eol = lineEndingOf(before);
|
|
7649
|
+
const suffix = (before.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
7650
|
+
return { before, after: before + suffix, outcome: "written" };
|
|
7651
|
+
}
|
|
7652
|
+
function putPlannedGitignore(targetRepo, planned) {
|
|
7653
|
+
if (planned.outcome !== "written" || planned.after === null)
|
|
7654
|
+
return planned.outcome;
|
|
6915
7655
|
try {
|
|
6916
|
-
|
|
6917
|
-
const body = [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n";
|
|
6918
|
-
writeFileSync5(gitignorePath, body, "utf-8");
|
|
6919
|
-
return "written";
|
|
6920
|
-
}
|
|
6921
|
-
const existing = readFileSync5(gitignorePath, "utf-8");
|
|
6922
|
-
if (gitignoreCovers(existing))
|
|
6923
|
-
return "skipped";
|
|
6924
|
-
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
6925
|
-
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
6926
|
-
const eol = lineEndingOf(existing);
|
|
6927
|
-
const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
6928
|
-
writeFileSync5(gitignorePath, existing + suffix, "utf-8");
|
|
7656
|
+
writeFileSync5(join6(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
|
|
6929
7657
|
return "written";
|
|
6930
7658
|
} catch {
|
|
6931
7659
|
return "failed";
|
|
@@ -6934,7 +7662,7 @@ function reconcileGitignore(targetRepo) {
|
|
|
6934
7662
|
var GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
|
|
6935
7663
|
function readTextOrNull(path) {
|
|
6936
7664
|
try {
|
|
6937
|
-
return
|
|
7665
|
+
return readFileSync6(path, "utf-8");
|
|
6938
7666
|
} catch {
|
|
6939
7667
|
return null;
|
|
6940
7668
|
}
|
|
@@ -6943,26 +7671,31 @@ function lfRules(paths) {
|
|
|
6943
7671
|
return [...paths].filter((path) => path.endsWith(".sh")).map((path) => `${path} text eol=lf`);
|
|
6944
7672
|
}
|
|
6945
7673
|
var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
|
|
6946
|
-
function
|
|
6947
|
-
const path =
|
|
7674
|
+
function planGitattributes(targetRepo) {
|
|
7675
|
+
const path = join6(targetRepo, GITATTRIBUTES_REL);
|
|
6948
7676
|
const rules = REQUIRED_GITATTRIBUTES_RULES;
|
|
6949
|
-
const
|
|
7677
|
+
const before = readTextOrNull(path);
|
|
7678
|
+
if (lstatOrNull(path)?.isSymbolicLink() === true)
|
|
7679
|
+
return { before, after: before, outcome: "failed" };
|
|
7680
|
+
if (!existsSync4(path)) {
|
|
7681
|
+
return { before, after: [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", outcome: "written" };
|
|
7682
|
+
}
|
|
7683
|
+
if (before === null)
|
|
7684
|
+
return { before, after: null, outcome: "failed" };
|
|
7685
|
+
const present = new Set(before.split("\n").map((l) => l.trim()));
|
|
7686
|
+
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
7687
|
+
if (toAdd.length === 0)
|
|
7688
|
+
return { before, after: before, outcome: "skipped" };
|
|
7689
|
+
const eol = lineEndingOf(before);
|
|
7690
|
+
const separator = before === "" ? "" : before.endsWith("\n") ? eol : eol + eol;
|
|
7691
|
+
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
|
|
7692
|
+
return { before, after: before + suffix, outcome: "written" };
|
|
7693
|
+
}
|
|
7694
|
+
function putPlannedGitattributes(targetRepo, planned) {
|
|
7695
|
+
if (planned.outcome !== "written" || planned.after === null)
|
|
7696
|
+
return planned.outcome;
|
|
6950
7697
|
try {
|
|
6951
|
-
|
|
6952
|
-
return "failed";
|
|
6953
|
-
if (!existsSync3(path)) {
|
|
6954
|
-
writeAllowlisted(path, targetRepo, [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", routed);
|
|
6955
|
-
return "written";
|
|
6956
|
-
}
|
|
6957
|
-
const existing = readFileSync5(path, "utf-8");
|
|
6958
|
-
const present = new Set(existing.split("\n").map((l) => l.trim()));
|
|
6959
|
-
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
6960
|
-
if (toAdd.length === 0)
|
|
6961
|
-
return "skipped";
|
|
6962
|
-
const eol = lineEndingOf(existing);
|
|
6963
|
-
const separator = existing === "" ? "" : existing.endsWith("\n") ? eol : eol + eol;
|
|
6964
|
-
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
|
|
6965
|
-
writeAllowlisted(path, targetRepo, existing + suffix, routed);
|
|
7698
|
+
writeAllowlisted(join6(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
|
|
6966
7699
|
return "written";
|
|
6967
7700
|
} catch {
|
|
6968
7701
|
return "failed";
|
|
@@ -6981,7 +7714,7 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
6981
7714
|
function nonEmpty(value) {
|
|
6982
7715
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
6983
7716
|
}
|
|
6984
|
-
function
|
|
7717
|
+
function planBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion) {
|
|
6985
7718
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
6986
7719
|
const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
|
|
6987
7720
|
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
@@ -6996,9 +7729,39 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
|
|
|
6996
7729
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
6997
7730
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
6998
7731
|
};
|
|
6999
|
-
const
|
|
7000
|
-
const
|
|
7001
|
-
|
|
7732
|
+
const current = readFileOrNull(join6(targetRepoRoot, BUNDLE_PIN_REL));
|
|
7733
|
+
const text = inLineEndingOf(current, JSON.stringify(pin, null, 2) + "\n");
|
|
7734
|
+
return {
|
|
7735
|
+
rel: BUNDLE_PIN_REL,
|
|
7736
|
+
text,
|
|
7737
|
+
outcome: current === text ? "skipped" : "written",
|
|
7738
|
+
replacedExisting: current !== null && current !== text
|
|
7739
|
+
};
|
|
7740
|
+
}
|
|
7741
|
+
function planBundlePinPreview(targetRepoRoot, version, home) {
|
|
7742
|
+
let existing;
|
|
7743
|
+
try {
|
|
7744
|
+
existing = readBundlePin(targetRepoRoot);
|
|
7745
|
+
} catch {
|
|
7746
|
+
return null;
|
|
7747
|
+
}
|
|
7748
|
+
if (existing === null || nonEmpty(existing.engagementId) === void 0 || existing.engagementType === void 0) {
|
|
7749
|
+
return null;
|
|
7750
|
+
}
|
|
7751
|
+
let pinned;
|
|
7752
|
+
try {
|
|
7753
|
+
pinned = readPinnedEngagement(targetRepoRoot, home);
|
|
7754
|
+
} catch {
|
|
7755
|
+
return null;
|
|
7756
|
+
}
|
|
7757
|
+
if (pinned === null || pinned.engagementId !== existing.engagementId || pinned.credential === void 0) {
|
|
7758
|
+
return null;
|
|
7759
|
+
}
|
|
7760
|
+
return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0);
|
|
7761
|
+
}
|
|
7762
|
+
function writeBundlePin(targetRepoRoot, planned, writtenPaths) {
|
|
7763
|
+
const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
|
|
7764
|
+
writeAllowlisted(pinPath, targetRepoRoot, planned.text, writtenPaths);
|
|
7002
7765
|
}
|
|
7003
7766
|
function updatePinnedMethodVersion(repoRoot, engagementId, version) {
|
|
7004
7767
|
let pin;
|
|
@@ -7009,7 +7772,7 @@ function updatePinnedMethodVersion(repoRoot, engagementId, version) {
|
|
|
7009
7772
|
}
|
|
7010
7773
|
if (pin === null || pin.engagementId !== engagementId)
|
|
7011
7774
|
return;
|
|
7012
|
-
const pinPath =
|
|
7775
|
+
const pinPath = join6(repoRoot, BUNDLE_PIN_REL);
|
|
7013
7776
|
const updated = { ...pin, methodVersion: version };
|
|
7014
7777
|
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(updated, null, 2) + "\n");
|
|
7015
7778
|
try {
|
|
@@ -7029,17 +7792,17 @@ function renderCrewRoster() {
|
|
|
7029
7792
|
}
|
|
7030
7793
|
function writeCrewRoster(targetRepoRoot, report) {
|
|
7031
7794
|
const rendered = renderCrewRoster();
|
|
7032
|
-
const crewPath =
|
|
7795
|
+
const crewPath = join6(targetRepoRoot, CREW_ROSTER_REL);
|
|
7033
7796
|
return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
|
|
7034
7797
|
}
|
|
7035
7798
|
function pinCreatedByAccountId(pin) {
|
|
7036
7799
|
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
7037
7800
|
}
|
|
7038
7801
|
function readBundlePin(targetRepoRoot) {
|
|
7039
|
-
const pinPath =
|
|
7040
|
-
if (!
|
|
7802
|
+
const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
|
|
7803
|
+
if (!existsSync4(pinPath))
|
|
7041
7804
|
return null;
|
|
7042
|
-
return JSON.parse(
|
|
7805
|
+
return JSON.parse(readFileSync6(pinPath, "utf-8"));
|
|
7043
7806
|
}
|
|
7044
7807
|
function readPinnedEngagement(targetRepoRoot, home) {
|
|
7045
7808
|
const pin = readBundlePin(targetRepoRoot);
|
|
@@ -7062,7 +7825,7 @@ function readPinnedEngagement(targetRepoRoot, home) {
|
|
|
7062
7825
|
}
|
|
7063
7826
|
function readLegacyEnvLocal(targetRepoRoot) {
|
|
7064
7827
|
try {
|
|
7065
|
-
return parseEnvText(
|
|
7828
|
+
return parseEnvText(readFileSync6(join6(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
|
|
7066
7829
|
} catch {
|
|
7067
7830
|
return null;
|
|
7068
7831
|
}
|
|
@@ -7100,15 +7863,32 @@ function checkDrift(targetRepoRoot) {
|
|
|
7100
7863
|
const current = manifest.version;
|
|
7101
7864
|
return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
|
|
7102
7865
|
}
|
|
7103
|
-
function writeProjectIdentity(targetRepo) {
|
|
7104
|
-
if (
|
|
7105
|
-
|
|
7106
|
-
return "skipped";
|
|
7866
|
+
function writeProjectIdentity(targetRepo, planned, probe = false) {
|
|
7867
|
+
if (planned.action === "add" || probe) {
|
|
7868
|
+
createProjectIdentityFile(targetRepo, planned.after ?? void 0);
|
|
7107
7869
|
}
|
|
7108
|
-
|
|
7109
|
-
|
|
7870
|
+
if (planned.action === "change" && planned.after !== null)
|
|
7871
|
+
replaceProjectIdentityFile(targetRepo, planned.after);
|
|
7872
|
+
return planned.action === "add" ? "written" : "skipped";
|
|
7110
7873
|
}
|
|
7111
7874
|
function writeEngagementCredential(credential, engagementId, home) {
|
|
7875
|
+
return writeEngagementEnv(engagementId, engagementCredentialValues(credential, engagementId), home);
|
|
7876
|
+
}
|
|
7877
|
+
function planEngagementCredential(credential, engagementId, home) {
|
|
7878
|
+
let existing;
|
|
7879
|
+
try {
|
|
7880
|
+
existing = readFileSync6(engagementEnvPath(engagementId, home), "utf-8");
|
|
7881
|
+
} catch {
|
|
7882
|
+
existing = null;
|
|
7883
|
+
}
|
|
7884
|
+
const reconciled = reconcileEnvText2(existing, engagementCredentialValues(credential, engagementId));
|
|
7885
|
+
return {
|
|
7886
|
+
outcome: existing === reconciled ? "skipped" : "written",
|
|
7887
|
+
existed: existing !== null,
|
|
7888
|
+
storedAfter: parseEnvText(reconciled)
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
function engagementCredentialValues(credential, engagementId) {
|
|
7112
7892
|
const values = {
|
|
7113
7893
|
HALFCYCLE_SERVICE_URL: credential.serviceUrl,
|
|
7114
7894
|
// T-22 — the second origin, persisted for the developer and for any tool that
|
|
@@ -7136,16 +7916,16 @@ function writeEngagementCredential(credential, engagementId, home) {
|
|
|
7136
7916
|
// telemetry origin, same as every other optional field on `DeviceCredential`.
|
|
7137
7917
|
CONTROL_TELEMETRY_URL: credential.controlTelemetryUrl ?? ""
|
|
7138
7918
|
};
|
|
7139
|
-
return
|
|
7919
|
+
return values;
|
|
7140
7920
|
}
|
|
7141
7921
|
var LEGACY_ENV_LOCAL_REL = ".env.local";
|
|
7142
7922
|
function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
7143
|
-
const path =
|
|
7144
|
-
if (!
|
|
7923
|
+
const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
7924
|
+
if (!existsSync4(path))
|
|
7145
7925
|
return { outcome: "absent", unbacked: [], missingForAdoption: [] };
|
|
7146
7926
|
let raw;
|
|
7147
7927
|
try {
|
|
7148
|
-
raw =
|
|
7928
|
+
raw = readFileSync6(path, "utf-8");
|
|
7149
7929
|
} catch {
|
|
7150
7930
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
7151
7931
|
}
|
|
@@ -7166,24 +7946,91 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
7166
7946
|
return !ours.has(line.slice(0, eq).replace(/^\s*export\s+/, "").trim());
|
|
7167
7947
|
});
|
|
7168
7948
|
const substantive = kept.filter((line) => line.trim() !== "" && !line.trim().startsWith("# Halfcycle"));
|
|
7949
|
+
if (substantive.length === 0)
|
|
7950
|
+
return { outcome: "removed", unbacked: [], missingForAdoption: [] };
|
|
7951
|
+
return { outcome: "stripped", unbacked: [], missingForAdoption: [], keptText: kept.join("\n") };
|
|
7952
|
+
}
|
|
7953
|
+
function applyLegacyMigration(targetRepo, decided) {
|
|
7954
|
+
const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
7169
7955
|
try {
|
|
7170
|
-
if (
|
|
7956
|
+
if (decided.outcome === "removed") {
|
|
7171
7957
|
rmSync2(path);
|
|
7172
|
-
return
|
|
7958
|
+
return decided;
|
|
7959
|
+
}
|
|
7960
|
+
if (decided.outcome === "stripped" && decided.keptText !== void 0) {
|
|
7961
|
+
writeFileSync5(path, decided.keptText, "utf-8");
|
|
7962
|
+
return decided;
|
|
7173
7963
|
}
|
|
7174
|
-
|
|
7175
|
-
return { outcome: "stripped", unbacked: [], missingForAdoption: [] };
|
|
7964
|
+
return decided;
|
|
7176
7965
|
} catch {
|
|
7177
7966
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
7178
7967
|
}
|
|
7179
7968
|
}
|
|
7180
7969
|
async function install(options) {
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
7185
|
-
}
|
|
7970
|
+
return applyInstallPlan(planInstall(options));
|
|
7971
|
+
}
|
|
7972
|
+
function planCommandStubs(targetRepo, previous, writeSet) {
|
|
7186
7973
|
const manifest = readPluginManifest();
|
|
7974
|
+
return { version: manifest.version, commands: planManifestCommands(manifest, targetRepo, previous, writeSet) };
|
|
7975
|
+
}
|
|
7976
|
+
function planVendoredBinary(targetRepo) {
|
|
7977
|
+
return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
|
|
7978
|
+
}
|
|
7979
|
+
function planSettings(targetRepo, previous, home, env) {
|
|
7980
|
+
const settingsPath = join6(targetRepo, SETTINGS_REL);
|
|
7981
|
+
const preexisted = existsSync4(settingsPath);
|
|
7982
|
+
const generated = JSON.parse(generateSettingsJson());
|
|
7983
|
+
let existing;
|
|
7984
|
+
try {
|
|
7985
|
+
existing = readExistingSettings(targetRepo) ?? {};
|
|
7986
|
+
} catch (err) {
|
|
7987
|
+
throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
|
|
7988
|
+
}
|
|
7989
|
+
const retiredOurs = retiredRulesThisInstallAdded(previous);
|
|
7990
|
+
const statusLine = statusLineSetElsewhere(targetRepo, home, env) ? void 0 : HALFCYCLE_STATUS_LINE;
|
|
7991
|
+
const merged = mergeSettings(existing, generated, retiredOurs, statusLine);
|
|
7992
|
+
const current = readFileOrNull(settingsPath);
|
|
7993
|
+
const text = inLineEndingOf(current, JSON.stringify(merged, null, 2) + "\n");
|
|
7994
|
+
const file = {
|
|
7995
|
+
rel: SETTINGS_REL,
|
|
7996
|
+
text,
|
|
7997
|
+
// Identical bytes are not rewritten; the report still names the file as merged or
|
|
7998
|
+
// written by whether it was there, as it always has.
|
|
7999
|
+
outcome: current === text ? "skipped" : "written",
|
|
8000
|
+
replacedExisting: false
|
|
8001
|
+
};
|
|
8002
|
+
return { file, preexisted, existing, generated, merged, retiredOurs };
|
|
8003
|
+
}
|
|
8004
|
+
function planHookScripts(targetRepo) {
|
|
8005
|
+
return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join6(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
|
|
8006
|
+
}
|
|
8007
|
+
function planMcpRegistration(targetRepo, credential) {
|
|
8008
|
+
const mcpPath = join6(targetRepo, MCP_REGISTRATION_REL);
|
|
8009
|
+
const existing = existsSync4(mcpPath) ? readFileSync6(mcpPath, "utf-8") : null;
|
|
8010
|
+
if (credential === void 0)
|
|
8011
|
+
return { kind: "none", existing };
|
|
8012
|
+
if (credential === "pending")
|
|
8013
|
+
return { kind: "pending", existing };
|
|
8014
|
+
const text = inLineEndingOf(existing, generateMcpRegistration(existing, credential.mcpUrl));
|
|
8015
|
+
const outcome = existing === null ? "written" : existing !== text ? "merged" : "skipped";
|
|
8016
|
+
return { kind: "write", existing, text, outcome };
|
|
8017
|
+
}
|
|
8018
|
+
function putPlannedMcp(targetRepo, planned) {
|
|
8019
|
+
if (planned.kind !== "write" || planned.outcome === "skipped")
|
|
8020
|
+
return;
|
|
8021
|
+
writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
8022
|
+
}
|
|
8023
|
+
function planCrewRoster(targetRepo) {
|
|
8024
|
+
return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
8025
|
+
}
|
|
8026
|
+
function planInstallRecord(targetRepo, text) {
|
|
8027
|
+
return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
8028
|
+
}
|
|
8029
|
+
function accountInstall(steps, actual) {
|
|
8030
|
+
const { targetRepo, writeSet, pin } = steps;
|
|
8031
|
+
if (pin === null || steps.mcp.kind === "pending" || steps.engagementId === void 0) {
|
|
8032
|
+
throw new Error("[halfcycle] a plan still waiting for the service cannot be applied or recorded.");
|
|
8033
|
+
}
|
|
7187
8034
|
const report = {
|
|
7188
8035
|
writtenPaths: [],
|
|
7189
8036
|
skippedPaths: [],
|
|
@@ -7192,38 +8039,22 @@ async function install(options) {
|
|
|
7192
8039
|
editedCollidedPaths: [],
|
|
7193
8040
|
replacedPaths: []
|
|
7194
8041
|
};
|
|
7195
|
-
const
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
noteFile(draft,
|
|
7203
|
-
const
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
}
|
|
7210
|
-
|
|
7211
|
-
}
|
|
7212
|
-
const retiredOurs = retiredRulesThisInstallAdded(draft.previous);
|
|
7213
|
-
const statusLine = statusLineSetElsewhere(targetRepo, home, options.env) ? void 0 : HALFCYCLE_STATUS_LINE;
|
|
7214
|
-
const mergedSettings = mergeSettings(existingSettings, generatedSettings, retiredOurs, statusLine);
|
|
7215
|
-
recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted, mergedSettings);
|
|
7216
|
-
recordDenyRetired(draft, retiredOurs.map((rule) => sha256Hex(rule)));
|
|
7217
|
-
const mergedSettingsText = inLineEndingOf(readFileOrNull(settingsPath), JSON.stringify(mergedSettings, null, 2) + "\n");
|
|
7218
|
-
mkdirSync5(dirname3(settingsPath), { recursive: true });
|
|
7219
|
-
writeFileSync5(settingsPath, mergedSettingsText, "utf-8");
|
|
7220
|
-
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
7221
|
-
for (const [rel, content] of hookScriptsThisVersionWrites()) {
|
|
7222
|
-
const hookPath = join5(targetRepo, rel);
|
|
7223
|
-
const outcome = recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
|
|
7224
|
-
noteFile(draft, targetRepo, rel, outcome);
|
|
7225
|
-
}
|
|
7226
|
-
switch (removeLegacyCiStanza(targetRepo)) {
|
|
8042
|
+
const draft = startManifest(steps.previous, (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
|
|
8043
|
+
for (const command of steps.commands) {
|
|
8044
|
+
recordOwnedGenerated(report, command);
|
|
8045
|
+
if (command.editedCollided)
|
|
8046
|
+
report.editedCollidedPaths.push(command.rel);
|
|
8047
|
+
noteFile(draft, command.rel, command.outcome, actual.bytesAfter);
|
|
8048
|
+
}
|
|
8049
|
+
noteFile(draft, VENDORED_BIN_REL, recordOwnedGenerated(report, steps.vendored), actual.bytesAfter);
|
|
8050
|
+
const settings = steps.settings;
|
|
8051
|
+
recordSettingsMerge(draft, settings.existing, settings.generated, settings.preexisted, settings.merged);
|
|
8052
|
+
recordDenyRetired(draft, settings.retiredOurs.map((rule) => sha256Hex(rule)));
|
|
8053
|
+
(settings.preexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
8054
|
+
for (const hook of steps.hooks) {
|
|
8055
|
+
noteFile(draft, hook.rel, recordOwnedGenerated(report, hook), actual.bytesAfter);
|
|
8056
|
+
}
|
|
8057
|
+
switch (actual.ciStanza) {
|
|
7227
8058
|
case "removed":
|
|
7228
8059
|
report.writtenPaths.push(".halfcycle/ci-stanza.yml (REMOVED \u2014 Halfcycle no longer generates this file)");
|
|
7229
8060
|
break;
|
|
@@ -7239,20 +8070,17 @@ async function install(options) {
|
|
|
7239
8070
|
case "absent":
|
|
7240
8071
|
break;
|
|
7241
8072
|
}
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
if (
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
writeAllowlisted(mcpPath, targetRepo, mcpContent, report.mergedPaths);
|
|
7251
|
-
} else {
|
|
8073
|
+
const mcp = steps.mcp;
|
|
8074
|
+
if (mcp.kind === "write") {
|
|
8075
|
+
recordMcpMerge(draft, mcp.existing, mcp.text);
|
|
8076
|
+
if (mcp.outcome === "written")
|
|
8077
|
+
report.writtenPaths.push(MCP_REGISTRATION_REL);
|
|
8078
|
+
else if (mcp.outcome === "merged")
|
|
8079
|
+
report.mergedPaths.push(MCP_REGISTRATION_REL);
|
|
8080
|
+
else
|
|
7252
8081
|
report.skippedPaths.push(MCP_REGISTRATION_REL);
|
|
7253
|
-
}
|
|
7254
8082
|
const helperRel = RETIRED_MCP_HEADERS_HELPER_REL;
|
|
7255
|
-
switch (
|
|
8083
|
+
switch (actual.mcpHelper) {
|
|
7256
8084
|
case "removed":
|
|
7257
8085
|
recordRemovedFile(draft, helperRel);
|
|
7258
8086
|
report.writtenPaths.push(`${helperRel} (REMOVED \u2014 Halfcycle no longer uses this file)`);
|
|
@@ -7267,43 +8095,36 @@ async function install(options) {
|
|
|
7267
8095
|
report.skippedPaths.push(`${helperRel} (could not be removed \u2014 Halfcycle no longer uses this file, DELETE IT BY HAND)`);
|
|
7268
8096
|
break;
|
|
7269
8097
|
case "absent":
|
|
8098
|
+
case null:
|
|
7270
8099
|
break;
|
|
7271
8100
|
}
|
|
7272
8101
|
} else {
|
|
7273
8102
|
report.skippedPaths.push(`${MCP_REGISTRATION_REL} (no MCP origin \u2014 no credential supplied)`);
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
recordMcpAdoptionOnly(draft, readFileSync5(mcpPath, "utf-8"));
|
|
8103
|
+
if (mcp.existing !== null)
|
|
8104
|
+
recordMcpAdoptionOnly(draft, mcp.existing);
|
|
7277
8105
|
}
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
const gitignoreOutcome = reconcileGitignore(targetRepo);
|
|
7281
|
-
recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null, writeSet);
|
|
7282
|
-
if (gitignoreOutcome === "failed") {
|
|
8106
|
+
recordGitignoreReconcile(draft, steps.gitignore.before, actual.gitignoreAfter, writeSet);
|
|
8107
|
+
if (actual.gitignore === "failed") {
|
|
7283
8108
|
report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
|
|
7284
8109
|
} else {
|
|
7285
|
-
record(report,
|
|
8110
|
+
record(report, actual.gitignore, ".gitignore");
|
|
7286
8111
|
}
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
const gitattributesOutcome = reconcileGitattributes(targetRepo);
|
|
7290
|
-
recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath), writeSet);
|
|
7291
|
-
if (gitattributesOutcome === "failed") {
|
|
8112
|
+
recordGitattributesReconcile(draft, steps.gitattributes.before, actual.gitattributesAfter, writeSet);
|
|
8113
|
+
if (actual.gitattributes === "failed") {
|
|
7292
8114
|
report.skippedPaths.push(`${GITATTRIBUTES_REL} (write failed \u2014 a clone made with core.autocrlf=true may check the hook scripts out with CRLF line endings)`);
|
|
7293
8115
|
} else {
|
|
7294
|
-
record(report,
|
|
8116
|
+
record(report, actual.gitattributes, GITATTRIBUTES_REL);
|
|
7295
8117
|
}
|
|
7296
|
-
const identityOutcome =
|
|
8118
|
+
const identityOutcome = steps.identity.action === "add" ? "written" : "skipped";
|
|
7297
8119
|
record(report, identityOutcome, PROJECT_IDENTITY_REL);
|
|
7298
|
-
noteFile(draft,
|
|
7299
|
-
const credentialPath = engagementEnvPath(engagementId, home);
|
|
7300
|
-
if (credential) {
|
|
7301
|
-
record(report,
|
|
8120
|
+
noteFile(draft, PROJECT_IDENTITY_REL, identityOutcome, actual.bytesAfter);
|
|
8121
|
+
const credentialPath = engagementEnvPath(steps.engagementId, steps.home);
|
|
8122
|
+
if (actual.credential !== null) {
|
|
8123
|
+
record(report, actual.credential, credentialPath);
|
|
7302
8124
|
} else {
|
|
7303
8125
|
report.skippedPaths.push(`${credentialPath} (no credential supplied)`);
|
|
7304
8126
|
}
|
|
7305
|
-
const
|
|
7306
|
-
const migration = migrateLegacyEnvLocal(targetRepo, stored);
|
|
8127
|
+
const migration = actual.legacy;
|
|
7307
8128
|
switch (migration.outcome) {
|
|
7308
8129
|
case "removed":
|
|
7309
8130
|
report.writtenPaths.push(".env.local (REMOVED \u2014 the credential now lives outside this repository)");
|
|
@@ -7320,32 +8141,22 @@ async function install(options) {
|
|
|
7320
8141
|
case "absent":
|
|
7321
8142
|
break;
|
|
7322
8143
|
}
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
noteFile(draft,
|
|
7326
|
-
|
|
7327
|
-
if (
|
|
8144
|
+
if (pin.outcome === "written")
|
|
8145
|
+
report.writtenPaths.push(BUNDLE_PIN_REL);
|
|
8146
|
+
noteFile(draft, BUNDLE_PIN_REL, "written", actual.bytesAfter);
|
|
8147
|
+
noteFile(draft, CREW_ROSTER_REL, recordOwnedGenerated(report, steps.crew), actual.bytesAfter);
|
|
8148
|
+
if (actual.stateExistsAfter)
|
|
7328
8149
|
recordPerMachineFile(draft, BOOTSTRAP_STATE_REL);
|
|
7329
8150
|
for (const dir of writeSet.dirs) {
|
|
7330
|
-
if (!dirsBefore.has(dir) &&
|
|
8151
|
+
if (!steps.dirsBefore.has(dir) && actual.dirExistsAfter(dir))
|
|
7331
8152
|
recordCreatedDir(draft, dir);
|
|
7332
8153
|
}
|
|
7333
8154
|
const installManifest = finishManifest(draft);
|
|
7334
8155
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
writtenPaths: report.writtenPaths,
|
|
7340
|
-
skippedPaths: report.skippedPaths,
|
|
7341
|
-
mergedPaths: report.mergedPaths,
|
|
7342
|
-
collidedPaths: report.collidedPaths,
|
|
7343
|
-
editedCollidedPaths: report.editedCollidedPaths,
|
|
7344
|
-
replacedPaths: report.replacedPaths,
|
|
7345
|
-
bootstrapScanRan: scanResult.ran,
|
|
7346
|
-
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
7347
|
-
notShared
|
|
7348
|
-
};
|
|
8156
|
+
return { report, installManifest, manifestText: serializeManifest(installManifest) };
|
|
8157
|
+
}
|
|
8158
|
+
function recordInstallRecord(report, planned) {
|
|
8159
|
+
recordOwnedGenerated(report, planned);
|
|
7349
8160
|
}
|
|
7350
8161
|
var SELF_BUILD_GITIGNORE_REL = ".halfcycle/.gitignore";
|
|
7351
8162
|
var SELF_BUILD_GITIGNORE_TEXT = "bin/\n";
|
|
@@ -7359,7 +8170,7 @@ function mcpHasHalfcycleEntry(text) {
|
|
|
7359
8170
|
function installSelfBuild(options) {
|
|
7360
8171
|
const { targetRepo, engagementId, credential, freshlyMinted, home } = options;
|
|
7361
8172
|
assertEngagementId(engagementId);
|
|
7362
|
-
if (!
|
|
8173
|
+
if (!existsSync4(targetRepo)) {
|
|
7363
8174
|
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
7364
8175
|
}
|
|
7365
8176
|
const report = {
|
|
@@ -7370,7 +8181,7 @@ function installSelfBuild(options) {
|
|
|
7370
8181
|
editedCollidedPaths: [],
|
|
7371
8182
|
replacedPaths: []
|
|
7372
8183
|
};
|
|
7373
|
-
const at = (rel) =>
|
|
8184
|
+
const at = (rel) => join6(targetRepo, rel);
|
|
7374
8185
|
const version = readPluginManifest().version;
|
|
7375
8186
|
const existingMcp = readFileOrNull(at(MCP_REGISTRATION_REL));
|
|
7376
8187
|
const mcpText = existingMcp === null || !mcpHasHalfcycleEntry(existingMcp) ? inLineEndingOf(existingMcp, generateMcpRegistration(existingMcp, credential.mcpUrl)) : null;
|
|
@@ -7381,17 +8192,17 @@ function installSelfBuild(options) {
|
|
|
7381
8192
|
if (mcpText !== null) {
|
|
7382
8193
|
writeAllowlisted(at(MCP_REGISTRATION_REL), targetRepo, mcpText, report.writtenPaths);
|
|
7383
8194
|
}
|
|
7384
|
-
if (!
|
|
7385
|
-
recordOwned(report, at(VENDORED_BIN_REL), targetRepo,
|
|
8195
|
+
if (!existsSync4(at(VENDORED_BIN_REL))) {
|
|
8196
|
+
recordOwned(report, at(VENDORED_BIN_REL), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"), VENDORED_BIN_REL);
|
|
7386
8197
|
}
|
|
7387
|
-
if (!
|
|
8198
|
+
if (!existsSync4(at(SELF_BUILD_GITIGNORE_REL))) {
|
|
7388
8199
|
writeAllowlisted(at(SELF_BUILD_GITIGNORE_REL), targetRepo, SELF_BUILD_GITIGNORE_TEXT, report.writtenPaths);
|
|
7389
8200
|
}
|
|
7390
|
-
if (!
|
|
8201
|
+
if (!existsSync4(at(CREW_ROSTER_REL))) {
|
|
7391
8202
|
writeCrewRoster(targetRepo, report);
|
|
7392
8203
|
}
|
|
7393
|
-
if (!
|
|
7394
|
-
writeBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion, report.writtenPaths);
|
|
8204
|
+
if (!existsSync4(at(BUNDLE_PIN_REL))) {
|
|
8205
|
+
writeBundlePin(targetRepo, planBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion), report.writtenPaths);
|
|
7395
8206
|
}
|
|
7396
8207
|
return { writtenPaths: report.writtenPaths, credentialWritten };
|
|
7397
8208
|
}
|
|
@@ -7507,14 +8318,14 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
7507
8318
|
else
|
|
7508
8319
|
pending = callback;
|
|
7509
8320
|
}, engagementId));
|
|
7510
|
-
const bindProblem = await new Promise((
|
|
8321
|
+
const bindProblem = await new Promise((resolve8) => {
|
|
7511
8322
|
const onError = (err) => {
|
|
7512
|
-
|
|
8323
|
+
resolve8(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
|
|
7513
8324
|
};
|
|
7514
8325
|
server.once("error", onError);
|
|
7515
8326
|
server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
|
|
7516
8327
|
server.removeListener("error", onError);
|
|
7517
|
-
|
|
8328
|
+
resolve8(null);
|
|
7518
8329
|
});
|
|
7519
8330
|
});
|
|
7520
8331
|
if (bindProblem !== null) {
|
|
@@ -7530,9 +8341,9 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
7530
8341
|
bound: true,
|
|
7531
8342
|
target: { port: address.port, state },
|
|
7532
8343
|
boundAddress: address.address,
|
|
7533
|
-
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((
|
|
8344
|
+
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve8) => {
|
|
7534
8345
|
if (pending !== null) {
|
|
7535
|
-
|
|
8346
|
+
resolve8(pending);
|
|
7536
8347
|
return;
|
|
7537
8348
|
}
|
|
7538
8349
|
const startedAt = Date.now();
|
|
@@ -7543,7 +8354,7 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
7543
8354
|
clearTimeout(timer);
|
|
7544
8355
|
deliver = null;
|
|
7545
8356
|
abandon = null;
|
|
7546
|
-
|
|
8357
|
+
resolve8(value);
|
|
7547
8358
|
};
|
|
7548
8359
|
const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
|
|
7549
8360
|
deliver = finish;
|
|
@@ -7557,9 +8368,9 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
7557
8368
|
};
|
|
7558
8369
|
}
|
|
7559
8370
|
function closeServer(server) {
|
|
7560
|
-
return new Promise((
|
|
8371
|
+
return new Promise((resolve8) => {
|
|
7561
8372
|
server.closeAllConnections();
|
|
7562
|
-
server.close(() =>
|
|
8373
|
+
server.close(() => resolve8());
|
|
7563
8374
|
});
|
|
7564
8375
|
}
|
|
7565
8376
|
|
|
@@ -7582,7 +8393,7 @@ function defaultWrite(text) {
|
|
|
7582
8393
|
process.stdout.write(text);
|
|
7583
8394
|
}
|
|
7584
8395
|
function defaultSleep(ms) {
|
|
7585
|
-
return new Promise((
|
|
8396
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
7586
8397
|
}
|
|
7587
8398
|
function browserOpenCommand(os, url) {
|
|
7588
8399
|
if (os === "darwin")
|
|
@@ -7629,12 +8440,12 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
|
|
|
7629
8440
|
};
|
|
7630
8441
|
}
|
|
7631
8442
|
const { command, args: args2, windowsVerbatimArguments } = browserOpenCommand(os, url);
|
|
7632
|
-
return new Promise((
|
|
8443
|
+
return new Promise((resolve8) => {
|
|
7633
8444
|
let child;
|
|
7634
8445
|
try {
|
|
7635
8446
|
child = spawnImpl(command, args2, { stdio: "ignore", env, windowsVerbatimArguments });
|
|
7636
8447
|
} catch {
|
|
7637
|
-
|
|
8448
|
+
resolve8({ opened: false, reason: `${command} could not be run` });
|
|
7638
8449
|
return;
|
|
7639
8450
|
}
|
|
7640
8451
|
let settled = false;
|
|
@@ -7643,7 +8454,7 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
|
|
|
7643
8454
|
return;
|
|
7644
8455
|
settled = true;
|
|
7645
8456
|
clearTimeout(timer);
|
|
7646
|
-
|
|
8457
|
+
resolve8(attempt);
|
|
7647
8458
|
};
|
|
7648
8459
|
const timer = setTimeout(() => {
|
|
7649
8460
|
child.unref();
|
|
@@ -7788,17 +8599,17 @@ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
|
|
|
7788
8599
|
continue;
|
|
7789
8600
|
}
|
|
7790
8601
|
if (sleep === void 0) {
|
|
7791
|
-
await new Promise((
|
|
7792
|
-
const timer = setTimeout(
|
|
8602
|
+
await new Promise((resolve8) => {
|
|
8603
|
+
const timer = setTimeout(resolve8, started.pollIntervalMs);
|
|
7793
8604
|
interrupt = () => {
|
|
7794
8605
|
clearTimeout(timer);
|
|
7795
|
-
|
|
8606
|
+
resolve8();
|
|
7796
8607
|
};
|
|
7797
8608
|
});
|
|
7798
8609
|
} else {
|
|
7799
|
-
await new Promise((
|
|
7800
|
-
interrupt =
|
|
7801
|
-
void sleep(started.pollIntervalMs).then(
|
|
8610
|
+
await new Promise((resolve8) => {
|
|
8611
|
+
interrupt = resolve8;
|
|
8612
|
+
void sleep(started.pollIntervalMs).then(resolve8);
|
|
7802
8613
|
});
|
|
7803
8614
|
}
|
|
7804
8615
|
interrupt = null;
|
|
@@ -8074,8 +8885,8 @@ async function switchAccount(serviceUrl, opts = {}) {
|
|
|
8074
8885
|
function writeToStdout(text) {
|
|
8075
8886
|
process.stdout.write(text);
|
|
8076
8887
|
}
|
|
8077
|
-
async function replaceRefusedCredential(serviceUrl,
|
|
8078
|
-
if (
|
|
8888
|
+
async function replaceRefusedCredential(serviceUrl, refused3, opts = {}) {
|
|
8889
|
+
if (refused3.source !== "stored")
|
|
8079
8890
|
return null;
|
|
8080
8891
|
forgetStoredCredential(serviceUrl, opts.home);
|
|
8081
8892
|
(opts.write ?? writeToStdout)(`[halfcycle] Your saved Halfcycle sign-in was refused \u2014 signing you in again.
|
|
@@ -8420,9 +9231,9 @@ function wrongAccountVisibility(act) {
|
|
|
8420
9231
|
async function askLine(question2) {
|
|
8421
9232
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8422
9233
|
try {
|
|
8423
|
-
return await new Promise((
|
|
8424
|
-
rl.once("close", () =>
|
|
8425
|
-
rl.question(question2).then(
|
|
9234
|
+
return await new Promise((resolve8) => {
|
|
9235
|
+
rl.once("close", () => resolve8(""));
|
|
9236
|
+
rl.question(question2).then(resolve8, () => resolve8(""));
|
|
8426
9237
|
});
|
|
8427
9238
|
} finally {
|
|
8428
9239
|
rl.close();
|
|
@@ -8658,8 +9469,8 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
|
|
|
8658
9469
|
|
|
8659
9470
|
// dist/pre-create-checks.js
|
|
8660
9471
|
import { spawnSync } from "node:child_process";
|
|
8661
|
-
import { existsSync as
|
|
8662
|
-
import { delimiter, dirname as dirname4, join as
|
|
9472
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7, realpathSync } from "node:fs";
|
|
9473
|
+
import { delimiter, dirname as dirname4, join as join7 } from "node:path";
|
|
8663
9474
|
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.';
|
|
8664
9475
|
var PreCreateRefused = class extends Error {
|
|
8665
9476
|
constructor(message) {
|
|
@@ -8702,7 +9513,7 @@ function refuseSystemFolder(targetRepo, platform6 = process.platform) {
|
|
|
8702
9513
|
function refuseInsideProject(targetRepo) {
|
|
8703
9514
|
let folder = realOrLiteral(targetRepo);
|
|
8704
9515
|
for (let parent = dirname4(folder); parent !== folder; folder = parent, parent = dirname4(parent)) {
|
|
8705
|
-
if (
|
|
9516
|
+
if (existsSync5(join7(parent, BUNDLE_PIN_REL))) {
|
|
8706
9517
|
throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in Claude Code instead, or run "npx halfcycle" in a folder outside it.`);
|
|
8707
9518
|
}
|
|
8708
9519
|
}
|
|
@@ -8725,35 +9536,43 @@ async function offerGitForWindows() {
|
|
|
8725
9536
|
return true;
|
|
8726
9537
|
throw new PreCreateRefused(`Git for Windows is needed and is not installed \u2014 install it with "${GIT_INSTALL_COMMAND}" or from ${GIT_DOWNLOAD_PAGE}, then open a new terminal and run "npx halfcycle" again.`);
|
|
8727
9538
|
}
|
|
8728
|
-
function
|
|
8729
|
-
const onPath = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "" &&
|
|
9539
|
+
function gitBashPath(env) {
|
|
9540
|
+
const onPath = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "" && existsSync5(join7(dir, "git.exe"))).map((dir) => join7(dir, "..", "bin", "bash.exe"));
|
|
8730
9541
|
const candidates = [
|
|
8731
9542
|
env["CLAUDE_CODE_GIT_BASH_PATH"],
|
|
8732
9543
|
...onPath,
|
|
8733
|
-
env["ProgramFiles"] === void 0 ? void 0 :
|
|
9544
|
+
env["ProgramFiles"] === void 0 ? void 0 : join7(env["ProgramFiles"], "Git", "bin", "bash.exe")
|
|
8734
9545
|
];
|
|
8735
|
-
return candidates.
|
|
9546
|
+
return candidates.find((file) => file !== void 0 && file !== "" && existsSync5(file));
|
|
9547
|
+
}
|
|
9548
|
+
function hasGitBash(env) {
|
|
9549
|
+
return gitBashPath(env) !== void 0;
|
|
8736
9550
|
}
|
|
8737
9551
|
function runWingetInstall() {
|
|
8738
9552
|
return spawnSync("winget", [...GIT_INSTALL_ARGS], { stdio: "inherit" }).status === 0;
|
|
8739
9553
|
}
|
|
9554
|
+
function unreadableProjectRefusal(targetRepo) {
|
|
9555
|
+
return `${join7(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.`;
|
|
9556
|
+
}
|
|
9557
|
+
function cannotWriteRefusal(targetRepo) {
|
|
9558
|
+
return `cannot write to ${targetRepo} \u2014 run "npx halfcycle" in a folder you can write to.`;
|
|
9559
|
+
}
|
|
8740
9560
|
function claimFolderId(targetRepo) {
|
|
9561
|
+
const planned = planProjectIdentity(targetRepo);
|
|
8741
9562
|
try {
|
|
8742
|
-
|
|
9563
|
+
writeProjectIdentity(targetRepo, planned, true);
|
|
8743
9564
|
} catch {
|
|
8744
|
-
throw new PreCreateRefused(
|
|
9565
|
+
throw new PreCreateRefused(cannotWriteRefusal(targetRepo));
|
|
8745
9566
|
}
|
|
8746
|
-
repairRemoteCredential(targetRepo);
|
|
8747
9567
|
const folderId = readFolderId(targetRepo);
|
|
8748
|
-
if (folderId === void 0)
|
|
8749
|
-
throw new PreCreateRefused(
|
|
8750
|
-
}
|
|
9568
|
+
if (folderId === void 0)
|
|
9569
|
+
throw new PreCreateRefused(unreadableProjectRefusal(targetRepo));
|
|
8751
9570
|
return folderId;
|
|
8752
9571
|
}
|
|
8753
9572
|
function readFolderId(targetRepo) {
|
|
8754
9573
|
let folderId;
|
|
8755
9574
|
try {
|
|
8756
|
-
const text =
|
|
9575
|
+
const text = readFileSync7(join7(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
|
|
8757
9576
|
folderId = JSON.parse(text)?.projectId;
|
|
8758
9577
|
} catch {
|
|
8759
9578
|
return void 0;
|
|
@@ -8769,20 +9588,420 @@ function checkSettingsParse(targetRepo) {
|
|
|
8769
9588
|
}
|
|
8770
9589
|
}
|
|
8771
9590
|
|
|
9591
|
+
// dist/folder-refusals.js
|
|
9592
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
9593
|
+
import { homedir as homedir3 } from "node:os";
|
|
9594
|
+
function folderMissingRefusal(targetRepo) {
|
|
9595
|
+
return `halfcycle: no such directory: ${targetRepo}`;
|
|
9596
|
+
}
|
|
9597
|
+
function folderIsFileRefusal(targetRepo) {
|
|
9598
|
+
return `halfcycle: ${targetRepo} is a file \u2014 run "npx halfcycle" with a project folder instead.`;
|
|
9599
|
+
}
|
|
9600
|
+
function monorepoRefusal(targetRepo) {
|
|
9601
|
+
return `halfcycle: refusing to install into the Halfcycle monorepo itself (${targetRepo}).
|
|
9602
|
+
The bare form installs into the CURRENT directory, and this directory is the
|
|
9603
|
+
product's own source tree \u2014 installing here would write over tracked files.
|
|
9604
|
+
To see what this CLI does: halfcycle --help
|
|
9605
|
+
To install into a real project: halfcycle <path-to-that-repo>`;
|
|
9606
|
+
}
|
|
9607
|
+
function homeFolderRefusal(targetRepo) {
|
|
9608
|
+
return `halfcycle: refusing to set up in your home folder (${targetRepo}). Run "npx halfcycle" inside a project folder.`;
|
|
9609
|
+
}
|
|
9610
|
+
function isHomeFolder(dir) {
|
|
9611
|
+
try {
|
|
9612
|
+
const target = realpathSync2(dir);
|
|
9613
|
+
const home = realpathSync2(homedir3());
|
|
9614
|
+
return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
|
|
9615
|
+
} catch {
|
|
9616
|
+
return false;
|
|
9617
|
+
}
|
|
9618
|
+
}
|
|
9619
|
+
|
|
9620
|
+
// dist/preview.js
|
|
9621
|
+
import { accessSync, constants, existsSync as existsSync6, readFileSync as readFileSync9, statSync } from "node:fs";
|
|
9622
|
+
import { join as join9 } from "node:path";
|
|
9623
|
+
|
|
9624
|
+
// dist/halfcycle-monorepo.js
|
|
9625
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
9626
|
+
import { join as join8 } from "node:path";
|
|
9627
|
+
var MONOREPO_PACKAGE_NAME = "halfcycle-monorepo";
|
|
9628
|
+
function isHalfcycleMonorepo(dir) {
|
|
9629
|
+
try {
|
|
9630
|
+
const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf-8"));
|
|
9631
|
+
return pkg.name === MONOREPO_PACKAGE_NAME;
|
|
9632
|
+
} catch {
|
|
9633
|
+
return false;
|
|
9634
|
+
}
|
|
9635
|
+
}
|
|
9636
|
+
function isSelfBuildInstallForm(engagementIdArg, engagementTypeArg) {
|
|
9637
|
+
return engagementIdArg !== void 0 && engagementTypeArg === "self-build";
|
|
9638
|
+
}
|
|
9639
|
+
|
|
9640
|
+
// dist/preview.js
|
|
9641
|
+
var CLAUDE_MD_REL = "CLAUDE.md";
|
|
9642
|
+
var AGENTS_IMPORT_LINE = "@AGENTS.md";
|
|
9643
|
+
var WORDS = {
|
|
9644
|
+
intro: "Halfcycle would set up this folder like this. Nothing has been written.",
|
|
9645
|
+
remove: "an older Halfcycle file this version no longer uses",
|
|
9646
|
+
claudeMdWillAdd: "already holds your own instructions; your first session will add one line to it, pointing to AGENTS.md",
|
|
9647
|
+
claudeMdAlreadyPoints: "already holds your own instructions, including the line pointing to AGENTS.md",
|
|
9648
|
+
signIn: " This project's sign-in, kept on this computer for you only, in your home folder's .halfcycle folder.",
|
|
9649
|
+
newProject: " A new project on Halfcycle's service for this folder, made when you sign in.",
|
|
9650
|
+
gitForWindows: " Git for Windows, which Claude Code needs: setting up will ask before installing it with winget.",
|
|
9651
|
+
firstSession: "Your first session will then write your project's own files as you answer its questions, starting with AGENTS.md and CLAUDE.md, which hold your project's own instructions.",
|
|
9652
|
+
toSetUp: `To set up for real, run: ${HALFCYCLE_COMMAND}`,
|
|
9653
|
+
planFailed: "halfcycle: setting up this folder would stop on a file that could not be read. Nothing has been written; fix that file, then run the preview again."
|
|
9654
|
+
};
|
|
9655
|
+
function runPreview(targetRepo, options) {
|
|
9656
|
+
const refusal2 = previewRefusal(targetRepo, options);
|
|
9657
|
+
if (refusal2 !== void 0)
|
|
9658
|
+
return { refusal: refusal2 };
|
|
9659
|
+
let plan;
|
|
9660
|
+
try {
|
|
9661
|
+
plan = planInstall({ targetRepo, env: options.env, home: options.home });
|
|
9662
|
+
} catch {
|
|
9663
|
+
return { refusal: WORDS.planFailed };
|
|
9664
|
+
}
|
|
9665
|
+
if (plan.refusal !== void 0)
|
|
9666
|
+
return { refusal: plan.refusal };
|
|
9667
|
+
return {
|
|
9668
|
+
text: renderPreview(plan, gitBashPath(options.env) === void 0 && options.platform === "win32", claudeMdLine(targetRepo))
|
|
9669
|
+
};
|
|
9670
|
+
}
|
|
9671
|
+
function claudeMdLine(targetRepo) {
|
|
9672
|
+
const path = join9(targetRepo, CLAUDE_MD_REL);
|
|
9673
|
+
if (!existsSync6(path))
|
|
9674
|
+
return void 0;
|
|
9675
|
+
let text;
|
|
9676
|
+
try {
|
|
9677
|
+
text = readFileSync9(path, "utf-8");
|
|
9678
|
+
} catch {
|
|
9679
|
+
return WORDS.claudeMdWillAdd;
|
|
9680
|
+
}
|
|
9681
|
+
return text.includes(AGENTS_IMPORT_LINE) ? WORDS.claudeMdAlreadyPoints : WORDS.claudeMdWillAdd;
|
|
9682
|
+
}
|
|
9683
|
+
function previewRefusal(targetRepo, options) {
|
|
9684
|
+
if (!existsSync6(targetRepo))
|
|
9685
|
+
return folderMissingRefusal(targetRepo);
|
|
9686
|
+
if (!statSync(targetRepo).isDirectory())
|
|
9687
|
+
return folderIsFileRefusal(targetRepo);
|
|
9688
|
+
if (isHalfcycleMonorepo(targetRepo))
|
|
9689
|
+
return monorepoRefusal(targetRepo);
|
|
9690
|
+
if (isHomeFolder(targetRepo))
|
|
9691
|
+
return homeFolderRefusal(targetRepo);
|
|
9692
|
+
try {
|
|
9693
|
+
refuseSystemFolder(targetRepo, options.platform);
|
|
9694
|
+
refuseInsideProject(targetRepo);
|
|
9695
|
+
} catch (err) {
|
|
9696
|
+
if (err instanceof PreCreateRefused)
|
|
9697
|
+
return err.message;
|
|
9698
|
+
throw err;
|
|
9699
|
+
}
|
|
9700
|
+
try {
|
|
9701
|
+
accessSync(targetRepo, constants.W_OK);
|
|
9702
|
+
} catch {
|
|
9703
|
+
return cannotWriteRefusal(targetRepo);
|
|
9704
|
+
}
|
|
9705
|
+
if (existsSync6(join9(targetRepo, PROJECT_IDENTITY_REL)) && readFolderId(targetRepo) === void 0) {
|
|
9706
|
+
return unreadableProjectRefusal(targetRepo);
|
|
9707
|
+
}
|
|
9708
|
+
try {
|
|
9709
|
+
checkSettingsParse(targetRepo);
|
|
9710
|
+
} catch (err) {
|
|
9711
|
+
if (err instanceof PreCreateRefused)
|
|
9712
|
+
return err.message;
|
|
9713
|
+
throw err;
|
|
9714
|
+
}
|
|
9715
|
+
return void 0;
|
|
9716
|
+
}
|
|
9717
|
+
function wordsOf(entry) {
|
|
9718
|
+
const words = PREVIEW_WORDS[entry.kind];
|
|
9719
|
+
switch (entry.action) {
|
|
9720
|
+
case "add":
|
|
9721
|
+
return words.add;
|
|
9722
|
+
case "change":
|
|
9723
|
+
return words.change;
|
|
9724
|
+
case "remove":
|
|
9725
|
+
return WORDS.remove;
|
|
9726
|
+
case "leave":
|
|
9727
|
+
return entry.yours === true ? words.yours : words.leave;
|
|
9728
|
+
}
|
|
9729
|
+
}
|
|
9730
|
+
var HEADINGS = [
|
|
9731
|
+
["add", "Add:"],
|
|
9732
|
+
["change", "Change:"],
|
|
9733
|
+
["remove", "Remove:"],
|
|
9734
|
+
["leave", "Leave alone:"]
|
|
9735
|
+
];
|
|
9736
|
+
function outsideLine2(outside) {
|
|
9737
|
+
return outside.kind === "sign-in" ? WORDS.signIn : WORDS.newProject;
|
|
9738
|
+
}
|
|
9739
|
+
function renderPreview(plan, missingGitForWindows, claudeMdWords) {
|
|
9740
|
+
const lines = [WORDS.intro];
|
|
9741
|
+
for (const [action, heading] of HEADINGS) {
|
|
9742
|
+
const rows = plan.entries.filter((entry) => entry.action === action);
|
|
9743
|
+
const claudeMdRow = action === "leave" && claudeMdWords !== void 0;
|
|
9744
|
+
if (rows.length === 0 && !claudeMdRow)
|
|
9745
|
+
continue;
|
|
9746
|
+
lines.push("", heading);
|
|
9747
|
+
for (const entry of rows)
|
|
9748
|
+
lines.push(` ${entry.path} \u2014 ${wordsOf(entry)}`);
|
|
9749
|
+
if (claudeMdRow)
|
|
9750
|
+
lines.push(` ${CLAUDE_MD_REL} \u2014 ${claudeMdWords}`);
|
|
9751
|
+
}
|
|
9752
|
+
lines.push("", "Outside this folder:");
|
|
9753
|
+
for (const outside of plan.outside)
|
|
9754
|
+
lines.push(outsideLine2(outside));
|
|
9755
|
+
if (missingGitForWindows)
|
|
9756
|
+
lines.push(WORDS.gitForWindows);
|
|
9757
|
+
lines.push("", WORDS.firstSession, "", WORDS.toSetUp);
|
|
9758
|
+
return lines.join("\n");
|
|
9759
|
+
}
|
|
9760
|
+
|
|
9761
|
+
// dist/doctor.js
|
|
9762
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9763
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
|
|
9764
|
+
import { join as join10 } from "node:path";
|
|
9765
|
+
var GUARD_RUNNER_REL2 = ".claude/hooks/guard-runner.sh";
|
|
9766
|
+
var CONNECTION_VERB = "check-connection";
|
|
9767
|
+
var WRAPPER_TIMEOUT_MS = 3e4;
|
|
9768
|
+
var SIGNED_OUT_KINDS = ["not-signed-in", "other-account", "signed-out"];
|
|
9769
|
+
var NOT_CONNECTED_KINDS = ["update-claude-code", "cannot-reach", "service-problem"];
|
|
9770
|
+
var KNOWN_KINDS = ["connected", "not-registered", ...SIGNED_OUT_KINDS, ...NOT_CONNECTED_KINDS];
|
|
9771
|
+
function runDoctor(root, options) {
|
|
9772
|
+
const current = packageVersion();
|
|
9773
|
+
const installed = installedVersion(root);
|
|
9774
|
+
const upToDate = installed === current;
|
|
9775
|
+
const setUp = setUpLine(root, upToDate);
|
|
9776
|
+
let running = setUp.state === "no" ? { state: "not-run" } : startWrapper(root, options);
|
|
9777
|
+
if (setUp.state === "yes" && running.state === "answered" && running.kind === "not-registered") {
|
|
9778
|
+
running = failed(repairSentence());
|
|
9779
|
+
}
|
|
9780
|
+
const checksRunning = checksRunningLine(running);
|
|
9781
|
+
const connection = connectionLines(running);
|
|
9782
|
+
const version = upToDate ? { ok: true, text: `Up to date: yes (${current}).` } : { ok: false, text: versionNoLine(installed, current) };
|
|
9783
|
+
const lines = [setUp.line, connection.signedIn, connection.connected, checksRunning, version];
|
|
9784
|
+
const failing = lines.filter((line) => !line.ok).length;
|
|
9785
|
+
const closing = failing === 0 ? "Everything is working." : `${failing} of these need fixing; each line above says what to do.`;
|
|
9786
|
+
return {
|
|
9787
|
+
text: [...lines.map((line) => line.text), closing].join("\n"),
|
|
9788
|
+
exitCode: failing === 0 ? 0 : 1
|
|
9789
|
+
};
|
|
9790
|
+
}
|
|
9791
|
+
function setUpLine(root, upToDate) {
|
|
9792
|
+
if (!existsSync7(join10(root, BUNDLE_PIN_REL))) {
|
|
9793
|
+
return {
|
|
9794
|
+
state: "no",
|
|
9795
|
+
line: {
|
|
9796
|
+
ok: false,
|
|
9797
|
+
text: `Set up: no. Halfcycle is not set up in this folder. To set it up, run ${HALFCYCLE_COMMAND} here.`
|
|
9798
|
+
}
|
|
9799
|
+
};
|
|
9800
|
+
}
|
|
9801
|
+
const missing = [];
|
|
9802
|
+
if (!pinNamesProject(root))
|
|
9803
|
+
missing.push("Halfcycle's project record");
|
|
9804
|
+
if (!existsSync7(join10(root, INSTALL_MANIFEST_REL)))
|
|
9805
|
+
missing.push("the install record");
|
|
9806
|
+
if (!existsSync7(join10(root, VENDORED_BIN_REL)))
|
|
9807
|
+
missing.push("Halfcycle's check program");
|
|
9808
|
+
if (!mcpNamesServer(root))
|
|
9809
|
+
missing.push("Halfcycle's server entry");
|
|
9810
|
+
if (upToDate && !settingsRegisterEveryCommand(root))
|
|
9811
|
+
missing.push("the checks in your settings");
|
|
9812
|
+
if (missing.length === 0)
|
|
9813
|
+
return { state: "yes", line: { ok: true, text: "Set up: yes." } };
|
|
9814
|
+
return {
|
|
9815
|
+
state: "not-complete",
|
|
9816
|
+
line: {
|
|
9817
|
+
ok: false,
|
|
9818
|
+
text: `Set up: not complete. Missing: ${naturalList(missing)}. To put it back, run ${HALFCYCLE_COMMAND} here; nothing of yours is changed.`
|
|
9819
|
+
}
|
|
9820
|
+
};
|
|
9821
|
+
}
|
|
9822
|
+
function pinNamesProject(root) {
|
|
9823
|
+
try {
|
|
9824
|
+
const pin = readBundlePin(root);
|
|
9825
|
+
return typeof pin?.engagementId === "string" && pin.engagementId !== "";
|
|
9826
|
+
} catch {
|
|
9827
|
+
return false;
|
|
9828
|
+
}
|
|
9829
|
+
}
|
|
9830
|
+
function mcpNamesServer(root) {
|
|
9831
|
+
const parsed = readJson(join10(root, MCP_REGISTRATION_REL));
|
|
9832
|
+
if (!isObject(parsed) || !isObject(parsed["mcpServers"]))
|
|
9833
|
+
return false;
|
|
9834
|
+
const entry = parsed["mcpServers"][MCP_SERVER_KEY];
|
|
9835
|
+
return isObject(entry) && typeof entry["url"] === "string";
|
|
9836
|
+
}
|
|
9837
|
+
function settingsRegisterEveryCommand(root) {
|
|
9838
|
+
const parsed = readJson(join10(root, SETTINGS_REL));
|
|
9839
|
+
if (!isObject(parsed) || !isObject(parsed["hooks"]))
|
|
9840
|
+
return false;
|
|
9841
|
+
const registered = /* @__PURE__ */ new Set();
|
|
9842
|
+
for (const groups of Object.values(parsed["hooks"])) {
|
|
9843
|
+
if (!Array.isArray(groups))
|
|
9844
|
+
continue;
|
|
9845
|
+
for (const group of groups) {
|
|
9846
|
+
if (!isObject(group) || !Array.isArray(group["hooks"]))
|
|
9847
|
+
continue;
|
|
9848
|
+
for (const hook of group["hooks"]) {
|
|
9849
|
+
if (isObject(hook) && typeof hook["command"] === "string")
|
|
9850
|
+
registered.add(hook["command"]);
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
}
|
|
9854
|
+
return HALFCYCLE_HOOK_WIRING.every((entry) => registered.has(entry.command));
|
|
9855
|
+
}
|
|
9856
|
+
function startWrapper(root, options) {
|
|
9857
|
+
const disabledIn = settingsDisablingHooks(root);
|
|
9858
|
+
if (disabledIn !== void 0) {
|
|
9859
|
+
return failed(`Checks running: no. Your settings turn off every check (${disabledIn}, "disableAllHooks"). Remove that line, then open a new session.`);
|
|
9860
|
+
}
|
|
9861
|
+
let shell = "bash";
|
|
9862
|
+
if (options.platform === "win32") {
|
|
9863
|
+
const gitBash = gitBashPath(options.env);
|
|
9864
|
+
if (gitBash === void 0) {
|
|
9865
|
+
return failed("Checks running: no. Claude Code needs Git for Windows to run Halfcycle's checks. Install it, then open a new session.");
|
|
9866
|
+
}
|
|
9867
|
+
shell = gitBash;
|
|
9868
|
+
}
|
|
9869
|
+
const wrapper = join10(root, GUARD_RUNNER_REL2);
|
|
9870
|
+
if (!existsSync7(wrapper))
|
|
9871
|
+
return failed(repairSentence());
|
|
9872
|
+
const run = spawnSync2(shell, [wrapper, CONNECTION_VERB], {
|
|
9873
|
+
cwd: root,
|
|
9874
|
+
env: { ...options.env, CLAUDE_PROJECT_DIR: root },
|
|
9875
|
+
encoding: "utf-8",
|
|
9876
|
+
timeout: WRAPPER_TIMEOUT_MS,
|
|
9877
|
+
windowsHide: true
|
|
9878
|
+
});
|
|
9879
|
+
if (run.error !== void 0 || run.status === null)
|
|
9880
|
+
return failed(repairSentence());
|
|
9881
|
+
if (run.status === 127) {
|
|
9882
|
+
return failed(`Checks running: no. ${NO_NODE_SENTENCE}`);
|
|
9883
|
+
}
|
|
9884
|
+
if (run.status !== 0) {
|
|
9885
|
+
if (String(run.stderr).includes("Halfcycle needs Node")) {
|
|
9886
|
+
return failed(`Checks running: no. Halfcycle's checks need Node ${NODE_FLOOR_MAJOR} or newer. Install it, then open a new session.`);
|
|
9887
|
+
}
|
|
9888
|
+
return failed(repairSentence());
|
|
9889
|
+
}
|
|
9890
|
+
const answer = parseAnswer(String(run.stdout));
|
|
9891
|
+
if (answer === void 0)
|
|
9892
|
+
return failed(repairSentence());
|
|
9893
|
+
return { state: "answered", ...answer };
|
|
9894
|
+
}
|
|
9895
|
+
function settingsDisablingHooks(root) {
|
|
9896
|
+
for (const rel of [SETTINGS_REL, LOCAL_SETTINGS_REL]) {
|
|
9897
|
+
const parsed = readJson(join10(root, rel));
|
|
9898
|
+
if (isObject(parsed) && parsed["disableAllHooks"] === true)
|
|
9899
|
+
return rel;
|
|
9900
|
+
}
|
|
9901
|
+
return void 0;
|
|
9902
|
+
}
|
|
9903
|
+
function parseAnswer(stdout) {
|
|
9904
|
+
const last = stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").pop();
|
|
9905
|
+
if (last === void 0)
|
|
9906
|
+
return void 0;
|
|
9907
|
+
let parsed;
|
|
9908
|
+
try {
|
|
9909
|
+
parsed = JSON.parse(last);
|
|
9910
|
+
} catch {
|
|
9911
|
+
return void 0;
|
|
9912
|
+
}
|
|
9913
|
+
if (!isObject(parsed) || typeof parsed["kind"] !== "string" || !KNOWN_KINDS.includes(parsed["kind"]))
|
|
9914
|
+
return void 0;
|
|
9915
|
+
const sentence = parsed["sentence"];
|
|
9916
|
+
if (sentence !== null && typeof sentence !== "string")
|
|
9917
|
+
return void 0;
|
|
9918
|
+
if (parsed["kind"] !== "not-registered" && typeof sentence !== "string")
|
|
9919
|
+
return void 0;
|
|
9920
|
+
return { kind: parsed["kind"], sentence };
|
|
9921
|
+
}
|
|
9922
|
+
function checksRunningLine(running) {
|
|
9923
|
+
if (running.state === "not-run")
|
|
9924
|
+
return notChecked("Checks running", "Set up");
|
|
9925
|
+
if (running.state === "failed")
|
|
9926
|
+
return running.line;
|
|
9927
|
+
return { ok: true, text: "Checks running: yes." };
|
|
9928
|
+
}
|
|
9929
|
+
function failed(text) {
|
|
9930
|
+
return { state: "failed", line: { ok: false, text } };
|
|
9931
|
+
}
|
|
9932
|
+
function repairSentence() {
|
|
9933
|
+
return `Checks running: no. Halfcycle's check could not start. To repair it, run ${HALFCYCLE_COMMAND} here.`;
|
|
9934
|
+
}
|
|
9935
|
+
function connectionLines(running) {
|
|
9936
|
+
if (running.state !== "answered") {
|
|
9937
|
+
return { signedIn: notChecked("Signed in", "Checks running"), connected: notChecked("Connected", "Checks running") };
|
|
9938
|
+
}
|
|
9939
|
+
const { kind, sentence } = running;
|
|
9940
|
+
if (kind === "not-registered") {
|
|
9941
|
+
return { signedIn: notChecked("Signed in", "Set up"), connected: notChecked("Connected", "Set up") };
|
|
9942
|
+
}
|
|
9943
|
+
if (kind === "connected") {
|
|
9944
|
+
return { signedIn: { ok: true, text: "Signed in: yes." }, connected: { ok: true, text: "Connected: yes." } };
|
|
9945
|
+
}
|
|
9946
|
+
if (SIGNED_OUT_KINDS.includes(kind)) {
|
|
9947
|
+
return {
|
|
9948
|
+
signedIn: { ok: false, text: `Signed in: no. ${sentence ?? ""}`.trimEnd() },
|
|
9949
|
+
connected: notChecked("Connected", "Signed in")
|
|
9950
|
+
};
|
|
9951
|
+
}
|
|
9952
|
+
return {
|
|
9953
|
+
signedIn: { ok: true, text: "Signed in: yes." },
|
|
9954
|
+
connected: { ok: false, text: `Connected: no. ${sentence ?? ""}`.trimEnd() }
|
|
9955
|
+
};
|
|
9956
|
+
}
|
|
9957
|
+
function installedVersion(root) {
|
|
9958
|
+
try {
|
|
9959
|
+
return checkDrift(root).installed;
|
|
9960
|
+
} catch {
|
|
9961
|
+
return null;
|
|
9962
|
+
}
|
|
9963
|
+
}
|
|
9964
|
+
function versionNoLine(installed, current) {
|
|
9965
|
+
if (installed === null) {
|
|
9966
|
+
return `Up to date: no. This folder has no Halfcycle version recorded; the newest is ${current}. To set it up, run ${HALFCYCLE_COMMAND} here.`;
|
|
9967
|
+
}
|
|
9968
|
+
return `Up to date: no. This folder has Halfcycle ${installed}; the newest is ${current}. To update, run ${HALFCYCLE_COMMAND} here.`;
|
|
9969
|
+
}
|
|
9970
|
+
function notChecked(check, earlier) {
|
|
9971
|
+
return { ok: false, text: `${check}: not checked, because ${earlier} is not working yet.` };
|
|
9972
|
+
}
|
|
9973
|
+
function naturalList(items) {
|
|
9974
|
+
if (items.length <= 1)
|
|
9975
|
+
return items.join("");
|
|
9976
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
9977
|
+
}
|
|
9978
|
+
function readJson(path) {
|
|
9979
|
+
if (!existsSync7(path))
|
|
9980
|
+
return void 0;
|
|
9981
|
+
try {
|
|
9982
|
+
return JSON.parse(readFileSync10(path, "utf-8"));
|
|
9983
|
+
} catch {
|
|
9984
|
+
return void 0;
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
function isObject(value) {
|
|
9988
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9989
|
+
}
|
|
9990
|
+
|
|
8772
9991
|
// dist/claude-plan.js
|
|
8773
|
-
import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as
|
|
9992
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8774
9993
|
import { platform as platform4 } from "node:os";
|
|
8775
|
-
import { dirname as dirname5, join as
|
|
9994
|
+
import { dirname as dirname5, join as join11 } from "node:path";
|
|
8776
9995
|
var CLAUDE_PLAN_FILENAME = "claude-plan.json";
|
|
8777
9996
|
var PLAN_QUESTION_TRIES = 3;
|
|
8778
9997
|
var SEND_TIMEOUT_MS = 5e3;
|
|
8779
9998
|
var SETTINGS_PAGE_SEGMENT = "settings";
|
|
8780
9999
|
function claudePlanPath(home) {
|
|
8781
|
-
return
|
|
10000
|
+
return join11(halfcycleHome(home), CLAUDE_PLAN_FILENAME);
|
|
8782
10001
|
}
|
|
8783
10002
|
function readRememberedClaudePlan(home) {
|
|
8784
10003
|
try {
|
|
8785
|
-
const parsed = claudePlanRequestSchema.safeParse(JSON.parse(
|
|
10004
|
+
const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync11(claudePlanPath(home), "utf-8")));
|
|
8786
10005
|
return parsed.success ? parsed.data.claudePlan : void 0;
|
|
8787
10006
|
} catch {
|
|
8788
10007
|
return void 0;
|
|
@@ -8853,6 +10072,8 @@ async function setupClaudePlan(input, deps) {
|
|
|
8853
10072
|
}
|
|
8854
10073
|
return { kind: "remembered", plan: remembered, sent };
|
|
8855
10074
|
}
|
|
10075
|
+
if (deps.assumeYes === true)
|
|
10076
|
+
return { kind: "no-person" };
|
|
8856
10077
|
let plan = null;
|
|
8857
10078
|
for (let tries = 0; tries < PLAN_QUESTION_TRIES && plan === null; tries++) {
|
|
8858
10079
|
plan = claudePlanFromAnswer(await deps.ask(`${CLAUDE_PLAN_QUESTION} `));
|
|
@@ -8866,22 +10087,6 @@ async function setupClaudePlan(input, deps) {
|
|
|
8866
10087
|
}
|
|
8867
10088
|
}
|
|
8868
10089
|
|
|
8869
|
-
// dist/halfcycle-monorepo.js
|
|
8870
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
8871
|
-
import { join as join8 } from "node:path";
|
|
8872
|
-
var MONOREPO_PACKAGE_NAME = "halfcycle-monorepo";
|
|
8873
|
-
function isHalfcycleMonorepo(dir) {
|
|
8874
|
-
try {
|
|
8875
|
-
const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf-8"));
|
|
8876
|
-
return pkg.name === MONOREPO_PACKAGE_NAME;
|
|
8877
|
-
} catch {
|
|
8878
|
-
return false;
|
|
8879
|
-
}
|
|
8880
|
-
}
|
|
8881
|
-
function isSelfBuildInstallForm(engagementIdArg, engagementTypeArg) {
|
|
8882
|
-
return engagementIdArg !== void 0 && engagementTypeArg === "self-build";
|
|
8883
|
-
}
|
|
8884
|
-
|
|
8885
10090
|
// dist/probe-mcp.js
|
|
8886
10091
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
8887
10092
|
async function probeMcpOrigin(mcpUrl, token) {
|
|
@@ -8936,8 +10141,8 @@ async function probeMcpOrigin(mcpUrl, token) {
|
|
|
8936
10141
|
|
|
8937
10142
|
// dist/claude-version-probe.js
|
|
8938
10143
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
8939
|
-
import { existsSync as
|
|
8940
|
-
import { delimiter as delimiter2, join as
|
|
10144
|
+
import { existsSync as existsSync8, realpathSync as realpathSync3 } from "node:fs";
|
|
10145
|
+
import { delimiter as delimiter2, join as join12 } from "node:path";
|
|
8941
10146
|
function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
8942
10147
|
const platform6 = deps.platform ?? process.platform;
|
|
8943
10148
|
const exec = deps.exec ?? execFileSync3;
|
|
@@ -8974,10 +10179,10 @@ var INSTALL_METHOD_RULES = [
|
|
|
8974
10179
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
8975
10180
|
const env = deps.env ?? process.env;
|
|
8976
10181
|
const platform6 = deps.platform ?? process.platform;
|
|
8977
|
-
const exists = deps.exists ??
|
|
10182
|
+
const exists = deps.exists ?? existsSync8;
|
|
8978
10183
|
const realpath = deps.realpath ?? ((file) => {
|
|
8979
10184
|
try {
|
|
8980
|
-
return
|
|
10185
|
+
return realpathSync3(file);
|
|
8981
10186
|
} catch {
|
|
8982
10187
|
return file;
|
|
8983
10188
|
}
|
|
@@ -8986,7 +10191,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
8986
10191
|
const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
|
|
8987
10192
|
for (const dir of dirs) {
|
|
8988
10193
|
for (const name of names) {
|
|
8989
|
-
const file =
|
|
10194
|
+
const file = join12(dir, name);
|
|
8990
10195
|
if (!exists(file))
|
|
8991
10196
|
continue;
|
|
8992
10197
|
const real = realpath(file);
|
|
@@ -8997,8 +10202,8 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
8997
10202
|
}
|
|
8998
10203
|
|
|
8999
10204
|
// dist/build-record/write.js
|
|
9000
|
-
import { existsSync as
|
|
9001
|
-
import { basename as basename2, join as
|
|
10205
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "node:fs";
|
|
10206
|
+
import { basename as basename2, join as join13, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
|
|
9002
10207
|
|
|
9003
10208
|
// dist/build-record/render-v2.js
|
|
9004
10209
|
var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
|
|
@@ -9390,10 +10595,14 @@ function renderClose(heading, observed, attested, checks, p, closingRound = fals
|
|
|
9390
10595
|
];
|
|
9391
10596
|
}
|
|
9392
10597
|
var REVIEW_LABEL = "**Recorded by Halfcycle from the owner's review.** Each part the review held, and how the project's owner decided it. A line marked as the agent's is the agent's own answer, not Halfcycle's.";
|
|
10598
|
+
var REVIEW_LABEL_WITH_AUTONOMY = `**Recorded by Halfcycle from the owner's review.** Each part the review held, and how it was decided: by the project's owner, unless the part says it was ${DECIDED_BY_AUTONOMY_SENTENCE}. A line marked as the agent's is the agent's own answer, not Halfcycle's.`;
|
|
9393
10599
|
var REVIEW_NO_PARTS = "The review had no parts, so it closed on one accept that names no part.";
|
|
9394
10600
|
function approvalLine(approval) {
|
|
9395
10601
|
if (approval.kind === "none")
|
|
9396
10602
|
return "Built without an approved scope.";
|
|
10603
|
+
if (approval.givenIn === "autonomy") {
|
|
10604
|
+
return `Plan version ${String(approval.planVersion)} was ${APPROVED_IN_ADVANCE_SENTENCE}.`;
|
|
10605
|
+
}
|
|
9397
10606
|
const where = approval.givenIn === "console" ? "in the console" : "in their own session";
|
|
9398
10607
|
return `Scope approved by the project's owner, plan version ${String(approval.planVersion)}, ${where}.`;
|
|
9399
10608
|
}
|
|
@@ -9407,8 +10616,11 @@ function decisionText(part, p) {
|
|
|
9407
10616
|
return `Dropped: ${p.text(part.reason ?? "")}`;
|
|
9408
10617
|
}
|
|
9409
10618
|
}
|
|
10619
|
+
var DECIDED_BY_AUTONOMY_LINE = `${DECIDED_BY_AUTONOMY_SENTENCE.charAt(0).toUpperCase()}${DECIDED_BY_AUTONOMY_SENTENCE.slice(1)}.`;
|
|
9410
10620
|
function reviewPartLines(part, p) {
|
|
9411
10621
|
const lines = [`- ${p.itemStart(part.name)} \u2014 ${decisionText(part, p)}`];
|
|
10622
|
+
if (part.givenIn === "autonomy")
|
|
10623
|
+
lines.push(` - ${DECIDED_BY_AUTONOMY_LINE}`);
|
|
9412
10624
|
const code = codeLinkLine(part.codeLink);
|
|
9413
10625
|
if (code !== void 0)
|
|
9414
10626
|
lines.push(` - ${p.text(code)}`);
|
|
@@ -9428,7 +10640,8 @@ function removalsLine(record2, p) {
|
|
|
9428
10640
|
}
|
|
9429
10641
|
function renderReview(record2, p) {
|
|
9430
10642
|
const review = record2.review;
|
|
9431
|
-
const
|
|
10643
|
+
const label = review.parts.some((part) => part.givenIn === "autonomy") ? REVIEW_LABEL_WITH_AUTONOMY : REVIEW_LABEL;
|
|
10644
|
+
const blocks = ["## The owner's review", label, approvalLine(review.approval)];
|
|
9432
10645
|
blocks.push(review.parts.length === 0 ? REVIEW_NO_PARTS : review.parts.flatMap((part) => reviewPartLines(part, p)).join("\n"));
|
|
9433
10646
|
const removals = removalsLine(record2, p);
|
|
9434
10647
|
if (removals !== null)
|
|
@@ -9440,10 +10653,9 @@ function renderCalls(calls, p) {
|
|
|
9440
10653
|
return [`## ${CALLS_HEADING}`, NO_CALLS_LINE];
|
|
9441
10654
|
const lines = calls.flatMap((c) => {
|
|
9442
10655
|
const by = p.itemStart(c.by);
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
];
|
|
10656
|
+
const undo = ` - ${callUndoLine({ by: p.text(c.by), undoable: c.undoable, how: c.how === null ? null : p.text(c.how) })}`;
|
|
10657
|
+
const head = c.kind === "listed" ? autonomyC8(by, p.text(c.question), p.text(c.chose)) : callMadeLine({ by, question: p.text(c.question), chose: p.text(c.chose), statedDefault: p.text(c.statedDefault) });
|
|
10658
|
+
return [`- ${head}`, undo];
|
|
9447
10659
|
});
|
|
9448
10660
|
return [`## ${CALLS_HEADING}`, CALLS_EXPLAINED, lines.join("\n")];
|
|
9449
10661
|
}
|
|
@@ -9543,7 +10755,7 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
9543
10755
|
}
|
|
9544
10756
|
|
|
9545
10757
|
// dist/build-record/pair-io.js
|
|
9546
|
-
import { closeSync, openSync, readFileSync as
|
|
10758
|
+
import { closeSync, openSync, readFileSync as readFileSync12, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
|
|
9547
10759
|
function createExclusiveOnDisk(path, data) {
|
|
9548
10760
|
const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
9549
10761
|
const fd = openSync(path, "wx");
|
|
@@ -9574,7 +10786,7 @@ function isEexist(err) {
|
|
|
9574
10786
|
}
|
|
9575
10787
|
function readIfPresent(path) {
|
|
9576
10788
|
try {
|
|
9577
|
-
return
|
|
10789
|
+
return readFileSync12(path, "utf-8");
|
|
9578
10790
|
} catch {
|
|
9579
10791
|
return null;
|
|
9580
10792
|
}
|
|
@@ -9606,7 +10818,7 @@ function replaceOne(io, tmp, finalPath, text) {
|
|
|
9606
10818
|
}
|
|
9607
10819
|
|
|
9608
10820
|
// dist/build-record/write.js
|
|
9609
|
-
var BUILD_RECORD_DIR =
|
|
10821
|
+
var BUILD_RECORD_DIR = join13("docs", "build-records");
|
|
9610
10822
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
9611
10823
|
const rel = relative2(outDir, resolve3(candidate));
|
|
9612
10824
|
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename2(rel);
|
|
@@ -9644,8 +10856,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
9644
10856
|
}
|
|
9645
10857
|
const outDir = resolve3(repoRoot, BUILD_RECORD_DIR);
|
|
9646
10858
|
const segment = renderPhaseSegment(phase);
|
|
9647
|
-
const jsonPath =
|
|
9648
|
-
const mdPath =
|
|
10859
|
+
const jsonPath = join13(outDir, `${segment}.json`);
|
|
10860
|
+
const mdPath = join13(outDir, `${segment}.md`);
|
|
9649
10861
|
const jsonTmp = `${jsonPath}.tmp`;
|
|
9650
10862
|
const mdTmp = `${mdPath}.tmp`;
|
|
9651
10863
|
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
@@ -9659,7 +10871,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
9659
10871
|
}
|
|
9660
10872
|
const existingJson = readIfPresent(jsonPath);
|
|
9661
10873
|
if (existingJson === null) {
|
|
9662
|
-
return { kind: "in-the-way", path:
|
|
10874
|
+
return { kind: "in-the-way", path: existsSync9(jsonPath) ? jsonPath : mdPath };
|
|
9663
10875
|
}
|
|
9664
10876
|
if (existingJson === jsonText) {
|
|
9665
10877
|
replaceOne(io, mdTmp, mdPath, mdText);
|
|
@@ -9686,8 +10898,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
9686
10898
|
import { relative as relative3 } from "node:path";
|
|
9687
10899
|
|
|
9688
10900
|
// dist/open-phase.js
|
|
9689
|
-
import { readFileSync as
|
|
9690
|
-
import { join as
|
|
10901
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
10902
|
+
import { join as join14 } from "node:path";
|
|
9691
10903
|
|
|
9692
10904
|
// dist/close-phase.js
|
|
9693
10905
|
import { execFile, execFileSync as execFileSync4 } from "node:child_process";
|
|
@@ -9771,7 +10983,7 @@ function readObserveLine(stdout) {
|
|
|
9771
10983
|
function runRunnerChild(binary, args2, credential, repoRoot, readLine, fallback) {
|
|
9772
10984
|
if (binary instanceof Error)
|
|
9773
10985
|
return Promise.resolve(fallback);
|
|
9774
|
-
return new Promise((
|
|
10986
|
+
return new Promise((resolve8) => {
|
|
9775
10987
|
try {
|
|
9776
10988
|
execFile(process.execPath, [binary, ...args2], {
|
|
9777
10989
|
cwd: repoRoot,
|
|
@@ -9781,10 +10993,10 @@ function runRunnerChild(binary, args2, credential, repoRoot, readLine, fallback)
|
|
|
9781
10993
|
windowsHide: true
|
|
9782
10994
|
}, (err, stdout) => {
|
|
9783
10995
|
const stopped = err !== null && err.killed === true;
|
|
9784
|
-
|
|
10996
|
+
resolve8(stopped || typeof stdout !== "string" ? fallback : readLine(stdout));
|
|
9785
10997
|
});
|
|
9786
10998
|
} catch {
|
|
9787
|
-
|
|
10999
|
+
resolve8(fallback);
|
|
9788
11000
|
}
|
|
9789
11001
|
});
|
|
9790
11002
|
}
|
|
@@ -9997,6 +11209,14 @@ function closeStepReport(phase, attempt, engagementId) {
|
|
|
9997
11209
|
case "unavailable":
|
|
9998
11210
|
stderr.push(`${REFUSED} the Halfcycle service could not open the review just now. Nothing was sent to the owner. Run this again in a few minutes.`);
|
|
9999
11211
|
break;
|
|
11212
|
+
case "closed-by-autonomy":
|
|
11213
|
+
stdout.push(autonomyA13(answer.accepted, answer.carried));
|
|
11214
|
+
exitCode = 0;
|
|
11215
|
+
break;
|
|
11216
|
+
case "autonomy-waits":
|
|
11217
|
+
stdout.push(answer.reason === "unchecked-claims" ? autonomyA14(answer.count, answer.parts.map((p) => p.name)) : autonomyA15(answer.count));
|
|
11218
|
+
exitCode = 0;
|
|
11219
|
+
break;
|
|
10000
11220
|
default:
|
|
10001
11221
|
return assertNever2(answer);
|
|
10002
11222
|
}
|
|
@@ -10168,7 +11388,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
10168
11388
|
if (pinned !== null)
|
|
10169
11389
|
assertEngagementId(pinned);
|
|
10170
11390
|
const fromStore = pinned === null ? null : readEngagementEnv(pinned, home);
|
|
10171
|
-
const looked = pinned === null ? `${
|
|
11391
|
+
const looked = pinned === null ? `${join14(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
|
|
10172
11392
|
const values = {};
|
|
10173
11393
|
for (const key of OPEN_PHASE_ENV_KEYS) {
|
|
10174
11394
|
const value = (env[key] ?? fromStore?.[key] ?? "").trim();
|
|
@@ -10185,7 +11405,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
10185
11405
|
}
|
|
10186
11406
|
function readPinnedEngagementId(repoRoot) {
|
|
10187
11407
|
try {
|
|
10188
|
-
const pin = JSON.parse(
|
|
11408
|
+
const pin = JSON.parse(readFileSync13(join14(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
|
|
10189
11409
|
const id = pin[PIN_ENGAGEMENT_ID_FIELD];
|
|
10190
11410
|
return typeof id === "string" && id !== "" ? id : null;
|
|
10191
11411
|
} catch {
|
|
@@ -10404,12 +11624,12 @@ async function readPhaseWork(credential, phase) {
|
|
|
10404
11624
|
|
|
10405
11625
|
// dist/system-report.js
|
|
10406
11626
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
10407
|
-
import { readFileSync as
|
|
10408
|
-
import { join as
|
|
11627
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
11628
|
+
import { join as join15 } from "node:path";
|
|
10409
11629
|
function readDescriptionFile(repoRoot) {
|
|
10410
11630
|
let text;
|
|
10411
11631
|
try {
|
|
10412
|
-
text =
|
|
11632
|
+
text = readFileSync14(join15(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
|
|
10413
11633
|
} catch (err) {
|
|
10414
11634
|
const code = err.code;
|
|
10415
11635
|
return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
|
|
@@ -10423,18 +11643,18 @@ function readDescriptionFile(repoRoot) {
|
|
|
10423
11643
|
function problemPlace(raw, path) {
|
|
10424
11644
|
if (path === "")
|
|
10425
11645
|
return SYSTEM_DESCRIPTION_REL;
|
|
10426
|
-
const [
|
|
11646
|
+
const [list2, index] = path.split(".");
|
|
10427
11647
|
const position = Number(index);
|
|
10428
11648
|
if (!Number.isInteger(position) || position < 0)
|
|
10429
11649
|
return path;
|
|
10430
|
-
const items = typeof raw === "object" && raw !== null ? raw[
|
|
11650
|
+
const items = typeof raw === "object" && raw !== null ? raw[list2 ?? ""] : void 0;
|
|
10431
11651
|
const item = Array.isArray(items) ? items[position] : void 0;
|
|
10432
11652
|
const fields = typeof item === "object" && item !== null ? item : {};
|
|
10433
|
-
if (
|
|
11653
|
+
if (list2 === "parts") {
|
|
10434
11654
|
const id = fields["id"];
|
|
10435
11655
|
return typeof id === "string" && id !== "" ? `${path} (part "${id}")` : `${path} (part ${position + 1})`;
|
|
10436
11656
|
}
|
|
10437
|
-
if (
|
|
11657
|
+
if (list2 === "connections") {
|
|
10438
11658
|
const from = fields["from"];
|
|
10439
11659
|
const to = fields["to"];
|
|
10440
11660
|
return typeof from === "string" && typeof to === "string" ? `${path} (connection "${from}" to "${to}")` : `${path} (connection ${position + 1})`;
|
|
@@ -10633,12 +11853,12 @@ async function runSystem(repoRoot, deps) {
|
|
|
10633
11853
|
}
|
|
10634
11854
|
|
|
10635
11855
|
// dist/plan.js
|
|
10636
|
-
import { existsSync as
|
|
10637
|
-
import { join as
|
|
11856
|
+
import { existsSync as existsSync11, readFileSync as readFileSync17 } from "node:fs";
|
|
11857
|
+
import { join as join20 } from "node:path";
|
|
10638
11858
|
|
|
10639
11859
|
// dist/draw/write.js
|
|
10640
|
-
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as
|
|
10641
|
-
import { dirname as dirname6, isAbsolute as isAbsolute4, join as
|
|
11860
|
+
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync15, readSync, realpathSync as realpathSync4, rmSync as rmSync5 } from "node:fs";
|
|
11861
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join16, relative as relative4, resolve as resolve4, sep } from "node:path";
|
|
10642
11862
|
var DOCS_DIR = "docs";
|
|
10643
11863
|
var DIAGRAMS_DIR = "diagrams";
|
|
10644
11864
|
var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
|
|
@@ -10694,9 +11914,9 @@ function embedImageLink(base, doc) {
|
|
|
10694
11914
|
}
|
|
10695
11915
|
function diagramPaths(repoRoot, base) {
|
|
10696
11916
|
const root = resolve4(repoRoot);
|
|
10697
|
-
const dir =
|
|
11917
|
+
const dir = join16(root, DOCS_DIR, DIAGRAMS_DIR);
|
|
10698
11918
|
const names = fileNames(base);
|
|
10699
|
-
const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which,
|
|
11919
|
+
const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join16(dir, names[which])]));
|
|
10700
11920
|
return { repoRoot: root, dir, file };
|
|
10701
11921
|
}
|
|
10702
11922
|
function diagramRel(base, which) {
|
|
@@ -10725,7 +11945,7 @@ function isInside(outer, inner) {
|
|
|
10725
11945
|
function checkDiagramPlace(paths) {
|
|
10726
11946
|
let root;
|
|
10727
11947
|
try {
|
|
10728
|
-
root =
|
|
11948
|
+
root = realpathSync4(paths.repoRoot);
|
|
10729
11949
|
} catch {
|
|
10730
11950
|
return { ok: false, why: `the folder ${paths.repoRoot} could not be opened. Check the path, then run this again.` };
|
|
10731
11951
|
}
|
|
@@ -10738,7 +11958,7 @@ function checkDiagramPlace(paths) {
|
|
|
10738
11958
|
}
|
|
10739
11959
|
let real;
|
|
10740
11960
|
try {
|
|
10741
|
-
real =
|
|
11961
|
+
real = realpathSync4(ancestor);
|
|
10742
11962
|
} catch {
|
|
10743
11963
|
return {
|
|
10744
11964
|
ok: false,
|
|
@@ -10791,7 +12011,7 @@ function readExistingSet(paths) {
|
|
|
10791
12011
|
if (!stat.isFile())
|
|
10792
12012
|
return { ok: false, inTheWay: which, temp: false };
|
|
10793
12013
|
try {
|
|
10794
|
-
found[which] =
|
|
12014
|
+
found[which] = readFileSync15(paths.file[which]);
|
|
10795
12015
|
} catch {
|
|
10796
12016
|
return { ok: false, inTheWay: which, temp: false };
|
|
10797
12017
|
}
|
|
@@ -10883,9 +12103,9 @@ function sameBytes(a, b) {
|
|
|
10883
12103
|
}
|
|
10884
12104
|
function writeDiagramSet(repoRoot, base, set, io = NODE_RECORD_PAIR_IO) {
|
|
10885
12105
|
const paths = diagramPaths(repoRoot, base);
|
|
10886
|
-
const
|
|
10887
|
-
if (!
|
|
10888
|
-
return { kind: "unsafe", why:
|
|
12106
|
+
const place2 = checkDiagramPlace(paths);
|
|
12107
|
+
if (!place2.ok)
|
|
12108
|
+
return { kind: "unsafe", why: place2.why };
|
|
10889
12109
|
const read = readExistingSet(paths);
|
|
10890
12110
|
if (!read.ok)
|
|
10891
12111
|
return { kind: "in-the-way", file: read.inTheWay, temp: read.temp };
|
|
@@ -10955,10 +12175,10 @@ function restore(io, paths, renamed, existing) {
|
|
|
10955
12175
|
}
|
|
10956
12176
|
|
|
10957
12177
|
// dist/change.js
|
|
10958
|
-
import { join as
|
|
12178
|
+
import { join as join18 } from "node:path";
|
|
10959
12179
|
|
|
10960
12180
|
// dist/cli-contract.js
|
|
10961
|
-
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "uninstall", "change", "pace", "prove"];
|
|
12181
|
+
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "uninstall", "change", "pace", "prove", "preview", "doctor", "restore"];
|
|
10962
12182
|
var CONTRACTS = [
|
|
10963
12183
|
{ verb: "install", required: [], conditional: [] },
|
|
10964
12184
|
{ verb: "check-drift", required: [], conditional: [] },
|
|
@@ -10971,6 +12191,9 @@ var CONTRACTS = [
|
|
|
10971
12191
|
{ verb: "uninstall", required: [], conditional: [] },
|
|
10972
12192
|
{ verb: "pace", required: [], conditional: [] },
|
|
10973
12193
|
{ verb: "prove", required: [], conditional: [] },
|
|
12194
|
+
{ verb: "preview", required: [], conditional: [] },
|
|
12195
|
+
{ verb: "doctor", required: [], conditional: [] },
|
|
12196
|
+
{ verb: "restore", required: [], conditional: [] },
|
|
10974
12197
|
{
|
|
10975
12198
|
verb: "open-phase",
|
|
10976
12199
|
required: [
|
|
@@ -11149,8 +12372,8 @@ function placeholderRefusal(verb, tokens) {
|
|
|
11149
12372
|
}
|
|
11150
12373
|
|
|
11151
12374
|
// dist/build-record/write-change.js
|
|
11152
|
-
import { existsSync as
|
|
11153
|
-
import { basename as basename3, isAbsolute as isAbsolute5, join as
|
|
12375
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
|
|
12376
|
+
import { basename as basename3, isAbsolute as isAbsolute5, join as join17, relative as relative5, resolve as resolve5 } from "node:path";
|
|
11154
12377
|
|
|
11155
12378
|
// dist/build-record/render-change.js
|
|
11156
12379
|
function counted2(n, one, many) {
|
|
@@ -11295,7 +12518,7 @@ function renderChangeRecordMarkdown(record2) {
|
|
|
11295
12518
|
}
|
|
11296
12519
|
|
|
11297
12520
|
// dist/build-record/write-change.js
|
|
11298
|
-
var CHANGE_RECORD_DIR =
|
|
12521
|
+
var CHANGE_RECORD_DIR = join17("docs", "build-records", "changes");
|
|
11299
12522
|
var InvalidChangeIdError = class extends Error {
|
|
11300
12523
|
id;
|
|
11301
12524
|
constructor(id) {
|
|
@@ -11315,8 +12538,8 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
|
|
|
11315
12538
|
if (!checked.success)
|
|
11316
12539
|
throw new InvalidChangeIdError(sortingId);
|
|
11317
12540
|
const outDir = resolve5(repoRoot, CHANGE_RECORD_DIR);
|
|
11318
|
-
const jsonPath =
|
|
11319
|
-
const mdPath =
|
|
12541
|
+
const jsonPath = join17(outDir, `${checked.data}.json`);
|
|
12542
|
+
const mdPath = join17(outDir, `${checked.data}.md`);
|
|
11320
12543
|
const mdTmp = `${mdPath}.tmp`;
|
|
11321
12544
|
for (const candidate of [jsonPath, mdPath, mdTmp]) {
|
|
11322
12545
|
assertInsideChangeDir(outDir, candidate, sortingId);
|
|
@@ -11329,7 +12552,7 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
|
|
|
11329
12552
|
}
|
|
11330
12553
|
const existingJson = readIfPresent(jsonPath);
|
|
11331
12554
|
if (existingJson === null) {
|
|
11332
|
-
return { kind: "in-the-way", path:
|
|
12555
|
+
return { kind: "in-the-way", path: existsSync10(jsonPath) ? jsonPath : mdPath };
|
|
11333
12556
|
}
|
|
11334
12557
|
if (existingJson !== jsonText)
|
|
11335
12558
|
return { kind: "in-the-way", path: jsonPath };
|
|
@@ -11592,7 +12815,7 @@ function writePairLines(deps, repoRoot, sortingId, found) {
|
|
|
11592
12815
|
};
|
|
11593
12816
|
}
|
|
11594
12817
|
return {
|
|
11595
|
-
stdout: `[halfcycle] Its record is in ${
|
|
12818
|
+
stdout: `[halfcycle] Its record is in ${join18(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
|
|
11596
12819
|
`,
|
|
11597
12820
|
stderr: "",
|
|
11598
12821
|
ok: true
|
|
@@ -11831,24 +13054,24 @@ function runChange(repoRoot, args2, deps) {
|
|
|
11831
13054
|
}
|
|
11832
13055
|
|
|
11833
13056
|
// dist/shown-record.js
|
|
11834
|
-
import { chmodSync as chmodSync4, mkdirSync as mkdirSync10, readFileSync as
|
|
13057
|
+
import { chmodSync as chmodSync4, mkdirSync as mkdirSync10, readFileSync as readFileSync16, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
11835
13058
|
import { platform as platform5 } from "node:os";
|
|
11836
|
-
import { join as
|
|
13059
|
+
import { join as join19 } from "node:path";
|
|
11837
13060
|
function updateShownRecord(engagementId, update, home) {
|
|
11838
13061
|
let temp = null;
|
|
11839
13062
|
try {
|
|
11840
13063
|
const dir = engagementStateDir(engagementId, home);
|
|
11841
|
-
const path =
|
|
13064
|
+
const path = join19(dir, SHOWN_PLANS_FILENAME);
|
|
11842
13065
|
let current;
|
|
11843
13066
|
try {
|
|
11844
|
-
current = parseShownRecord(
|
|
13067
|
+
current = parseShownRecord(readFileSync16(path, "utf-8"));
|
|
11845
13068
|
} catch {
|
|
11846
13069
|
current = { entries: [], reviews: [] };
|
|
11847
13070
|
}
|
|
11848
13071
|
const next = update(current);
|
|
11849
13072
|
const body = serialiseShownRecord(next);
|
|
11850
13073
|
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
11851
|
-
temp =
|
|
13074
|
+
temp = join19(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
|
|
11852
13075
|
writeFileSync7(temp, body, { mode: 384 });
|
|
11853
13076
|
if (platform5() !== "win32")
|
|
11854
13077
|
chmodSync4(temp, 384);
|
|
@@ -11907,9 +13130,9 @@ function parsePlanArgs(argv) {
|
|
|
11907
13130
|
function planProblemPlace(raw, fileRel, path) {
|
|
11908
13131
|
if (path === "")
|
|
11909
13132
|
return fileRel;
|
|
11910
|
-
const [
|
|
13133
|
+
const [list2, index] = path.split(".");
|
|
11911
13134
|
const position = Number(index);
|
|
11912
|
-
if (
|
|
13135
|
+
if (list2 !== "changes" || !Number.isInteger(position) || position < 0)
|
|
11913
13136
|
return path;
|
|
11914
13137
|
const changes = typeof raw === "object" && raw !== null ? raw["changes"] : void 0;
|
|
11915
13138
|
const item = Array.isArray(changes) ? changes[position] : void 0;
|
|
@@ -11925,7 +13148,7 @@ function readAndCheckPlan(repoRoot, phase, description, verb) {
|
|
|
11925
13148
|
const fileRel = planFileRel(phase);
|
|
11926
13149
|
let text;
|
|
11927
13150
|
try {
|
|
11928
|
-
text =
|
|
13151
|
+
text = readFileSync17(join20(repoRoot, fileRel), "utf-8");
|
|
11929
13152
|
} catch (err) {
|
|
11930
13153
|
const code = err.code;
|
|
11931
13154
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
@@ -12017,7 +13240,8 @@ async function reportPhasePlan(credential, plan, descriptionVersion) {
|
|
|
12017
13240
|
changedParts: answer.changedParts,
|
|
12018
13241
|
typedApprovalOffered: answer.typedApprovalOffered,
|
|
12019
13242
|
carriedFrom: answer.carriedFrom,
|
|
12020
|
-
movedUp: answer.movedUp
|
|
13243
|
+
movedUp: answer.movedUp,
|
|
13244
|
+
givenIn: answer.givenIn
|
|
12021
13245
|
};
|
|
12022
13246
|
case "refused":
|
|
12023
13247
|
return { kind: "refused", problems: answer.problems };
|
|
@@ -12080,7 +13304,7 @@ function removalOwedLines(verb, parts) {
|
|
|
12080
13304
|
function redrawLine(repoRoot, phase) {
|
|
12081
13305
|
let exists;
|
|
12082
13306
|
try {
|
|
12083
|
-
exists =
|
|
13307
|
+
exists = existsSync11(join20(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
|
|
12084
13308
|
} catch {
|
|
12085
13309
|
return null;
|
|
12086
13310
|
}
|
|
@@ -12094,14 +13318,14 @@ function movedUpLines(movedUp) {
|
|
|
12094
13318
|
${nextSayLine(movedUp.next.say)}
|
|
12095
13319
|
`;
|
|
12096
13320
|
}
|
|
12097
|
-
function planStateSentence(state) {
|
|
13321
|
+
function planStateSentence(state, givenIn) {
|
|
12098
13322
|
switch (state) {
|
|
12099
13323
|
case "waiting":
|
|
12100
13324
|
return "Waiting for the owner's approval.";
|
|
12101
13325
|
case "approved":
|
|
12102
|
-
return "The owner has approved this plan.";
|
|
13326
|
+
return givenIn === "autonomy" ? `The plan was ${APPROVED_IN_ADVANCE_SENTENCE}.` : "The owner has approved this plan.";
|
|
12103
13327
|
case "partly-stale":
|
|
12104
|
-
return "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
|
|
13328
|
+
return givenIn === "autonomy" ? `The plan was ${APPROVED_IN_ADVANCE_SENTENCE}; the parts marked as changed wait for the owner's approval.` : "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
|
|
12105
13329
|
case "sent-back":
|
|
12106
13330
|
return "The owner sent this plan back. Change it to answer the owner's notes, then run this again.";
|
|
12107
13331
|
}
|
|
@@ -12172,7 +13396,7 @@ async function runPlan(repoRoot, phase, show, deps) {
|
|
|
12172
13396
|
}
|
|
12173
13397
|
const shownPhase = String(plan.phase);
|
|
12174
13398
|
const recorded = `[halfcycle] Plan for "${shownPhase}" recorded: plan version ${outcome.version}.`;
|
|
12175
|
-
const stateSentence = planStateSentence(outcome.state);
|
|
13399
|
+
const stateSentence = planStateSentence(outcome.state, outcome.givenIn);
|
|
12176
13400
|
const redraw = redrawLine(repoRoot, phase) ?? "";
|
|
12177
13401
|
const moved = outcome.movedUp === void 0 ? "" : `${movedUpLines(outcome.movedUp)}`;
|
|
12178
13402
|
if (!show)
|
|
@@ -12329,9 +13553,9 @@ Paste this into ${where}:
|
|
|
12329
13553
|
}
|
|
12330
13554
|
function localRefusal(repoRoot, base) {
|
|
12331
13555
|
const paths = diagramPaths(repoRoot, base);
|
|
12332
|
-
const
|
|
12333
|
-
if (!
|
|
12334
|
-
return `${P} ${
|
|
13556
|
+
const place2 = checkDiagramPlace(paths);
|
|
13557
|
+
if (!place2.ok)
|
|
13558
|
+
return `${P} ${place2.why}
|
|
12335
13559
|
`;
|
|
12336
13560
|
const read = readExistingSet(paths);
|
|
12337
13561
|
if (!read.ok)
|
|
@@ -12478,8 +13702,8 @@ async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, canno
|
|
|
12478
13702
|
}
|
|
12479
13703
|
|
|
12480
13704
|
// dist/check-tasks.js
|
|
12481
|
-
import { readFileSync as
|
|
12482
|
-
import { join as
|
|
13705
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
13706
|
+
import { join as join21 } from "node:path";
|
|
12483
13707
|
var CHECK_TASKS_USAGE = `${HALFCYCLE_COMMAND} check-tasks <phase> [--with-task-list] [--repo <root>]`;
|
|
12484
13708
|
function parseCheckTasksArgs(argv) {
|
|
12485
13709
|
const positionals = [];
|
|
@@ -12515,7 +13739,7 @@ function issuePlace(rel, path) {
|
|
|
12515
13739
|
function readJsonFile(repoRoot, rel, parse) {
|
|
12516
13740
|
let text;
|
|
12517
13741
|
try {
|
|
12518
|
-
text =
|
|
13742
|
+
text = readFileSync18(join21(repoRoot, rel), "utf-8");
|
|
12519
13743
|
} catch (err) {
|
|
12520
13744
|
const code = err.code;
|
|
12521
13745
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -12593,13 +13817,13 @@ function splitProveInvocation(words) {
|
|
|
12593
13817
|
function runProveChild(binary, credential, repoRoot, words, deps = {}) {
|
|
12594
13818
|
const out = deps.stdout ?? process.stdout;
|
|
12595
13819
|
const err = deps.stderr ?? process.stderr;
|
|
12596
|
-
return new Promise((
|
|
13820
|
+
return new Promise((resolve8) => {
|
|
12597
13821
|
let settled = false;
|
|
12598
13822
|
const finish = (code) => {
|
|
12599
13823
|
if (settled)
|
|
12600
13824
|
return;
|
|
12601
13825
|
settled = true;
|
|
12602
|
-
|
|
13826
|
+
resolve8(code);
|
|
12603
13827
|
};
|
|
12604
13828
|
let child;
|
|
12605
13829
|
try {
|
|
@@ -12681,6 +13905,17 @@ function sentBackInRound(part, round) {
|
|
|
12681
13905
|
return part.sentBack.some((s) => s.round === round);
|
|
12682
13906
|
}
|
|
12683
13907
|
function partStatus(part, round) {
|
|
13908
|
+
const settled = settledStatus(part);
|
|
13909
|
+
if (settled !== void 0) {
|
|
13910
|
+
return part.givenIn === "autonomy" ? `${settled}, ${DECIDED_BY_AUTONOMY_SENTENCE}` : settled;
|
|
13911
|
+
}
|
|
13912
|
+
if (awaitingAnswer(part))
|
|
13913
|
+
return "Waiting for the agent's answer";
|
|
13914
|
+
if (sentBackInRound(part, round))
|
|
13915
|
+
return "Sent back, waiting for the next round";
|
|
13916
|
+
return "Waits for your decision";
|
|
13917
|
+
}
|
|
13918
|
+
function settledStatus(part) {
|
|
12684
13919
|
switch (part.decision) {
|
|
12685
13920
|
case "accepted":
|
|
12686
13921
|
return "Accepted";
|
|
@@ -12689,13 +13924,8 @@ function partStatus(part, round) {
|
|
|
12689
13924
|
case "dropped":
|
|
12690
13925
|
return "Dropped";
|
|
12691
13926
|
case void 0:
|
|
12692
|
-
|
|
13927
|
+
return void 0;
|
|
12693
13928
|
}
|
|
12694
|
-
if (awaitingAnswer(part))
|
|
12695
|
-
return "Waiting for the agent's answer";
|
|
12696
|
-
if (sentBackInRound(part, round))
|
|
12697
|
-
return "Sent back, waiting for the next round";
|
|
12698
|
-
return "Waits for your decision";
|
|
12699
13929
|
}
|
|
12700
13930
|
function taskStateWord(state) {
|
|
12701
13931
|
switch (state) {
|
|
@@ -12791,10 +14021,10 @@ var CALLS_HEADING2 = "Calls made without asking you";
|
|
|
12791
14021
|
var CALLS_SUBHEADING = "These are the decisions the agents recorded taking on their own, each against a stated default. Halfcycle lists what they recorded; it cannot see a call an agent did not record.";
|
|
12792
14022
|
var NO_CALLS_LINE2 = "No agent recorded a call made without asking in this phase.";
|
|
12793
14023
|
function callLines2(call) {
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
];
|
|
14024
|
+
const undo = callUndoLine({ by: call.by, undoable: call.undoable, how: call.how });
|
|
14025
|
+
if (call.kind === "listed")
|
|
14026
|
+
return [autonomyC8(call.by, call.question, call.chose), undo];
|
|
14027
|
+
return [callMadeLine({ by: call.by, question: call.question, chose: call.chose, statedDefault: call.statedDefault }), undo];
|
|
12798
14028
|
}
|
|
12799
14029
|
function callsSectionLines(calls) {
|
|
12800
14030
|
const lines = [CALLS_HEADING2, CALLS_SUBHEADING];
|
|
@@ -13010,9 +14240,9 @@ async function runReviewAnswer(repoRoot, phase, part, answer, deps) {
|
|
|
13010
14240
|
}
|
|
13011
14241
|
|
|
13012
14242
|
// dist/uninstall.js
|
|
13013
|
-
import { lstatSync as lstatSync3, readFileSync as
|
|
13014
|
-
import { homedir as
|
|
13015
|
-
import { dirname as dirname7, join as
|
|
14243
|
+
import { lstatSync as lstatSync3, readFileSync as readFileSync19, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14244
|
+
import { homedir as homedir4 } from "node:os";
|
|
14245
|
+
import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
|
|
13016
14246
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
13017
14247
|
function parseUninstallArgs(args2) {
|
|
13018
14248
|
let removeCredential = false;
|
|
@@ -13038,7 +14268,7 @@ function lexists(path) {
|
|
|
13038
14268
|
function symlinkOnPath(root, rel) {
|
|
13039
14269
|
let current = root;
|
|
13040
14270
|
for (const segment of rel.split("/")) {
|
|
13041
|
-
current =
|
|
14271
|
+
current = join22(current, segment);
|
|
13042
14272
|
try {
|
|
13043
14273
|
if (lstatSync3(current).isSymbolicLink())
|
|
13044
14274
|
return true;
|
|
@@ -13060,13 +14290,13 @@ function readMergedJson(targetRepo, rule) {
|
|
|
13060
14290
|
const { rel } = rule;
|
|
13061
14291
|
if (symlinkOnPath(targetRepo, rel))
|
|
13062
14292
|
return { state: "symlink" };
|
|
13063
|
-
const path =
|
|
14293
|
+
const path = join22(targetRepo, rel);
|
|
13064
14294
|
if (!lexists(path))
|
|
13065
14295
|
return { state: "absent" };
|
|
13066
14296
|
let text;
|
|
13067
14297
|
let value;
|
|
13068
14298
|
try {
|
|
13069
|
-
text =
|
|
14299
|
+
text = readFileSync19(path, "utf-8");
|
|
13070
14300
|
} catch {
|
|
13071
14301
|
text = "";
|
|
13072
14302
|
}
|
|
@@ -13083,15 +14313,15 @@ function readMergedJson(targetRepo, rule) {
|
|
|
13083
14313
|
}
|
|
13084
14314
|
function validate(options, rules) {
|
|
13085
14315
|
const { targetRepo, removeCredential, home } = options;
|
|
13086
|
-
const manifestPath =
|
|
13087
|
-
const pinPath =
|
|
14316
|
+
const manifestPath = join22(targetRepo, INSTALL_MANIFEST_REL);
|
|
14317
|
+
const pinPath = join22(targetRepo, BUNDLE_PIN_REL);
|
|
13088
14318
|
if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
|
|
13089
14319
|
throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
|
|
13090
14320
|
}
|
|
13091
14321
|
let manifest = null;
|
|
13092
14322
|
if (lexists(manifestPath)) {
|
|
13093
14323
|
try {
|
|
13094
|
-
manifest = parseManifest(
|
|
14324
|
+
manifest = parseManifest(readFileSync19(manifestPath, "utf-8"));
|
|
13095
14325
|
} catch (err) {
|
|
13096
14326
|
const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
|
|
13097
14327
|
throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
|
|
@@ -13101,7 +14331,7 @@ function validate(options, rules) {
|
|
|
13101
14331
|
let pinReadable = true;
|
|
13102
14332
|
if (lexists(pinPath)) {
|
|
13103
14333
|
try {
|
|
13104
|
-
const pin = JSON.parse(
|
|
14334
|
+
const pin = JSON.parse(readFileSync19(pinPath, "utf-8"));
|
|
13105
14335
|
const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
13106
14336
|
engagementId = typeof id === "string" && id !== "" ? id : void 0;
|
|
13107
14337
|
} catch {
|
|
@@ -13125,7 +14355,7 @@ function validate(options, rules) {
|
|
|
13125
14355
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
|
|
13126
14356
|
}
|
|
13127
14357
|
if (engagementId !== void 0) {
|
|
13128
|
-
const expectedParent =
|
|
14358
|
+
const expectedParent = join22(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
13129
14359
|
if (dirname7(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
13130
14360
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
13131
14361
|
}
|
|
@@ -13149,7 +14379,7 @@ function mergedFileRules(writeSet) {
|
|
|
13149
14379
|
function writeBack(targetRepo, rel, current, next, lists, name) {
|
|
13150
14380
|
if (next === current)
|
|
13151
14381
|
return;
|
|
13152
|
-
const path =
|
|
14382
|
+
const path = join22(targetRepo, rel);
|
|
13153
14383
|
try {
|
|
13154
14384
|
if (next === null) {
|
|
13155
14385
|
rmSync6(path);
|
|
@@ -13166,7 +14396,7 @@ function statusLineClaimedFromTree(targetRepo, writeSet) {
|
|
|
13166
14396
|
try {
|
|
13167
14397
|
if (symlinkOnPath(targetRepo, SETTINGS_REL))
|
|
13168
14398
|
return {};
|
|
13169
|
-
const value = JSON.parse(
|
|
14399
|
+
const value = JSON.parse(readFileSync19(join22(targetRepo, SETTINGS_REL), "utf-8"));
|
|
13170
14400
|
if (!isPlainObject3(value) || !("statusLine" in value))
|
|
13171
14401
|
return {};
|
|
13172
14402
|
const hash = canonicalSha256(value["statusLine"]);
|
|
@@ -13180,7 +14410,7 @@ function noRecord(targetRepo, writeSet) {
|
|
|
13180
14410
|
const claim = (rel, header, lines) => {
|
|
13181
14411
|
let text;
|
|
13182
14412
|
try {
|
|
13183
|
-
text = symlinkOnPath(targetRepo, rel) ? "" :
|
|
14413
|
+
text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync19(join22(targetRepo, rel), "utf-8");
|
|
13184
14414
|
} catch {
|
|
13185
14415
|
text = "";
|
|
13186
14416
|
}
|
|
@@ -13233,11 +14463,11 @@ function ourMcpServers(value) {
|
|
|
13233
14463
|
}
|
|
13234
14464
|
function unmergeSettings(value, rec, writeSet) {
|
|
13235
14465
|
let changed = false;
|
|
13236
|
-
const
|
|
14466
|
+
const refused3 = [];
|
|
13237
14467
|
const commands = new Set(writeSet.hookCommands);
|
|
13238
14468
|
const denyHashes = rec.denyAddedSha256.filter((h) => writeSet.denyRuleSha256.has(h));
|
|
13239
14469
|
if (denyHashes.length < rec.denyAddedSha256.length) {
|
|
13240
|
-
|
|
14470
|
+
refused3.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
|
|
13241
14471
|
}
|
|
13242
14472
|
const hooks = value["hooks"];
|
|
13243
14473
|
if (isPlainObject3(hooks)) {
|
|
@@ -13307,7 +14537,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
13307
14537
|
value["$schema"] = rec.schemaBefore;
|
|
13308
14538
|
changed = true;
|
|
13309
14539
|
}
|
|
13310
|
-
return { changed, refused:
|
|
14540
|
+
return { changed, refused: refused3 };
|
|
13311
14541
|
}
|
|
13312
14542
|
function unmergeMcp(value, rec) {
|
|
13313
14543
|
const servers = value["mcpServers"];
|
|
@@ -13411,7 +14641,7 @@ function lastOccurrence(text, block) {
|
|
|
13411
14641
|
}
|
|
13412
14642
|
function coveredPathExists(targetRepo, line) {
|
|
13413
14643
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
13414
|
-
return path !== "" && lexists(
|
|
14644
|
+
return path !== "" && lexists(join22(targetRepo, path));
|
|
13415
14645
|
}
|
|
13416
14646
|
function selfBuildMergedFileRules() {
|
|
13417
14647
|
return {
|
|
@@ -13424,7 +14654,7 @@ function removeSelfBuildOwnedFile(targetRepo, rel, lists, name) {
|
|
|
13424
14654
|
name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
|
|
13425
14655
|
return;
|
|
13426
14656
|
}
|
|
13427
|
-
const abs =
|
|
14657
|
+
const abs = join22(targetRepo, rel);
|
|
13428
14658
|
let isFile;
|
|
13429
14659
|
try {
|
|
13430
14660
|
isFile = lstatSync3(abs).isFile();
|
|
@@ -13448,10 +14678,10 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
|
|
|
13448
14678
|
name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
|
|
13449
14679
|
return;
|
|
13450
14680
|
}
|
|
13451
|
-
const abs =
|
|
14681
|
+
const abs = join22(targetRepo, rel);
|
|
13452
14682
|
let bytes;
|
|
13453
14683
|
try {
|
|
13454
|
-
bytes =
|
|
14684
|
+
bytes = readFileSync19(abs);
|
|
13455
14685
|
} catch {
|
|
13456
14686
|
return;
|
|
13457
14687
|
}
|
|
@@ -13473,7 +14703,7 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
|
|
|
13473
14703
|
function removeSelfBuildEmptyDir(targetRepo, dir, lists, name) {
|
|
13474
14704
|
if (symlinkOnPath(targetRepo, dir))
|
|
13475
14705
|
return;
|
|
13476
|
-
const abs =
|
|
14706
|
+
const abs = join22(targetRepo, dir);
|
|
13477
14707
|
try {
|
|
13478
14708
|
if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
13479
14709
|
return;
|
|
@@ -13507,9 +14737,9 @@ function uninstallSelfBuild(options) {
|
|
|
13507
14737
|
throw err;
|
|
13508
14738
|
}
|
|
13509
14739
|
const { targetRepo } = options;
|
|
13510
|
-
const pinExisted = lexists(
|
|
13511
|
-
const name = (
|
|
13512
|
-
|
|
14740
|
+
const pinExisted = lexists(join22(targetRepo, BUNDLE_PIN_REL));
|
|
14741
|
+
const name = (list2, text, _path) => {
|
|
14742
|
+
list2.push(text);
|
|
13513
14743
|
};
|
|
13514
14744
|
applyMergedJson(targetRepo, rules.mcp, checked.mcp, true, result, name, (value) => ({
|
|
13515
14745
|
changed: unmergeMcp(value, BEFORE_UNKNOWN_MCP).changed,
|
|
@@ -13557,15 +14787,15 @@ function uninstall(options) {
|
|
|
13557
14787
|
const manifest = checked.manifest ?? noRecord(targetRepo, writeSet);
|
|
13558
14788
|
const deleteWhenEmpty = (created) => created || checked.manifest === null;
|
|
13559
14789
|
const named = /* @__PURE__ */ new Set();
|
|
13560
|
-
const name = (
|
|
13561
|
-
|
|
14790
|
+
const name = (list2, text, path) => {
|
|
14791
|
+
list2.push(text);
|
|
13562
14792
|
if (path !== void 0)
|
|
13563
14793
|
named.add(path);
|
|
13564
14794
|
};
|
|
13565
14795
|
const settingsEmptyGoes = deleteWhenEmpty(manifest.settings.created);
|
|
13566
14796
|
applyMergedJson(targetRepo, rules.settings, checked.settings, settingsEmptyGoes, result, name, (value) => {
|
|
13567
|
-
const { changed, refused:
|
|
13568
|
-
for (const note of
|
|
14797
|
+
const { changed, refused: refused3 } = unmergeSettings(value, manifest.settings, writeSet);
|
|
14798
|
+
for (const note of refused3)
|
|
13569
14799
|
name(result.leftAlone, note);
|
|
13570
14800
|
return { changed, adopted: manifest.settings.adopted };
|
|
13571
14801
|
});
|
|
@@ -13610,7 +14840,7 @@ function uninstall(options) {
|
|
|
13610
14840
|
const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
|
|
13611
14841
|
trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
|
|
13612
14842
|
const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0 && result.keptIgnoreLines.length === 0;
|
|
13613
|
-
if (checked.manifest === null && !lexists(
|
|
14843
|
+
if (checked.manifest === null && !lexists(join22(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
|
|
13614
14844
|
return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
|
|
13615
14845
|
}
|
|
13616
14846
|
const credential = handleCredential(options, checked);
|
|
@@ -13618,7 +14848,7 @@ function uninstall(options) {
|
|
|
13618
14848
|
if (result.failed.length === 0 && !credentialFailed) {
|
|
13619
14849
|
removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
|
|
13620
14850
|
try {
|
|
13621
|
-
rmSync6(
|
|
14851
|
+
rmSync6(join22(targetRepo, INSTALL_MANIFEST_REL), { force: true });
|
|
13622
14852
|
named.add(INSTALL_MANIFEST_REL);
|
|
13623
14853
|
} catch {
|
|
13624
14854
|
name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
|
|
@@ -13653,7 +14883,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
13653
14883
|
return;
|
|
13654
14884
|
const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
|
|
13655
14885
|
writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
|
|
13656
|
-
if (adopted && lexists(
|
|
14886
|
+
if (adopted && lexists(join22(targetRepo, rel))) {
|
|
13657
14887
|
name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
|
|
13658
14888
|
}
|
|
13659
14889
|
}
|
|
@@ -13666,7 +14896,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
13666
14896
|
name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
|
|
13667
14897
|
return;
|
|
13668
14898
|
}
|
|
13669
|
-
const abs =
|
|
14899
|
+
const abs = join22(targetRepo, path);
|
|
13670
14900
|
let isFile;
|
|
13671
14901
|
try {
|
|
13672
14902
|
isFile = lstatSync3(abs).isFile();
|
|
@@ -13680,7 +14910,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
13680
14910
|
if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
|
|
13681
14911
|
let bytes;
|
|
13682
14912
|
try {
|
|
13683
|
-
bytes =
|
|
14913
|
+
bytes = readFileSync19(abs);
|
|
13684
14914
|
} catch {
|
|
13685
14915
|
name(lists.failed, path, path);
|
|
13686
14916
|
return;
|
|
@@ -13700,7 +14930,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
13700
14930
|
function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
|
|
13701
14931
|
if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
|
|
13702
14932
|
return;
|
|
13703
|
-
const abs =
|
|
14933
|
+
const abs = join22(targetRepo, dir);
|
|
13704
14934
|
try {
|
|
13705
14935
|
if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
13706
14936
|
return;
|
|
@@ -13719,10 +14949,10 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
|
|
|
13719
14949
|
name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
|
|
13720
14950
|
return [];
|
|
13721
14951
|
}
|
|
13722
|
-
const path =
|
|
14952
|
+
const path = join22(targetRepo, rel);
|
|
13723
14953
|
let before;
|
|
13724
14954
|
try {
|
|
13725
|
-
before =
|
|
14955
|
+
before = readFileSync19(path, "utf-8");
|
|
13726
14956
|
} catch {
|
|
13727
14957
|
return [];
|
|
13728
14958
|
}
|
|
@@ -13766,16 +14996,16 @@ function filesUnder(root, rel) {
|
|
|
13766
14996
|
const walk = (dirRel) => {
|
|
13767
14997
|
let names;
|
|
13768
14998
|
try {
|
|
13769
|
-
if (!lstatSync3(
|
|
14999
|
+
if (!lstatSync3(join22(root, dirRel)).isDirectory())
|
|
13770
15000
|
return;
|
|
13771
|
-
names = readdirSync(
|
|
15001
|
+
names = readdirSync(join22(root, dirRel));
|
|
13772
15002
|
} catch {
|
|
13773
15003
|
return;
|
|
13774
15004
|
}
|
|
13775
15005
|
for (const entry of names.sort()) {
|
|
13776
15006
|
const childRel = `${dirRel}/${entry}`;
|
|
13777
15007
|
try {
|
|
13778
|
-
if (lstatSync3(
|
|
15008
|
+
if (lstatSync3(join22(root, childRel)).isDirectory())
|
|
13779
15009
|
walk(childRel);
|
|
13780
15010
|
else
|
|
13781
15011
|
out.push(childRel);
|
|
@@ -13788,7 +15018,7 @@ function filesUnder(root, rel) {
|
|
|
13788
15018
|
return out;
|
|
13789
15019
|
}
|
|
13790
15020
|
function shownPath(path, home) {
|
|
13791
|
-
const base = home ??
|
|
15021
|
+
const base = home ?? homedir4();
|
|
13792
15022
|
const rel = relative6(base, path);
|
|
13793
15023
|
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
|
|
13794
15024
|
return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
|
|
@@ -13847,8 +15077,8 @@ function renderUninstallReport(result, home) {
|
|
|
13847
15077
|
}
|
|
13848
15078
|
|
|
13849
15079
|
// dist/banner-facts.js
|
|
13850
|
-
import { readFileSync as
|
|
13851
|
-
import { basename as basename4, join as
|
|
15080
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
15081
|
+
import { basename as basename4, join as join23, resolve as resolve6 } from "node:path";
|
|
13852
15082
|
var BRAND_URL = "halfcycle.ai";
|
|
13853
15083
|
var TAGLINE = [
|
|
13854
15084
|
"Fewer cycles.",
|
|
@@ -13861,7 +15091,7 @@ var TAGLINE_NARROW = [
|
|
|
13861
15091
|
];
|
|
13862
15092
|
function bundleVersion() {
|
|
13863
15093
|
try {
|
|
13864
|
-
const pkg = JSON.parse(
|
|
15094
|
+
const pkg = JSON.parse(readFileSync20(join23(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
13865
15095
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
13866
15096
|
} catch {
|
|
13867
15097
|
return void 0;
|
|
@@ -14005,13 +15235,13 @@ function runPaceChild(binary, credential, repoRoot, args2, deps = {}) {
|
|
|
14005
15235
|
if (binary instanceof Error)
|
|
14006
15236
|
return Promise.resolve({ code: 1, stdout: "" });
|
|
14007
15237
|
const err = deps.stderr ?? process.stderr;
|
|
14008
|
-
return new Promise((
|
|
15238
|
+
return new Promise((resolve8) => {
|
|
14009
15239
|
let settled = false;
|
|
14010
15240
|
const done = (output) => {
|
|
14011
15241
|
if (settled)
|
|
14012
15242
|
return;
|
|
14013
15243
|
settled = true;
|
|
14014
|
-
|
|
15244
|
+
resolve8(output);
|
|
14015
15245
|
};
|
|
14016
15246
|
try {
|
|
14017
15247
|
const child = spawn3(process.execPath, [binary, "pace", ...args2], {
|
|
@@ -14090,6 +15320,424 @@ async function runPace(repoRoot, start, deps) {
|
|
|
14090
15320
|
`, stderr: "", exitCode: 0 };
|
|
14091
15321
|
}
|
|
14092
15322
|
|
|
15323
|
+
// dist/restore.js
|
|
15324
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
15325
|
+
import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync21, readlinkSync, realpathSync as realpathSync5, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
|
|
15326
|
+
import { homedir as homedir5 } from "node:os";
|
|
15327
|
+
import { join as join24, resolve as resolve7 } from "node:path";
|
|
15328
|
+
var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
|
|
15329
|
+
function parseRestoreArgs(rest) {
|
|
15330
|
+
if (rest.length > 1)
|
|
15331
|
+
return { ok: false };
|
|
15332
|
+
const first = rest[0];
|
|
15333
|
+
if (first !== void 0 && first.startsWith("-"))
|
|
15334
|
+
return { ok: false };
|
|
15335
|
+
return { ok: true, id: first };
|
|
15336
|
+
}
|
|
15337
|
+
var LIST_COMMAND = `${HALFCYCLE_COMMAND} restore`;
|
|
15338
|
+
function runRestore(cwd, id, options = {}) {
|
|
15339
|
+
const now = options.now ?? Date.now;
|
|
15340
|
+
const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
|
|
15341
|
+
if (!top.ok || top.text.trim() === "") {
|
|
15342
|
+
return refused2("This folder is not in a project kept with git, so Halfcycle has saved nothing here to bring back.");
|
|
15343
|
+
}
|
|
15344
|
+
const root = realOrSelf(top.text.trim());
|
|
15345
|
+
const engagementId = pinnedEngagement(root);
|
|
15346
|
+
if (engagementId === null) {
|
|
15347
|
+
return refused2(`Halfcycle is not set up in this project, so it has saved nothing here. To set it up, run: ${HALFCYCLE_COMMAND}`);
|
|
15348
|
+
}
|
|
15349
|
+
const folder = join24(options.home ?? homedir5(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
|
|
15350
|
+
const refs = savedRefs(root);
|
|
15351
|
+
if (refs === null)
|
|
15352
|
+
return refused2("Halfcycle could not read the saved copies in this project, so nothing was put back.");
|
|
15353
|
+
pruneAtRestore(root, folder, refs, now());
|
|
15354
|
+
for (const [saveId, madeAt] of [...refs]) {
|
|
15355
|
+
const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: 0, folderRepository: null }, { repository: null, now: now() });
|
|
15356
|
+
if (answer !== "keep")
|
|
15357
|
+
refs.delete(saveId);
|
|
15358
|
+
}
|
|
15359
|
+
if (id === void 0)
|
|
15360
|
+
return list(root, folder, refs);
|
|
15361
|
+
return restoreOne(root, folder, refs, id);
|
|
15362
|
+
}
|
|
15363
|
+
function refused2(line) {
|
|
15364
|
+
return { stdout: "", stderr: `${line}
|
|
15365
|
+
`, exitCode: 1 };
|
|
15366
|
+
}
|
|
15367
|
+
function list(root, folder, refs) {
|
|
15368
|
+
const rows = [];
|
|
15369
|
+
for (const [id, at] of refs) {
|
|
15370
|
+
const saved = readSaved(folder, id);
|
|
15371
|
+
if (saved !== null)
|
|
15372
|
+
rows.push({ id, at, what: saved.what });
|
|
15373
|
+
}
|
|
15374
|
+
if (rows.length === 0) {
|
|
15375
|
+
return {
|
|
15376
|
+
stdout: "Nothing has been saved in this project in the last 7 days, so there is nothing to bring back.\n",
|
|
15377
|
+
stderr: "",
|
|
15378
|
+
exitCode: 0
|
|
15379
|
+
};
|
|
15380
|
+
}
|
|
15381
|
+
rows.sort((a, b) => b.at - a.at || (a.id < b.id ? 1 : -1));
|
|
15382
|
+
const lines = rows.map((r) => ` ${r.id} saved ${whenOf(r.at)}: ${printable(r.what)}`);
|
|
15383
|
+
return {
|
|
15384
|
+
stdout: `Saved copies you can bring back, newest first (each is kept for 7 days):
|
|
15385
|
+
${lines.join("\n")}
|
|
15386
|
+
To bring one back, run: ${LIST_COMMAND} <id>
|
|
15387
|
+
`,
|
|
15388
|
+
stderr: "",
|
|
15389
|
+
exitCode: 0
|
|
15390
|
+
};
|
|
15391
|
+
}
|
|
15392
|
+
function whenOf(ms) {
|
|
15393
|
+
if (!Number.isFinite(ms))
|
|
15394
|
+
return "at an unknown time";
|
|
15395
|
+
return `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
15396
|
+
}
|
|
15397
|
+
function printable(text) {
|
|
15398
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
15399
|
+
}
|
|
15400
|
+
function restoreOne(root, folder, refs, rawId) {
|
|
15401
|
+
const valid = savedIdSchema.safeParse(rawId);
|
|
15402
|
+
const named = valid.success ? `"${rawId}"` : "with that name";
|
|
15403
|
+
if (!valid.success || !refs.has(rawId)) {
|
|
15404
|
+
return refused2(`No saved copy ${named} is kept in this project. Saved copies are kept for 7 days. To see the ones there are, run: ${LIST_COMMAND}`);
|
|
15405
|
+
}
|
|
15406
|
+
const id = valid.data;
|
|
15407
|
+
const saved = readSaved(folder, id);
|
|
15408
|
+
if (saved === null)
|
|
15409
|
+
return refused2(`The saved copy "${id}" could not be read, so nothing was put back.`);
|
|
15410
|
+
const ref = savedRefOf(id);
|
|
15411
|
+
const out = { back: [], already: [], left: [] };
|
|
15412
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15413
|
+
const fromCommit = [];
|
|
15414
|
+
for (const p of saved.paths) {
|
|
15415
|
+
if (seen.has(p.path))
|
|
15416
|
+
continue;
|
|
15417
|
+
seen.add(p.path);
|
|
15418
|
+
if (insideGitFolder(p.path)) {
|
|
15419
|
+
out.left.push(`${p.path}: it is inside git's own folder, which restore never writes`);
|
|
15420
|
+
continue;
|
|
15421
|
+
}
|
|
15422
|
+
if (p.keptIn === "commit")
|
|
15423
|
+
fromCommit.push(p.path);
|
|
15424
|
+
else
|
|
15425
|
+
restoreCopy(root, join24(folder, id), p.path, out);
|
|
15426
|
+
}
|
|
15427
|
+
if (fromCommit.length > 0)
|
|
15428
|
+
restoreFromCommit(root, ref, fromCommit, out);
|
|
15429
|
+
restoreLines(root, ref, saved, out);
|
|
15430
|
+
const parts = [];
|
|
15431
|
+
if (out.back.length > 0)
|
|
15432
|
+
parts.push(`Put back from the saved copy ${id}:
|
|
15433
|
+
${out.back.map((l) => ` ${l}`).join("\n")}`);
|
|
15434
|
+
else
|
|
15435
|
+
parts.push(`Nothing was put back from the saved copy ${id}.`);
|
|
15436
|
+
if (out.already.length > 0)
|
|
15437
|
+
parts.push(`Already as saved:
|
|
15438
|
+
${out.already.map((l) => ` ${l}`).join("\n")}`);
|
|
15439
|
+
if (out.left.length > 0)
|
|
15440
|
+
parts.push(`Left as they are:
|
|
15441
|
+
${out.left.map((l) => ` ${l}`).join("\n")}`);
|
|
15442
|
+
return { stdout: `${parts.join("\n")}
|
|
15443
|
+
`, stderr: "", exitCode: out.left.length > 0 ? 1 : 0 };
|
|
15444
|
+
}
|
|
15445
|
+
var MOVE_AWAY = "move it away and run this again to bring back the saved one";
|
|
15446
|
+
function insideGitFolder(rel) {
|
|
15447
|
+
return rel.split(/[\\/]/).some(isGitFolderSegment);
|
|
15448
|
+
}
|
|
15449
|
+
function makeFolders(root, rel) {
|
|
15450
|
+
const segments = rel.split("/");
|
|
15451
|
+
let at = root;
|
|
15452
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
15453
|
+
at = join24(at, segments[i]);
|
|
15454
|
+
const shown2 = segments.slice(0, i + 1).join("/");
|
|
15455
|
+
let st = lstatOrNull3(at);
|
|
15456
|
+
if (st === null) {
|
|
15457
|
+
try {
|
|
15458
|
+
mkdirSync11(at);
|
|
15459
|
+
} catch {
|
|
15460
|
+
}
|
|
15461
|
+
st = lstatOrNull3(at);
|
|
15462
|
+
}
|
|
15463
|
+
if (st === null)
|
|
15464
|
+
return "its folder could not be made";
|
|
15465
|
+
if (st.isSymbolicLink())
|
|
15466
|
+
return `${shown2} is a link now, and restore never writes through a link`;
|
|
15467
|
+
if (!st.isDirectory())
|
|
15468
|
+
return `${shown2} is a file now, where a folder is needed; ${MOVE_AWAY}`;
|
|
15469
|
+
}
|
|
15470
|
+
return null;
|
|
15471
|
+
}
|
|
15472
|
+
function place(root, rel, content, out) {
|
|
15473
|
+
const abs = join24(root, rel);
|
|
15474
|
+
const there = lstatOrNull3(abs);
|
|
15475
|
+
if (there !== null) {
|
|
15476
|
+
if (sameAs(abs, there, content))
|
|
15477
|
+
out.already.push(rel);
|
|
15478
|
+
else
|
|
15479
|
+
out.left.push(`${rel}: something is there now; ${MOVE_AWAY}`);
|
|
15480
|
+
return;
|
|
15481
|
+
}
|
|
15482
|
+
const blocked = makeFolders(root, rel);
|
|
15483
|
+
if (blocked !== null) {
|
|
15484
|
+
out.left.push(`${rel}: ${blocked}`);
|
|
15485
|
+
return;
|
|
15486
|
+
}
|
|
15487
|
+
try {
|
|
15488
|
+
if ("link" in content) {
|
|
15489
|
+
symlinkSync(content.link, abs);
|
|
15490
|
+
} else {
|
|
15491
|
+
const fd = openSync3(abs, "wx", 384);
|
|
15492
|
+
try {
|
|
15493
|
+
let off = 0;
|
|
15494
|
+
while (off < content.bytes.length)
|
|
15495
|
+
off += writeSync2(fd, content.bytes, off, content.bytes.length - off);
|
|
15496
|
+
fchmodSync(fd, content.mode);
|
|
15497
|
+
} finally {
|
|
15498
|
+
closeSync3(fd);
|
|
15499
|
+
}
|
|
15500
|
+
}
|
|
15501
|
+
out.back.push(rel);
|
|
15502
|
+
} catch (e) {
|
|
15503
|
+
const code = e.code;
|
|
15504
|
+
out.left.push(code === "EEXIST" ? `${rel}: something is there now; ${MOVE_AWAY}` : `${rel}: it could not be written here`);
|
|
15505
|
+
}
|
|
15506
|
+
}
|
|
15507
|
+
function sameAs(abs, st, content) {
|
|
15508
|
+
try {
|
|
15509
|
+
if ("link" in content)
|
|
15510
|
+
return st.isSymbolicLink() && readlinkSync(abs) === content.link;
|
|
15511
|
+
return st.isFile() && readFileSync21(abs).equals(content.bytes);
|
|
15512
|
+
} catch {
|
|
15513
|
+
return false;
|
|
15514
|
+
}
|
|
15515
|
+
}
|
|
15516
|
+
function restoreCopy(root, saveDir, rel, out) {
|
|
15517
|
+
const segments = rel.split("/");
|
|
15518
|
+
let at = saveDir;
|
|
15519
|
+
const chain = [SAVED_COPIES_DIR_NAME, ...segments];
|
|
15520
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
15521
|
+
at = join24(at, chain[i]);
|
|
15522
|
+
const st2 = lstatOrNull3(at);
|
|
15523
|
+
if (st2 === null || st2.isSymbolicLink() || !st2.isDirectory()) {
|
|
15524
|
+
out.left.push(`${rel}: the saved copy does not hold it`);
|
|
15525
|
+
return;
|
|
15526
|
+
}
|
|
15527
|
+
}
|
|
15528
|
+
const src = join24(at, chain[chain.length - 1]);
|
|
15529
|
+
const st = lstatOrNull3(src);
|
|
15530
|
+
try {
|
|
15531
|
+
if (st !== null && st.isSymbolicLink())
|
|
15532
|
+
return place(root, rel, { link: readlinkSync(src) }, out);
|
|
15533
|
+
if (st !== null && st.isFile())
|
|
15534
|
+
return place(root, rel, { bytes: readFileSync21(src), mode: st.mode & 511 }, out);
|
|
15535
|
+
} catch {
|
|
15536
|
+
}
|
|
15537
|
+
out.left.push(`${rel}: the saved copy does not hold it`);
|
|
15538
|
+
}
|
|
15539
|
+
function restoreFromCommit(root, ref, rels, out) {
|
|
15540
|
+
const entries = /* @__PURE__ */ new Map();
|
|
15541
|
+
for (let i = 0; i < rels.length; i += 200) {
|
|
15542
|
+
const chunk = rels.slice(i, i + 200);
|
|
15543
|
+
const listed = git2(root, ["--literal-pathspecs", "ls-tree", "-z", "--full-tree", `${ref}^{commit}`, "--", ...chunk]);
|
|
15544
|
+
if (!listed.ok) {
|
|
15545
|
+
for (const rel of rels)
|
|
15546
|
+
out.left.push(`${rel}: the saved copy could not be read`);
|
|
15547
|
+
return;
|
|
15548
|
+
}
|
|
15549
|
+
for (const rec of listed.text.split("\0")) {
|
|
15550
|
+
const tab = rec.indexOf(" ");
|
|
15551
|
+
if (tab < 0)
|
|
15552
|
+
continue;
|
|
15553
|
+
const [mode, type, object] = rec.slice(0, tab).split(" ");
|
|
15554
|
+
if (type === "blob" && mode !== void 0 && object !== void 0)
|
|
15555
|
+
entries.set(rec.slice(tab + 1), { mode, object });
|
|
15556
|
+
}
|
|
15557
|
+
}
|
|
15558
|
+
const objects = [...new Set([...entries.values()].map((e) => e.object))];
|
|
15559
|
+
const blobs = objects.length === 0 ? /* @__PURE__ */ new Map() : readBlobs(root, objects);
|
|
15560
|
+
for (const rel of rels) {
|
|
15561
|
+
const entry = entries.get(rel);
|
|
15562
|
+
const bytes = entry === void 0 ? void 0 : blobs?.get(entry.object);
|
|
15563
|
+
if (entry === void 0 || bytes === void 0) {
|
|
15564
|
+
out.left.push(`${rel}: the saved copy does not hold it`);
|
|
15565
|
+
continue;
|
|
15566
|
+
}
|
|
15567
|
+
if (entry.mode === "120000")
|
|
15568
|
+
place(root, rel, { link: bytes.toString("utf-8") }, out);
|
|
15569
|
+
else
|
|
15570
|
+
place(root, rel, { bytes, mode: entry.mode === "100755" ? 493 : 420 }, out);
|
|
15571
|
+
}
|
|
15572
|
+
}
|
|
15573
|
+
function readBlobs(root, objects) {
|
|
15574
|
+
const r = spawnSync3("git", ["cat-file", "--batch"], {
|
|
15575
|
+
cwd: root,
|
|
15576
|
+
env: gitEnvForNamedRepo(),
|
|
15577
|
+
input: `${objects.join("\n")}
|
|
15578
|
+
`,
|
|
15579
|
+
maxBuffer: 1024 * 1024 * 1024
|
|
15580
|
+
});
|
|
15581
|
+
if (r.status !== 0 || !Buffer.isBuffer(r.stdout))
|
|
15582
|
+
return null;
|
|
15583
|
+
const out = /* @__PURE__ */ new Map();
|
|
15584
|
+
const buf = r.stdout;
|
|
15585
|
+
let at = 0;
|
|
15586
|
+
while (at < buf.length) {
|
|
15587
|
+
const nl = buf.indexOf(10, at);
|
|
15588
|
+
if (nl < 0)
|
|
15589
|
+
break;
|
|
15590
|
+
const [object, type, size] = buf.subarray(at, nl).toString("utf-8").split(" ");
|
|
15591
|
+
if (type !== "blob" || object === void 0 || size === void 0)
|
|
15592
|
+
return null;
|
|
15593
|
+
const n = Number(size);
|
|
15594
|
+
out.set(object, Buffer.from(buf.subarray(nl + 1, nl + 1 + n)));
|
|
15595
|
+
at = nl + 1 + n + 1;
|
|
15596
|
+
}
|
|
15597
|
+
return out;
|
|
15598
|
+
}
|
|
15599
|
+
function restoreLines(root, ref, saved, out) {
|
|
15600
|
+
if (saved.branches.length === 0)
|
|
15601
|
+
return;
|
|
15602
|
+
const parents = git2(root, ["rev-list", "--parents", "-n", "1", `${ref}^{commit}`]);
|
|
15603
|
+
const held = new Set(parents.ok ? parents.text.trim().split(/\s+/).slice(1) : []);
|
|
15604
|
+
for (const b of saved.branches) {
|
|
15605
|
+
const shown2 = `the line of work "${b.name}"`;
|
|
15606
|
+
const full = `refs/heads/${b.name}`;
|
|
15607
|
+
if (!git2(root, ["check-ref-format", full]).ok) {
|
|
15608
|
+
out.left.push(`${shown2}: its name cannot be used`);
|
|
15609
|
+
continue;
|
|
15610
|
+
}
|
|
15611
|
+
if (!held.has(b.commit)) {
|
|
15612
|
+
out.left.push(`${shown2}: the saved copy does not hold its commit`);
|
|
15613
|
+
continue;
|
|
15614
|
+
}
|
|
15615
|
+
const now = git2(root, ["rev-parse", "--verify", "--quiet", `${full}^{commit}`]);
|
|
15616
|
+
if (now.ok && now.text.trim() !== "") {
|
|
15617
|
+
if (now.text.trim() === b.commit)
|
|
15618
|
+
out.already.push(`${shown2}, at its saved commit ${b.commit.slice(0, 12)}`);
|
|
15619
|
+
else
|
|
15620
|
+
out.left.push(`${shown2}: one with this name exists now; rename it and run this again to bring back the saved one`);
|
|
15621
|
+
continue;
|
|
15622
|
+
}
|
|
15623
|
+
const made = git2(root, ["update-ref", "--no-deref", full, b.commit, ""]);
|
|
15624
|
+
if (made.ok)
|
|
15625
|
+
out.back.push(`${shown2}, at its saved commit ${b.commit.slice(0, 12)}`);
|
|
15626
|
+
else
|
|
15627
|
+
out.left.push(`${shown2}: it could not be made here, perhaps because another line of work's name is in the way`);
|
|
15628
|
+
}
|
|
15629
|
+
}
|
|
15630
|
+
function pinnedEngagement(root) {
|
|
15631
|
+
try {
|
|
15632
|
+
const pin = JSON.parse(readFileSync21(join24(root, BUNDLE_PIN_REL), "utf-8"));
|
|
15633
|
+
return isEngagementId(pin.engagementId) ? pin.engagementId : null;
|
|
15634
|
+
} catch {
|
|
15635
|
+
return null;
|
|
15636
|
+
}
|
|
15637
|
+
}
|
|
15638
|
+
function savedRefs(root) {
|
|
15639
|
+
const listed = git2(root, ["for-each-ref", "--format=%(refname)%00%(committerdate:unix)", SAVED_REF_PREFIX]);
|
|
15640
|
+
if (!listed.ok)
|
|
15641
|
+
return null;
|
|
15642
|
+
const refs = /* @__PURE__ */ new Map();
|
|
15643
|
+
for (const line of listed.text.split("\n")) {
|
|
15644
|
+
const [name, when] = line.split("\0");
|
|
15645
|
+
if (name === void 0 || when === void 0 || !name.startsWith(SAVED_REF_PREFIX))
|
|
15646
|
+
continue;
|
|
15647
|
+
const id = name.slice(SAVED_REF_PREFIX.length);
|
|
15648
|
+
if (savedIdSchema.safeParse(id).success)
|
|
15649
|
+
refs.set(id, Number(when) * 1e3);
|
|
15650
|
+
}
|
|
15651
|
+
return refs;
|
|
15652
|
+
}
|
|
15653
|
+
function readSaved(folder, id) {
|
|
15654
|
+
const dir = join24(folder, id);
|
|
15655
|
+
const st = lstatOrNull3(dir);
|
|
15656
|
+
if (st === null || !st.isDirectory())
|
|
15657
|
+
return null;
|
|
15658
|
+
const file = join24(dir, SAVED_FILE_NAME);
|
|
15659
|
+
const fst = lstatOrNull3(file);
|
|
15660
|
+
if (fst === null || !fst.isFile())
|
|
15661
|
+
return null;
|
|
15662
|
+
try {
|
|
15663
|
+
const parsed = savedSchema.safeParse(JSON.parse(readFileSync21(file, "utf-8")));
|
|
15664
|
+
return parsed.success && parsed.data.id === id ? parsed.data : null;
|
|
15665
|
+
} catch {
|
|
15666
|
+
return null;
|
|
15667
|
+
}
|
|
15668
|
+
}
|
|
15669
|
+
function pruneAtRestore(root, folder, refs, now) {
|
|
15670
|
+
try {
|
|
15671
|
+
const common = git2(root, ["rev-parse", "--git-common-dir"]);
|
|
15672
|
+
const repository = common.ok && common.text.trim() !== "" ? realOrSelf(resolve7(root, common.text.trim())) : null;
|
|
15673
|
+
let names = [];
|
|
15674
|
+
try {
|
|
15675
|
+
names = readdirSync2(folder);
|
|
15676
|
+
} catch {
|
|
15677
|
+
names = [];
|
|
15678
|
+
}
|
|
15679
|
+
const here = /* @__PURE__ */ new Set();
|
|
15680
|
+
for (const id of names) {
|
|
15681
|
+
if (!savedIdSchema.safeParse(id).success)
|
|
15682
|
+
continue;
|
|
15683
|
+
here.add(id);
|
|
15684
|
+
const dir = join24(folder, id);
|
|
15685
|
+
const st = lstatOrNull3(dir);
|
|
15686
|
+
const answer = savedPruneAnswer({
|
|
15687
|
+
refMadeAt: refs.get(id) ?? null,
|
|
15688
|
+
folderChangedAt: st !== null && st.isDirectory() ? st.mtimeMs : null,
|
|
15689
|
+
folderRepository: markedRepository(dir)
|
|
15690
|
+
}, { repository, now });
|
|
15691
|
+
if (answer === "remove-folder")
|
|
15692
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
15693
|
+
else if (answer === "remove-ref-then-folder" && git2(root, ["update-ref", "-d", savedRefOf(id)]).ok) {
|
|
15694
|
+
refs.delete(id);
|
|
15695
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
15696
|
+
} else if (answer === "remove-ref" && git2(root, ["update-ref", "-d", savedRefOf(id)]).ok) {
|
|
15697
|
+
refs.delete(id);
|
|
15698
|
+
}
|
|
15699
|
+
}
|
|
15700
|
+
for (const [id, madeAt] of [...refs]) {
|
|
15701
|
+
if (here.has(id))
|
|
15702
|
+
continue;
|
|
15703
|
+
const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: null, folderRepository: null }, { repository, now });
|
|
15704
|
+
if (answer === "remove-ref" && git2(root, ["update-ref", "-d", savedRefOf(id)]).ok)
|
|
15705
|
+
refs.delete(id);
|
|
15706
|
+
}
|
|
15707
|
+
} catch {
|
|
15708
|
+
}
|
|
15709
|
+
}
|
|
15710
|
+
function markedRepository(dir) {
|
|
15711
|
+
try {
|
|
15712
|
+
const text = readFileSync21(join24(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
|
|
15713
|
+
return text === "" ? null : text;
|
|
15714
|
+
} catch {
|
|
15715
|
+
return null;
|
|
15716
|
+
}
|
|
15717
|
+
}
|
|
15718
|
+
function git2(cwd, args2) {
|
|
15719
|
+
try {
|
|
15720
|
+
const r = spawnSync3("git", args2, { cwd, encoding: "utf-8", env: gitEnvForNamedRepo(), maxBuffer: 64 * 1024 * 1024 });
|
|
15721
|
+
return r.status === 0 ? { ok: true, text: r.stdout ?? "" } : { ok: false };
|
|
15722
|
+
} catch {
|
|
15723
|
+
return { ok: false };
|
|
15724
|
+
}
|
|
15725
|
+
}
|
|
15726
|
+
function lstatOrNull3(p) {
|
|
15727
|
+
try {
|
|
15728
|
+
return lstatSync4(p);
|
|
15729
|
+
} catch {
|
|
15730
|
+
return null;
|
|
15731
|
+
}
|
|
15732
|
+
}
|
|
15733
|
+
function realOrSelf(p) {
|
|
15734
|
+
try {
|
|
15735
|
+
return realpathSync5(p);
|
|
15736
|
+
} catch {
|
|
15737
|
+
return p;
|
|
15738
|
+
}
|
|
15739
|
+
}
|
|
15740
|
+
|
|
14093
15741
|
// dist/bin.js
|
|
14094
15742
|
var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
|
|
14095
15743
|
var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
|
|
@@ -14162,6 +15810,13 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
14162
15810
|
${PACE_USAGE}
|
|
14163
15811
|
suggest how many agents this build should run at once, and for how long; with no flag, how
|
|
14164
15812
|
many may start right now
|
|
15813
|
+
${HALFCYCLE_COMMAND} preview [folder]
|
|
15814
|
+
show what setting up this folder would add, change, remove or leave alone; writes nothing
|
|
15815
|
+
${HALFCYCLE_COMMAND} doctor
|
|
15816
|
+
check this folder: set up, signed in, connected, checks running, up to date; says what to do for each one that is not; changes nothing
|
|
15817
|
+
${RESTORE_USAGE}
|
|
15818
|
+
list the copies Halfcycle saved before a change it let through, or bring one back; never
|
|
15819
|
+
replaces a file or a line of work that exists now
|
|
14165
15820
|
${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
|
|
14166
15821
|
take out what the installer added to this project, keeping anything you changed
|
|
14167
15822
|
${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
|
|
@@ -14172,15 +15827,6 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
14172
15827
|
|
|
14173
15828
|
Support: hello@halfcycle.ai
|
|
14174
15829
|
`;
|
|
14175
|
-
function isHomeFolder(dir) {
|
|
14176
|
-
try {
|
|
14177
|
-
const target = realpathSync4(dir);
|
|
14178
|
-
const home = realpathSync4(homedir4());
|
|
14179
|
-
return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
|
|
14180
|
-
} catch {
|
|
14181
|
-
return false;
|
|
14182
|
-
}
|
|
14183
|
-
}
|
|
14184
15830
|
var RETIRED_VERBS = /* @__PURE__ */ new Map([
|
|
14185
15831
|
["ci", '"ci" was removed \u2014 CI setup is part of your project, not Halfcycle']
|
|
14186
15832
|
]);
|
|
@@ -14278,34 +15924,65 @@ ${USAGE}`);
|
|
|
14278
15924
|
process.exit(0);
|
|
14279
15925
|
return;
|
|
14280
15926
|
}
|
|
15927
|
+
if (cmd === "preview") {
|
|
15928
|
+
const outcome = runPreview(rest[0] ?? process.cwd(), { env: process.env, platform: process.platform });
|
|
15929
|
+
if ("refusal" in outcome) {
|
|
15930
|
+
process.stderr.write(`${outcome.refusal}
|
|
15931
|
+
`);
|
|
15932
|
+
process.exitCode = 1;
|
|
15933
|
+
return;
|
|
15934
|
+
}
|
|
15935
|
+
process.stdout.write(`${outcome.text}
|
|
15936
|
+
`);
|
|
15937
|
+
return;
|
|
15938
|
+
}
|
|
15939
|
+
if (cmd === "doctor") {
|
|
15940
|
+
const report = runDoctor(process.cwd(), { env: process.env, platform: process.platform });
|
|
15941
|
+
process.stdout.write(`${report.text}
|
|
15942
|
+
`);
|
|
15943
|
+
process.exitCode = report.exitCode;
|
|
15944
|
+
return;
|
|
15945
|
+
}
|
|
15946
|
+
if (cmd === "restore") {
|
|
15947
|
+
const parsed = parseRestoreArgs(rest);
|
|
15948
|
+
if (!parsed.ok) {
|
|
15949
|
+
process.stderr.write(`halfcycle restore: usage: ${RESTORE_USAGE}
|
|
15950
|
+
`);
|
|
15951
|
+
process.exitCode = 2;
|
|
15952
|
+
return;
|
|
15953
|
+
}
|
|
15954
|
+
const run = runRestore(process.cwd(), parsed.id);
|
|
15955
|
+
if (run.stdout !== "")
|
|
15956
|
+
process.stdout.write(run.stdout);
|
|
15957
|
+
if (run.stderr !== "")
|
|
15958
|
+
process.stderr.write(run.stderr);
|
|
15959
|
+
process.exitCode = run.exitCode;
|
|
15960
|
+
return;
|
|
15961
|
+
}
|
|
14281
15962
|
if (installArm) {
|
|
14282
15963
|
const engagementIdArg = positionals[1];
|
|
14283
15964
|
const engagementTypeRaw = positionals[2] ?? "client";
|
|
14284
|
-
if (!
|
|
14285
|
-
process.stderr.write(
|
|
15965
|
+
if (!existsSync12(targetRepo)) {
|
|
15966
|
+
process.stderr.write(`${folderMissingRefusal(targetRepo)}
|
|
14286
15967
|
`);
|
|
14287
15968
|
process.exit(1);
|
|
14288
15969
|
return;
|
|
14289
15970
|
}
|
|
14290
|
-
if (!
|
|
14291
|
-
process.stderr.write(
|
|
15971
|
+
if (!statSync2(targetRepo).isDirectory()) {
|
|
15972
|
+
process.stderr.write(`${folderIsFileRefusal(targetRepo)}
|
|
14292
15973
|
`);
|
|
14293
15974
|
process.exit(1);
|
|
14294
15975
|
return;
|
|
14295
15976
|
}
|
|
14296
15977
|
const selfBuild = isHalfcycleMonorepo(targetRepo);
|
|
14297
15978
|
if (selfBuild && !isSelfBuildInstallForm(engagementIdArg, positionals[2])) {
|
|
14298
|
-
process.stderr.write(
|
|
14299
|
-
The bare form installs into the CURRENT directory, and this directory is the
|
|
14300
|
-
product's own source tree \u2014 installing here would write over tracked files.
|
|
14301
|
-
To see what this CLI does: halfcycle --help
|
|
14302
|
-
To install into a real project: halfcycle <path-to-that-repo>
|
|
15979
|
+
process.stderr.write(`${monorepoRefusal(targetRepo)}
|
|
14303
15980
|
`);
|
|
14304
15981
|
process.exit(1);
|
|
14305
15982
|
return;
|
|
14306
15983
|
}
|
|
14307
15984
|
if (isHomeFolder(targetRepo)) {
|
|
14308
|
-
process.stderr.write(
|
|
15985
|
+
process.stderr.write(`${homeFolderRefusal(targetRepo)}
|
|
14309
15986
|
`);
|
|
14310
15987
|
process.exit(1);
|
|
14311
15988
|
return;
|
|
@@ -14324,7 +16001,7 @@ ${USAGE}`);
|
|
|
14324
16001
|
const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
|
|
14325
16002
|
` : "";
|
|
14326
16003
|
try {
|
|
14327
|
-
|
|
16004
|
+
writeSync3(2, `halfcycle: interrupted.
|
|
14328
16005
|
${kept}`);
|
|
14329
16006
|
} catch {
|
|
14330
16007
|
}
|
|
@@ -14573,7 +16250,7 @@ ${PROJECT_RESET_LINE}
|
|
|
14573
16250
|
process.stdout.write(`[halfcycle] Method version: ${credential.methodVersion}
|
|
14574
16251
|
`);
|
|
14575
16252
|
}
|
|
14576
|
-
await setupClaudePlan({ serviceUrl: credential?.serviceUrl ?? serviceUrl, engagementId, ownsProject, consoleUrl }, { interactive: () => isInteractiveRun(), ask: askLine, write: (text) => process.stdout.write(text) });
|
|
16253
|
+
await setupClaudePlan({ serviceUrl: credential?.serviceUrl ?? serviceUrl, engagementId, ownsProject, consoleUrl }, { interactive: () => isInteractiveRun(), assumeYes, ask: askLine, write: (text) => process.stdout.write(text) });
|
|
14577
16254
|
if (checks.gitInstalled) {
|
|
14578
16255
|
process.stdout.write(`[halfcycle] Git for Windows is installed. Open a new terminal before starting Claude Code.
|
|
14579
16256
|
`);
|