halfcycle 0.3.23 → 0.3.24
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 +885 -127
- package/dist/account-credential.d.ts +12 -12
- package/dist/account-credential.d.ts.map +1 -1
- package/dist/banner-facts.d.ts +9 -0
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +585 -180
- package/dist/bin.js.map +3 -3
- package/dist/build-record/template.d.ts +11 -0
- package/dist/build-record/template.d.ts.map +1 -1
- package/dist/ci-bind.d.ts +116 -0
- package/dist/ci-bind.d.ts.map +1 -0
- package/dist/cli-contract.d.ts +11 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/confirm-identity.d.ts +50 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +15 -4
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/device-signin.d.ts +13 -0
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +64 -85
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +338 -109
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +14 -11
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +22 -7
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/setup/manifest.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -30,6 +30,107 @@ var matcherSchema = z.discriminatedUnion("kind", [
|
|
|
30
30
|
llmMatcherSchema
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
+
// ../core/dist/credential-format.js
|
|
34
|
+
var HALFCYCLE_DIR_NAME = ".halfcycle";
|
|
35
|
+
var ENGAGEMENTS_DIR_NAME = "engagements";
|
|
36
|
+
var ENGAGEMENT_ENV_FILENAME = "env";
|
|
37
|
+
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
38
|
+
var ACCOUNT_STORE_FILENAME = "account.json";
|
|
39
|
+
var ENGAGEMENT_ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
40
|
+
function shq(value) {
|
|
41
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
42
|
+
}
|
|
43
|
+
function unquote(value) {
|
|
44
|
+
const v = value.trim();
|
|
45
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
46
|
+
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
47
|
+
}
|
|
48
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
49
|
+
return v.slice(1, -1);
|
|
50
|
+
return v;
|
|
51
|
+
}
|
|
52
|
+
function parseEnvText(raw) {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const line of raw.split("\n")) {
|
|
55
|
+
const eq = line.indexOf("=");
|
|
56
|
+
if (eq === -1)
|
|
57
|
+
continue;
|
|
58
|
+
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
59
|
+
if (key === "" || key.startsWith("#"))
|
|
60
|
+
continue;
|
|
61
|
+
out[key] = unquote(line.slice(eq + 1));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function reconcileEnvText(existing, values, keys) {
|
|
66
|
+
const desired = new Map(keys.map((k) => {
|
|
67
|
+
const value = values[k];
|
|
68
|
+
return [k, value === void 0 ? "" : value];
|
|
69
|
+
}));
|
|
70
|
+
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
71
|
+
const written = keys.filter((k) => desired.get(k) !== null);
|
|
72
|
+
if (existing === null) {
|
|
73
|
+
if (written.length === 0)
|
|
74
|
+
return "";
|
|
75
|
+
return `${ENGAGEMENT_ENV_HEADER}
|
|
76
|
+
` + written.map(line).join("\n") + "\n";
|
|
77
|
+
}
|
|
78
|
+
const seen = /* @__PURE__ */ new Set();
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const existingLine of existing.split("\n")) {
|
|
81
|
+
const eq = existingLine.indexOf("=");
|
|
82
|
+
if (eq === -1) {
|
|
83
|
+
out.push(existingLine);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const lhs = existingLine.slice(0, eq);
|
|
87
|
+
const exported = /^\s*export\s+/.exec(lhs);
|
|
88
|
+
const prefix = exported === null ? "" : exported[0];
|
|
89
|
+
const key = lhs.slice(prefix.length).trim();
|
|
90
|
+
if (!desired.has(key)) {
|
|
91
|
+
out.push(existingLine);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(key);
|
|
95
|
+
if (desired.get(key) === null)
|
|
96
|
+
continue;
|
|
97
|
+
out.push(`${prefix}${line(key)}`);
|
|
98
|
+
}
|
|
99
|
+
const missing = written.filter((k) => !seen.has(k));
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
const header = existing.includes(ENGAGEMENT_ENV_HEADER) ? "" : `${ENGAGEMENT_ENV_HEADER}
|
|
102
|
+
`;
|
|
103
|
+
const block = header + missing.map(line).join("\n");
|
|
104
|
+
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
105
|
+
if (trailingBlank)
|
|
106
|
+
out.splice(out.length - 1, 0, block);
|
|
107
|
+
else
|
|
108
|
+
out.push(`
|
|
109
|
+
${block}`);
|
|
110
|
+
}
|
|
111
|
+
let result = out.join("\n");
|
|
112
|
+
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
113
|
+
result += "\n";
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function normaliseOrigin(serviceUrl) {
|
|
117
|
+
return serviceUrl.trim().replace(/\/+$/, "").toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
function parseAccountStoreText(raw) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(raw);
|
|
122
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.accounts !== "object") {
|
|
123
|
+
return { version: 1, accounts: {} };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
version: 1,
|
|
127
|
+
accounts: parsed.accounts ?? {}
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { version: 1, accounts: {} };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
33
134
|
// ../events/dist/result.js
|
|
34
135
|
var firedGuardSchema = z2.object({
|
|
35
136
|
guardId: z2.string(),
|
|
@@ -90,7 +191,7 @@ var wireErrorSchema = z2.object({
|
|
|
90
191
|
|
|
91
192
|
// ../events/dist/telemetry.js
|
|
92
193
|
import { z as z3 } from "zod";
|
|
93
|
-
var utcIso8601 = z3.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z
|
|
194
|
+
var utcIso8601 = z3.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
94
195
|
var guardEvalOutcomeSchema = z3.enum([
|
|
95
196
|
"evaluated",
|
|
96
197
|
"infra-error",
|
|
@@ -146,7 +247,7 @@ var guardEvalRunSchema = z3.object({
|
|
|
146
247
|
|
|
147
248
|
// ../events/dist/engagement-lifecycle.js
|
|
148
249
|
import { z as z4 } from "zod";
|
|
149
|
-
var utcIso86012 = z4.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z
|
|
250
|
+
var utcIso86012 = z4.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
150
251
|
var engagementTypeSchema = z4.enum(["client"]);
|
|
151
252
|
var lifecycleStatusSchema = z4.enum(["active", "delivering", "closed"]);
|
|
152
253
|
var createEngagementRequestSchema = z4.object({
|
|
@@ -275,16 +376,184 @@ var openPhaseResponseSchema = z4.object({
|
|
|
275
376
|
decision: phaseEntryDecisionRecordSchema
|
|
276
377
|
}).strict();
|
|
277
378
|
|
|
278
|
-
// ../events/dist/
|
|
379
|
+
// ../events/dist/state.js
|
|
279
380
|
import { z as z5 } from "zod";
|
|
280
|
-
var
|
|
281
|
-
var
|
|
381
|
+
var utcIso86013 = z5.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
382
|
+
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
383
|
+
var stepIdSchema = z5.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
384
|
+
var methodVersionSchema = z5.string().regex(/^\d+\.\d+\.\d+$/);
|
|
385
|
+
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
386
|
+
var recordKindSchema = z5.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
387
|
+
var methodLayerSchema = z5.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
388
|
+
var evidenceKindSchema = z5.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
389
|
+
var declarableEvidenceKindSchema = z5.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
390
|
+
var dispositionSchema = z5.enum([
|
|
391
|
+
"prevented",
|
|
392
|
+
"corrected-in-spec",
|
|
393
|
+
"corrected-in-scope",
|
|
394
|
+
"blocked-in-code",
|
|
395
|
+
"flagged",
|
|
396
|
+
"auto-fixed",
|
|
397
|
+
"surfaced",
|
|
398
|
+
"filed-forward"
|
|
399
|
+
]);
|
|
400
|
+
var mechanismSchema = z5.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
401
|
+
var artefactStateSchema = z5.enum(["created", "updated"]);
|
|
402
|
+
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
403
|
+
var qualifiedOutcomeSchema = z5.enum(QUALIFIED_OUTCOMES);
|
|
404
|
+
function isQualifiedOutcome(outcome) {
|
|
405
|
+
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
406
|
+
}
|
|
407
|
+
var runOutcomeSchema = z5.enum([
|
|
408
|
+
"completed",
|
|
409
|
+
"attempted-failed",
|
|
410
|
+
...QUALIFIED_OUTCOMES
|
|
411
|
+
]);
|
|
412
|
+
var gateKindSchema = z5.enum(["approval", "mechanical"]);
|
|
413
|
+
var gateVerdictSchema = z5.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
414
|
+
function outcomeReasonIssue(outcome, reason) {
|
|
415
|
+
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
416
|
+
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
417
|
+
}
|
|
418
|
+
if (!isQualifiedOutcome(outcome) && reason !== void 0) {
|
|
419
|
+
return {
|
|
420
|
+
message: `outcomeReason is only present on a qualified outcome (${QUALIFIED_OUTCOMES.join(" | ")})`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
var anchorEvidenceSchema = z5.object({
|
|
426
|
+
kind: z5.literal("anchor"),
|
|
427
|
+
path: z5.string().regex(anchorPathRegex),
|
|
428
|
+
// Optional 1-based line number within the file at `path`. Present when the
|
|
429
|
+
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
430
|
+
// does not, because a line number is not always obtainable. Absence is a
|
|
431
|
+
// valid, permanent state here, not a gap meant to be filled in later.
|
|
432
|
+
line: z5.number().optional()
|
|
433
|
+
}).strict();
|
|
434
|
+
var diffEvidenceSchema = z5.object({
|
|
435
|
+
kind: z5.literal("diff"),
|
|
436
|
+
ref: z5.string()
|
|
437
|
+
}).strict();
|
|
438
|
+
var reproEvidenceSchema = z5.object({
|
|
439
|
+
kind: z5.literal("repro"),
|
|
440
|
+
steps: z5.string().max(2e3)
|
|
441
|
+
}).strict();
|
|
442
|
+
var questionAnswerEvidenceSchema = z5.object({
|
|
443
|
+
kind: z5.literal("question-answer"),
|
|
444
|
+
question: z5.string().max(2e3),
|
|
445
|
+
before: z5.string().max(2e3),
|
|
446
|
+
after: z5.string().max(2e3)
|
|
447
|
+
}).strict();
|
|
448
|
+
var noOpEvidenceSchema = z5.object({
|
|
449
|
+
kind: z5.literal("no-op"),
|
|
450
|
+
inspected: z5.string().max(2e3),
|
|
451
|
+
unchangedBecause: z5.string().max(2e3)
|
|
452
|
+
}).strict();
|
|
453
|
+
var evidenceSchema = z5.discriminatedUnion("kind", [
|
|
454
|
+
anchorEvidenceSchema,
|
|
455
|
+
diffEvidenceSchema,
|
|
456
|
+
reproEvidenceSchema,
|
|
457
|
+
questionAnswerEvidenceSchema,
|
|
458
|
+
noOpEvidenceSchema
|
|
459
|
+
]);
|
|
460
|
+
var stateRecordEnvelopeSchema = z5.object({
|
|
461
|
+
format: z5.literal(STATE_RECORD_FORMAT),
|
|
462
|
+
recordKind: recordKindSchema,
|
|
463
|
+
recordId: z5.string().uuid(),
|
|
464
|
+
methodVersion: methodVersionSchema,
|
|
465
|
+
engagementId: z5.string(),
|
|
466
|
+
stepId: stepIdSchema,
|
|
467
|
+
// Immutable: names when the thing happened, never when a later disposition
|
|
468
|
+
// moved.
|
|
469
|
+
occurredAt: utcIso86013,
|
|
470
|
+
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
471
|
+
// Carried on the shared envelope so a "when did this last change" query is
|
|
472
|
+
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
473
|
+
// `disposition` is the only field any re-emit may change (§5.3).
|
|
474
|
+
dispositionAt: utcIso86013,
|
|
475
|
+
evidence: evidenceSchema,
|
|
476
|
+
// Which phase of the project this record belongs to, if any. Present on
|
|
477
|
+
// records written during phase-scoped work; absent on records that run once
|
|
478
|
+
// for the whole project rather than per phase. Absence is meaningful on its
|
|
479
|
+
// own — it is never replaced with a placeholder value, and it is read as
|
|
480
|
+
// "satisfies any phase", not as "unknown".
|
|
481
|
+
phase: z5.string().optional()
|
|
482
|
+
});
|
|
483
|
+
var artefactRefSchema = z5.object({
|
|
484
|
+
name: z5.string(),
|
|
485
|
+
parent: z5.string()
|
|
486
|
+
}).strict();
|
|
487
|
+
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
488
|
+
recordKind: z5.literal("artefact"),
|
|
489
|
+
artefactRef: artefactRefSchema,
|
|
490
|
+
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
491
|
+
// state, so the schema says so.
|
|
492
|
+
writtenByLayers: z5.array(methodLayerSchema).min(1),
|
|
493
|
+
state: artefactStateSchema
|
|
494
|
+
}).strict();
|
|
495
|
+
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
496
|
+
recordKind: z5.literal("step-run"),
|
|
497
|
+
runOutcome: runOutcomeSchema,
|
|
498
|
+
failureReason: z5.string().optional(),
|
|
499
|
+
outcomeReason: z5.string().max(2e3).optional()
|
|
500
|
+
}).strict().superRefine((rec, ctx) => {
|
|
501
|
+
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
502
|
+
ctx.addIssue({
|
|
503
|
+
code: z5.ZodIssueCode.custom,
|
|
504
|
+
path: ["failureReason"],
|
|
505
|
+
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
509
|
+
ctx.addIssue({
|
|
510
|
+
code: z5.ZodIssueCode.custom,
|
|
511
|
+
path: ["failureReason"],
|
|
512
|
+
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
const issue = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
516
|
+
if (issue) {
|
|
517
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
521
|
+
recordKind: z5.literal("gate"),
|
|
522
|
+
gateKind: gateKindSchema,
|
|
523
|
+
verdict: gateVerdictSchema,
|
|
524
|
+
actor: z5.string(),
|
|
525
|
+
outcomeReason: z5.string().max(2e3).optional()
|
|
526
|
+
}).strict().superRefine((rec, ctx) => {
|
|
527
|
+
const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
528
|
+
if (issue) {
|
|
529
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
533
|
+
recordKind: z5.literal("intervention"),
|
|
534
|
+
layer: methodLayerSchema,
|
|
535
|
+
mechanism: mechanismSchema,
|
|
536
|
+
severity: severitySchema,
|
|
537
|
+
disposition: dispositionSchema,
|
|
538
|
+
summary: z5.string().max(2e3)
|
|
539
|
+
}).strict();
|
|
540
|
+
var stateRecordSchema = z5.discriminatedUnion("recordKind", [
|
|
541
|
+
artefactRecordSchema,
|
|
542
|
+
stepRunRecordSchema,
|
|
543
|
+
gateRecordSchema,
|
|
544
|
+
interventionRecordSchema
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
// ../events/dist/crew.js
|
|
548
|
+
import { z as z6 } from "zod";
|
|
549
|
+
var crewGroupSchema = z6.enum(["builder", "reviewer", "always-on"]);
|
|
550
|
+
var crewTierSchema = z6.enum(["deep", "standard", "fast"]);
|
|
282
551
|
var CREW_DISCIPLINE_MAX = 64;
|
|
283
|
-
var crewFaceHeadSchema =
|
|
284
|
-
var crewFaceAntennaSchema =
|
|
285
|
-
var crewFaceEyesSchema =
|
|
286
|
-
var crewFaceMouthSchema =
|
|
287
|
-
var crewFaceSchema =
|
|
552
|
+
var crewFaceHeadSchema = z6.enum(["sq", "sqr", "rnd", "dome", "hex"]);
|
|
553
|
+
var crewFaceAntennaSchema = z6.enum(["none", "stalk", "twin", "dish"]);
|
|
554
|
+
var crewFaceEyesSchema = z6.enum(["dots", "bars", "visor", "cyclops"]);
|
|
555
|
+
var crewFaceMouthSchema = z6.enum(["line", "grid", "dots", "wave"]);
|
|
556
|
+
var crewFaceSchema = z6.object({
|
|
288
557
|
// The outline of the head. Members who do the same kind of work share one, so a
|
|
289
558
|
// team is recognisable before any name is read.
|
|
290
559
|
head: crewFaceHeadSchema,
|
|
@@ -295,17 +564,17 @@ var crewFaceSchema = z5.object({
|
|
|
295
564
|
// The mouth.
|
|
296
565
|
mouth: crewFaceMouthSchema,
|
|
297
566
|
// Whether this member is drawn with ears.
|
|
298
|
-
ears:
|
|
567
|
+
ears: z6.boolean()
|
|
299
568
|
}).strict();
|
|
300
|
-
var crewMemberSchema =
|
|
569
|
+
var crewMemberSchema = z6.object({
|
|
301
570
|
// The agent's name. The same twenty-one on every project, so a name is a
|
|
302
571
|
// vocabulary a returning client already knows.
|
|
303
|
-
callsign:
|
|
572
|
+
callsign: z6.string().min(1).max(32),
|
|
304
573
|
// Which of the three headings this member reads under.
|
|
305
574
|
group: crewGroupSchema,
|
|
306
575
|
// The kind of work this member takes. A task names a discipline, and only
|
|
307
576
|
// agents holding it can be dispatched to it.
|
|
308
|
-
discipline:
|
|
577
|
+
discipline: z6.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
309
578
|
// How much thinking this member brings. There is no field naming the supplier
|
|
310
579
|
// behind it, on this shape or on any other.
|
|
311
580
|
tier: crewTierSchema,
|
|
@@ -470,8 +739,8 @@ var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
|
470
739
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
471
740
|
|
|
472
741
|
// ../events/dist/device-auth.js
|
|
473
|
-
import { z as
|
|
474
|
-
var
|
|
742
|
+
import { z as z7 } from "zod";
|
|
743
|
+
var utcIso86014 = z7.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
475
744
|
var DEVICE_AUTH_STATUS = {
|
|
476
745
|
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
477
746
|
PENDING: "pending",
|
|
@@ -497,7 +766,7 @@ var DEVICE_AUTH_STATUS = {
|
|
|
497
766
|
ALREADY_DECIDED: "already-decided",
|
|
498
767
|
/**
|
|
499
768
|
* Confirm: an approval named a signed-in identity whose account has not accepted
|
|
500
|
-
* the terms of service and privacy policy currently in force
|
|
769
|
+
* the terms of service and privacy policy currently in force. 400.
|
|
501
770
|
*
|
|
502
771
|
* NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
|
|
503
772
|
* says nothing about the code itself — the code is still `pending` after this
|
|
@@ -512,25 +781,25 @@ var DEVICE_POLL_REFUSALS = [
|
|
|
512
781
|
DEVICE_AUTH_STATUS.EXPIRED,
|
|
513
782
|
DEVICE_AUTH_STATUS.UNKNOWN
|
|
514
783
|
];
|
|
515
|
-
var deviceAuthStartResponseSchema =
|
|
516
|
-
deviceCode:
|
|
517
|
-
userCode:
|
|
518
|
-
verificationUrl:
|
|
519
|
-
expiresAt:
|
|
520
|
-
pollIntervalMs:
|
|
784
|
+
var deviceAuthStartResponseSchema = z7.object({
|
|
785
|
+
deviceCode: z7.string().min(1),
|
|
786
|
+
userCode: z7.string().min(1),
|
|
787
|
+
verificationUrl: z7.string().min(1),
|
|
788
|
+
expiresAt: utcIso86014,
|
|
789
|
+
pollIntervalMs: z7.number().int().positive()
|
|
521
790
|
}).strict();
|
|
522
|
-
var devicePollPendingSchema =
|
|
523
|
-
status:
|
|
524
|
-
pollIntervalMs:
|
|
791
|
+
var devicePollPendingSchema = z7.object({
|
|
792
|
+
status: z7.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
793
|
+
pollIntervalMs: z7.number().int().positive()
|
|
525
794
|
}).strict();
|
|
526
|
-
var devicePollApprovedSchema =
|
|
527
|
-
status:
|
|
528
|
-
accountId:
|
|
529
|
-
credential:
|
|
530
|
-
expiresAt:
|
|
795
|
+
var devicePollApprovedSchema = z7.object({
|
|
796
|
+
status: z7.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
797
|
+
accountId: z7.string().min(1),
|
|
798
|
+
credential: z7.string().min(1),
|
|
799
|
+
expiresAt: utcIso86014
|
|
531
800
|
}).strict();
|
|
532
|
-
var deviceAuthRefusalSchema =
|
|
533
|
-
status:
|
|
801
|
+
var deviceAuthRefusalSchema = z7.object({
|
|
802
|
+
status: z7.enum([
|
|
534
803
|
DEVICE_AUTH_STATUS.DECLINED,
|
|
535
804
|
DEVICE_AUTH_STATUS.EXPIRED,
|
|
536
805
|
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
@@ -541,25 +810,25 @@ var deviceAuthRefusalSchema = z6.object({
|
|
|
541
810
|
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
542
811
|
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
543
812
|
]),
|
|
544
|
-
message:
|
|
813
|
+
message: z7.string().min(1)
|
|
545
814
|
}).strict();
|
|
546
|
-
var devicePollResponseSchema =
|
|
815
|
+
var devicePollResponseSchema = z7.union([
|
|
547
816
|
devicePollPendingSchema,
|
|
548
817
|
devicePollApprovedSchema,
|
|
549
818
|
deviceAuthRefusalSchema
|
|
550
819
|
]);
|
|
551
|
-
var deviceConfirmRequestSchema =
|
|
552
|
-
userCode:
|
|
553
|
-
decision:
|
|
554
|
-
externalAuthId:
|
|
555
|
-
email:
|
|
556
|
-
acceptTerms:
|
|
820
|
+
var deviceConfirmRequestSchema = z7.object({
|
|
821
|
+
userCode: z7.string().min(1),
|
|
822
|
+
decision: z7.enum(["approve", "decline"]),
|
|
823
|
+
externalAuthId: z7.string().min(1).optional(),
|
|
824
|
+
email: z7.string().optional(),
|
|
825
|
+
acceptTerms: z7.boolean().optional()
|
|
557
826
|
}).strict();
|
|
558
|
-
var deviceConfirmRecordedSchema =
|
|
559
|
-
status:
|
|
560
|
-
accountId:
|
|
827
|
+
var deviceConfirmRecordedSchema = z7.object({
|
|
828
|
+
status: z7.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
829
|
+
accountId: z7.string().min(1).optional()
|
|
561
830
|
}).strict();
|
|
562
|
-
var deviceConfirmResponseSchema =
|
|
831
|
+
var deviceConfirmResponseSchema = z7.union([
|
|
563
832
|
deviceConfirmRecordedSchema,
|
|
564
833
|
deviceAuthRefusalSchema
|
|
565
834
|
]);
|
|
@@ -588,25 +857,25 @@ function loopbackVerificationUrl(verificationUrl, target) {
|
|
|
588
857
|
}
|
|
589
858
|
|
|
590
859
|
// ../events/dist/join-refusal.js
|
|
591
|
-
import { z as
|
|
860
|
+
import { z as z8 } from "zod";
|
|
592
861
|
var NOT_THIS_ACCOUNT_REASON = "not-this-account";
|
|
593
862
|
var JOIN_REFUSAL_STATUS = 403;
|
|
594
|
-
var notThisAccountRefusalSchema =
|
|
595
|
-
statusCode:
|
|
596
|
-
error:
|
|
597
|
-
message:
|
|
598
|
-
reason:
|
|
863
|
+
var notThisAccountRefusalSchema = z8.object({
|
|
864
|
+
statusCode: z8.literal(JOIN_REFUSAL_STATUS),
|
|
865
|
+
error: z8.string(),
|
|
866
|
+
message: z8.string(),
|
|
867
|
+
reason: z8.literal(NOT_THIS_ACCOUNT_REASON)
|
|
599
868
|
}).strict();
|
|
600
|
-
var joinRefusalSchema =
|
|
869
|
+
var joinRefusalSchema = z8.union([notThisAccountRefusalSchema, wireErrorSchema]);
|
|
601
870
|
function isNotThisAccountRefusal(body) {
|
|
602
871
|
return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
|
|
603
872
|
}
|
|
604
873
|
|
|
605
874
|
// ../events/dist/account-identity.js
|
|
606
|
-
import { z as
|
|
607
|
-
var accountIdentitySchema =
|
|
608
|
-
accountId:
|
|
609
|
-
email:
|
|
875
|
+
import { z as z9 } from "zod";
|
|
876
|
+
var accountIdentitySchema = z9.object({
|
|
877
|
+
accountId: z9.string().min(1),
|
|
878
|
+
email: z9.string().min(1).optional()
|
|
610
879
|
}).strict();
|
|
611
880
|
function safeParseAccountIdentity(payload) {
|
|
612
881
|
return accountIdentitySchema.safeParse(payload);
|
|
@@ -673,6 +942,21 @@ function describeIdentity(identity) {
|
|
|
673
942
|
return `(a ${typeof identity})`;
|
|
674
943
|
}
|
|
675
944
|
|
|
945
|
+
// ../events/dist/credential-wire.js
|
|
946
|
+
import { z as z10 } from "zod";
|
|
947
|
+
var engagementCredentialResponseSchema = z10.object({
|
|
948
|
+
engagementId: z10.string(),
|
|
949
|
+
sessionToken: z10.string(),
|
|
950
|
+
mcpUrl: z10.string(),
|
|
951
|
+
guardUrl: z10.string(),
|
|
952
|
+
controlTelemetryUrl: z10.string()
|
|
953
|
+
}).strict();
|
|
954
|
+
var ciTokenExchangeResponseSchema = z10.object({
|
|
955
|
+
token: z10.string(),
|
|
956
|
+
expiresAt: utcIso86013,
|
|
957
|
+
engagementId: z10.string()
|
|
958
|
+
}).strict();
|
|
959
|
+
|
|
676
960
|
// dist/engagement-credential.js
|
|
677
961
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
678
962
|
import { platform as platform2 } from "node:os";
|
|
@@ -683,27 +967,14 @@ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
683
967
|
import { homedir, platform } from "node:os";
|
|
684
968
|
import { dirname, join } from "node:path";
|
|
685
969
|
function halfcycleHome(home) {
|
|
686
|
-
return join(home ?? homedir(),
|
|
970
|
+
return join(home ?? homedir(), HALFCYCLE_DIR_NAME);
|
|
687
971
|
}
|
|
688
972
|
function accountStorePath(home) {
|
|
689
|
-
return join(halfcycleHome(home),
|
|
690
|
-
}
|
|
691
|
-
function normaliseOrigin(serviceUrl) {
|
|
692
|
-
return serviceUrl.trim().replace(/\/+$/, "").toLowerCase();
|
|
973
|
+
return join(halfcycleHome(home), ACCOUNT_STORE_FILENAME);
|
|
693
974
|
}
|
|
694
975
|
function readStore(home) {
|
|
695
|
-
let raw;
|
|
696
976
|
try {
|
|
697
|
-
|
|
698
|
-
} catch {
|
|
699
|
-
return { version: 1, accounts: {} };
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
const parsed = JSON.parse(raw);
|
|
703
|
-
if (!parsed || typeof parsed !== "object" || typeof parsed.accounts !== "object") {
|
|
704
|
-
return { version: 1, accounts: {} };
|
|
705
|
-
}
|
|
706
|
-
return { version: 1, accounts: parsed.accounts ?? {} };
|
|
977
|
+
return parseAccountStoreText(readFileSync(accountStorePath(home), "utf-8"));
|
|
707
978
|
} catch {
|
|
708
979
|
return { version: 1, accounts: {} };
|
|
709
980
|
}
|
|
@@ -749,8 +1020,9 @@ function forgetStoredCredential(serviceUrl, home) {
|
|
|
749
1020
|
}
|
|
750
1021
|
|
|
751
1022
|
// dist/engagement-credential.js
|
|
752
|
-
var ENGAGEMENTS_DIR =
|
|
753
|
-
var ENV_FILENAME =
|
|
1023
|
+
var ENGAGEMENTS_DIR = ENGAGEMENTS_DIR_NAME;
|
|
1024
|
+
var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
1025
|
+
var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
754
1026
|
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
755
1027
|
function engagementStateDir(engagementId, home) {
|
|
756
1028
|
return join2(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
@@ -758,7 +1030,6 @@ function engagementStateDir(engagementId, home) {
|
|
|
758
1030
|
function engagementEnvPath(engagementId, home) {
|
|
759
1031
|
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
760
1032
|
}
|
|
761
|
-
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
762
1033
|
function notThisAccountMarkerPath(engagementId, home) {
|
|
763
1034
|
return join2(engagementStateDir(engagementId, home), NOT_THIS_ACCOUNT_MARKER_FILENAME);
|
|
764
1035
|
}
|
|
@@ -785,82 +1056,8 @@ var ENGAGEMENT_ENV_KEYS = [
|
|
|
785
1056
|
"CONTROL_TELEMETRY_URL"
|
|
786
1057
|
];
|
|
787
1058
|
var PHASE_STAMP_ENV_KEY = "HALFCYCLE_PHASE";
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
791
|
-
}
|
|
792
|
-
function unquote(value) {
|
|
793
|
-
const v = value.trim();
|
|
794
|
-
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
795
|
-
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
796
|
-
}
|
|
797
|
-
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
798
|
-
return v.slice(1, -1);
|
|
799
|
-
return v;
|
|
800
|
-
}
|
|
801
|
-
function parseEnvText(raw) {
|
|
802
|
-
const out = {};
|
|
803
|
-
for (const line of raw.split("\n")) {
|
|
804
|
-
const eq = line.indexOf("=");
|
|
805
|
-
if (eq === -1)
|
|
806
|
-
continue;
|
|
807
|
-
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
808
|
-
if (key === "" || key.startsWith("#"))
|
|
809
|
-
continue;
|
|
810
|
-
out[key] = unquote(line.slice(eq + 1));
|
|
811
|
-
}
|
|
812
|
-
return out;
|
|
813
|
-
}
|
|
814
|
-
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
815
|
-
const desired = new Map(keys.map((k) => {
|
|
816
|
-
const value = values[k];
|
|
817
|
-
return [k, value === void 0 ? "" : value];
|
|
818
|
-
}));
|
|
819
|
-
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
820
|
-
const written = keys.filter((k) => desired.get(k) !== null);
|
|
821
|
-
if (existing === null) {
|
|
822
|
-
if (written.length === 0)
|
|
823
|
-
return "";
|
|
824
|
-
return `${ENV_HEADER}
|
|
825
|
-
` + written.map(line).join("\n") + "\n";
|
|
826
|
-
}
|
|
827
|
-
const seen = /* @__PURE__ */ new Set();
|
|
828
|
-
const out = [];
|
|
829
|
-
for (const existingLine of existing.split("\n")) {
|
|
830
|
-
const eq = existingLine.indexOf("=");
|
|
831
|
-
if (eq === -1) {
|
|
832
|
-
out.push(existingLine);
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
const lhs = existingLine.slice(0, eq);
|
|
836
|
-
const exported = /^\s*export\s+/.exec(lhs);
|
|
837
|
-
const prefix = exported === null ? "" : exported[0];
|
|
838
|
-
const key = lhs.slice(prefix.length).trim();
|
|
839
|
-
if (!desired.has(key)) {
|
|
840
|
-
out.push(existingLine);
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
seen.add(key);
|
|
844
|
-
if (desired.get(key) === null)
|
|
845
|
-
continue;
|
|
846
|
-
out.push(`${prefix}${line(key)}`);
|
|
847
|
-
}
|
|
848
|
-
const missing = written.filter((k) => !seen.has(k));
|
|
849
|
-
if (missing.length > 0) {
|
|
850
|
-
const header = existing.includes(ENV_HEADER) ? "" : `${ENV_HEADER}
|
|
851
|
-
`;
|
|
852
|
-
const block = header + missing.map(line).join("\n");
|
|
853
|
-
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
854
|
-
if (trailingBlank)
|
|
855
|
-
out.splice(out.length - 1, 0, block);
|
|
856
|
-
else
|
|
857
|
-
out.push(`
|
|
858
|
-
${block}`);
|
|
859
|
-
}
|
|
860
|
-
let result = out.join("\n");
|
|
861
|
-
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
862
|
-
result += "\n";
|
|
863
|
-
return result;
|
|
1059
|
+
function reconcileEnvText2(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
1060
|
+
return reconcileEnvText(existing, values, keys);
|
|
864
1061
|
}
|
|
865
1062
|
function readEngagementEnv(engagementId, home) {
|
|
866
1063
|
try {
|
|
@@ -878,7 +1075,7 @@ function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KE
|
|
|
878
1075
|
} catch {
|
|
879
1076
|
existing = null;
|
|
880
1077
|
}
|
|
881
|
-
const reconciled =
|
|
1078
|
+
const reconciled = reconcileEnvText2(existing, values, keys);
|
|
882
1079
|
if (existing === null && reconciled === "")
|
|
883
1080
|
return "skipped";
|
|
884
1081
|
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
@@ -904,7 +1101,7 @@ function applyOwnerOnly(path, dir) {
|
|
|
904
1101
|
function engagementResolutionShell() {
|
|
905
1102
|
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
906
1103
|
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
907
|
-
# $HOME
|
|
1104
|
+
# $HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
908
1105
|
# written owner-only by \`npx halfcycle\`. What this repository holds is the
|
|
909
1106
|
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
910
1107
|
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
@@ -935,10 +1132,10 @@ halfcycle_env_file() {
|
|
|
935
1132
|
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
936
1133
|
return 1
|
|
937
1134
|
fi
|
|
938
|
-
if [ -f "$HOME
|
|
1135
|
+
if [ -f "$HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/$hc_id/${NOT_THIS_ACCOUNT_MARKER_FILENAME}" ]; then
|
|
939
1136
|
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=1
|
|
940
1137
|
fi
|
|
941
|
-
hc_env="$HOME
|
|
1138
|
+
hc_env="$HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
942
1139
|
if [ ! -f "$hc_env" ]; then
|
|
943
1140
|
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
944
1141
|
return 1
|
|
@@ -1129,7 +1326,7 @@ function resolveVendoredBinary() {
|
|
|
1129
1326
|
if (existsSync3(candidate))
|
|
1130
1327
|
return candidate;
|
|
1131
1328
|
}
|
|
1132
|
-
throw new Error(`[bundle install] Cannot find the self-contained guard binary bin.bundle.mjs.
|
|
1329
|
+
throw new Error(`[bundle install] 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(", ")}.`);
|
|
1133
1330
|
}
|
|
1134
1331
|
var WRITE_ALLOWLIST = [
|
|
1135
1332
|
".claude/commands",
|
|
@@ -1278,6 +1475,15 @@ function generateSettingsJson() {
|
|
|
1278
1475
|
timeout: 5
|
|
1279
1476
|
}
|
|
1280
1477
|
]
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
hooks: [
|
|
1481
|
+
{
|
|
1482
|
+
type: "command",
|
|
1483
|
+
command: "bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
1484
|
+
timeout: 10
|
|
1485
|
+
}
|
|
1486
|
+
]
|
|
1281
1487
|
}
|
|
1282
1488
|
],
|
|
1283
1489
|
UserPromptSubmit: [
|
|
@@ -1347,9 +1553,9 @@ function generateGuardRunnerWrapper() {
|
|
|
1347
1553
|
${GENERATED_GUARD_RUNNER_HEADER} the Halfcycle installer.
|
|
1348
1554
|
#
|
|
1349
1555
|
# Loads this engagement's credentials so the guard runner has its GUARD_SERVICE_*
|
|
1350
|
-
# values when Claude Code fires PostToolUse / Stop / SubagentStop,
|
|
1351
|
-
# SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path
|
|
1352
|
-
# project, so the hook resolves in any checkout of it.
|
|
1556
|
+
# values when Claude Code fires SessionStart / PostToolUse / Stop / SubagentStop,
|
|
1557
|
+
# then exec's the SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path
|
|
1558
|
+
# inside this project, so the hook resolves in any checkout of it.
|
|
1353
1559
|
#
|
|
1354
1560
|
# The runner itself reads only process.env, so env loading lives here in the hook
|
|
1355
1561
|
# wiring rather than inside the binary.
|
|
@@ -1364,6 +1570,16 @@ ${engagementResolutionShell()}
|
|
|
1364
1570
|
# the runner say what it found \u2014 its never-configured branch is loud and non-zero
|
|
1365
1571
|
# on purpose, and a wrapper that exited quietly here would hide it.
|
|
1366
1572
|
if halfcycle_env_file "$PROJECT_DIR"; then
|
|
1573
|
+
# EXPORTED, NOT MERELY SET \u2014 one word, and without it a whole feature is off.
|
|
1574
|
+
# \`set -a\` exports what is assigned AFTER it, and the resolution above assigned
|
|
1575
|
+
# this variable BEFORE it, so the exec'd binary would never see the name of the
|
|
1576
|
+
# file these credentials came from. It needs that name: when the service says
|
|
1577
|
+
# this engagement's credential has timed out, the binary renews it by rewriting
|
|
1578
|
+
# that file. With nothing naming the file it renews nothing \u2014 and it says
|
|
1579
|
+
# nothing and exits 0 while doing so, because there is no session in which that
|
|
1580
|
+
# silence is wrong on its own. So a wrapper missing this line looks perfectly
|
|
1581
|
+
# healthy at every session start and renews nothing, forever.
|
|
1582
|
+
export HALFCYCLE_ENV_FILE
|
|
1367
1583
|
set -a
|
|
1368
1584
|
. "$HALFCYCLE_ENV_FILE"
|
|
1369
1585
|
set +a
|
|
@@ -2213,6 +2429,7 @@ var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle service stopped
|
|
|
2213
2429
|
var SIGN_IN_NON_INTERACTIVE_MESSAGE = "Halfcycle needs a signed-in account and this looks like a non-interactive run (CI is set), so there is no browser to sign in with. Set HALFCYCLE_TOKEN in the environment to a Halfcycle credential and run this again. Nothing was created.";
|
|
2214
2430
|
var DEADLINE_SLACK_INTERVALS = 1;
|
|
2215
2431
|
var STILL_WAITING_MS = 15e3;
|
|
2432
|
+
var DEFAULT_SIGN_IN_REASON = "create an engagement";
|
|
2216
2433
|
function defaultWrite(text) {
|
|
2217
2434
|
process.stdout.write(text);
|
|
2218
2435
|
}
|
|
@@ -2402,7 +2619,7 @@ async function signIn(serviceUrl, deps = {}) {
|
|
|
2402
2619
|
if (isNonInteractive(env)) {
|
|
2403
2620
|
throw new SignInRefused(SIGN_IN_TIMED_OUT, SIGN_IN_NON_INTERACTIVE_MESSAGE);
|
|
2404
2621
|
}
|
|
2405
|
-
write(`[halfcycle] Halfcycle needs an account before it can
|
|
2622
|
+
write(`[halfcycle] Halfcycle needs an account before it can ${deps.reason ?? DEFAULT_SIGN_IN_REASON} \u2014 signing you in through your browser.
|
|
2406
2623
|
`);
|
|
2407
2624
|
const bind = await bindLoopback(env);
|
|
2408
2625
|
let closed = false;
|
|
@@ -2559,6 +2776,12 @@ async function joinEngagement(baseUrl, engagementId, credential) {
|
|
|
2559
2776
|
route: "POST /engagements/:engagementId/join"
|
|
2560
2777
|
});
|
|
2561
2778
|
}
|
|
2779
|
+
function withDeclaredTelemetryKey(raw) {
|
|
2780
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
2781
|
+
return null;
|
|
2782
|
+
const obj = raw;
|
|
2783
|
+
return "controlTelemetryUrl" in obj ? obj : { ...obj, controlTelemetryUrl: "" };
|
|
2784
|
+
}
|
|
2562
2785
|
async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
2563
2786
|
const bearer = credential?.trim();
|
|
2564
2787
|
let res;
|
|
@@ -2583,7 +2806,9 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2583
2806
|
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
2584
2807
|
throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2585
2808
|
}
|
|
2586
|
-
const
|
|
2809
|
+
const candidate = withDeclaredTelemetryKey(await res.json().catch(() => null));
|
|
2810
|
+
const declared = engagementCredentialResponseSchema.safeParse(candidate);
|
|
2811
|
+
const body = declared.success ? declared.data : candidate;
|
|
2587
2812
|
if (!body || typeof body.engagementId !== "string" || typeof body.sessionToken !== "string") {
|
|
2588
2813
|
throw new Error(`[bundle install] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2589
2814
|
}
|
|
@@ -2602,6 +2827,16 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2602
2827
|
throw new Error(`[bundle install] The Halfcycle service at ${url} created an engagement but did not say where its guard service lives (no guardUrl on the response). Without that address the install would wire a guard hook that evaluates nothing, so nothing was written. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
|
|
2603
2828
|
}
|
|
2604
2829
|
const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
|
|
2830
|
+
if (!declared.success) {
|
|
2831
|
+
const fields = declared.error.issues.map((issue) => {
|
|
2832
|
+
const unrecognised = issue.keys;
|
|
2833
|
+
if (unrecognised && unrecognised.length > 0) {
|
|
2834
|
+
return `unexpected: ${unrecognised.map(String).join(", ")}`;
|
|
2835
|
+
}
|
|
2836
|
+
return issue.path.length > 0 ? issue.path.map(String).join(".") : "(the body itself)";
|
|
2837
|
+
}).join(", ");
|
|
2838
|
+
throw new Error(`[bundle install] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
|
|
2839
|
+
}
|
|
2605
2840
|
return {
|
|
2606
2841
|
engagementId: body.engagementId,
|
|
2607
2842
|
sessionToken: body.sessionToken,
|
|
@@ -2940,7 +3175,8 @@ function currentEnvironment(quiet2) {
|
|
|
2940
3175
|
// dist/confirm-identity.js
|
|
2941
3176
|
var IDENTITY_TIMEOUT_MS = 5e3;
|
|
2942
3177
|
var IDENTITY_DECLINED = "identity-declined";
|
|
2943
|
-
|
|
3178
|
+
var CREDENTIAL_REFUSED = 401;
|
|
3179
|
+
async function askAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
2944
3180
|
const url = `${serviceUrl.replace(/\/+$/, "")}/account`;
|
|
2945
3181
|
const controller = new AbortController();
|
|
2946
3182
|
const timer = setTimeout(() => controller.abort(), IDENTITY_TIMEOUT_MS);
|
|
@@ -2950,15 +3186,18 @@ async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
|
2950
3186
|
signal: controller.signal
|
|
2951
3187
|
});
|
|
2952
3188
|
if (!res.ok)
|
|
2953
|
-
return void 0;
|
|
3189
|
+
return { identity: void 0, refused: res.status === CREDENTIAL_REFUSED };
|
|
2954
3190
|
const parsed = safeParseAccountIdentity(await res.json());
|
|
2955
|
-
return parsed.success ? parsed.data : void 0;
|
|
3191
|
+
return { identity: parsed.success ? parsed.data : void 0, refused: false };
|
|
2956
3192
|
} catch {
|
|
2957
|
-
return void 0;
|
|
3193
|
+
return { identity: void 0, refused: false };
|
|
2958
3194
|
} finally {
|
|
2959
3195
|
clearTimeout(timer);
|
|
2960
3196
|
}
|
|
2961
3197
|
}
|
|
3198
|
+
async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
3199
|
+
return (await askAccount(serviceUrl, credential, fetchImpl)).identity;
|
|
3200
|
+
}
|
|
2962
3201
|
function accountLabel(identity, storedAccountId, verbose2 = false) {
|
|
2963
3202
|
const accountId = identity?.accountId ?? storedAccountId;
|
|
2964
3203
|
if (identity?.email !== void 0 && identity.email.trim() !== "") {
|
|
@@ -3412,7 +3651,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
3412
3651
|
"",
|
|
3413
3652
|
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
3414
3653
|
"",
|
|
3415
|
-
"> This Build Record was assembled automatically at phase close by
|
|
3654
|
+
"> This Build Record was assembled automatically at phase close by `npx halfcycle close-phase`. Every data field below is projected from the companion JSON \u2014 no record content is hand-authored.",
|
|
3416
3655
|
"",
|
|
3417
3656
|
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
3418
3657
|
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
@@ -3432,7 +3671,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
3432
3671
|
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
3433
3672
|
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
3434
3673
|
"",
|
|
3435
|
-
guardsList ? `Guards evaluated (
|
|
3674
|
+
guardsList ? `Guards evaluated (results only): ${guardsList}. *(What a guard reported about this phase is recorded; the guard's own definition and its history are not.)*` : "No guards fired during this phase.",
|
|
3436
3675
|
"",
|
|
3437
3676
|
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
3438
3677
|
"",
|
|
@@ -3444,7 +3683,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
3444
3683
|
""
|
|
3445
3684
|
];
|
|
3446
3685
|
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
3447
|
-
lines.push(`**
|
|
3686
|
+
lines.push(`**Production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
3448
3687
|
}
|
|
3449
3688
|
lines.push("## Gates that earned their keep", "", `**${record2.gatesEarnedKeep.defectsCaughtPreHuman}** defects were caught by the gates before the human walk; the walk itself found **${record2.gatesEarnedKeep.bugsReachingHumanWalk}**. Named: ${record2.gatesEarnedKeep.named.join("; ")}.`, "", "## Instruments", "", `- **Marginal-cost self-accounting:** ${record2.instruments.marginalCostSelfAccounting}`, `- **COE add-rate:** seam-new ${record2.instruments.coeAddRate.seamNew}, seam-repeat ${record2.instruments.coeAddRate.seamRepeat}, model-limitation ${record2.instruments.coeAddRate.modelLimitation}. ${record2.instruments.coeAddRate.notes}`, "", "## Invariants exercised", "", record2.invariantsExercised.join(", "), "", "## Deviations & decisions recorded", "", ...record2.deviations.map((d) => `- ${d}`), "", "## Tasks", "", `- **Planned:** ${record2.tasks.planned} \xB7 **Landed:** ${record2.tasks.landed} \xB7 **Cancelled:** ${record2.tasks.cancelled.length > 0 ? record2.tasks.cancelled.join(", ") : "none"}`, "");
|
|
3450
3689
|
return lines.join("\n");
|
|
@@ -3837,15 +4076,115 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot) {
|
|
|
3837
4076
|
return { engagementId: body.engagementId, status: body.status, stampFailure, closeRecord };
|
|
3838
4077
|
}
|
|
3839
4078
|
|
|
4079
|
+
// dist/ci-bind.js
|
|
4080
|
+
function parseCiRepositoryArg(arg) {
|
|
4081
|
+
const parts = arg.split("/");
|
|
4082
|
+
if (parts.length !== 2)
|
|
4083
|
+
return null;
|
|
4084
|
+
const [owner, repo] = parts;
|
|
4085
|
+
if (owner === void 0 || owner.trim() === "")
|
|
4086
|
+
return null;
|
|
4087
|
+
if (repo === void 0 || repo.trim() === "")
|
|
4088
|
+
return null;
|
|
4089
|
+
return { owner, repo };
|
|
4090
|
+
}
|
|
4091
|
+
var CiBindRefused = class extends Error {
|
|
4092
|
+
status;
|
|
4093
|
+
constructor(status, message) {
|
|
4094
|
+
super(message);
|
|
4095
|
+
this.status = status;
|
|
4096
|
+
this.name = "CiBindRefused";
|
|
4097
|
+
}
|
|
4098
|
+
/** Is this the ONE arm signing in again can fix — see this file's header. */
|
|
4099
|
+
get authRefused() {
|
|
4100
|
+
return this.status === 401;
|
|
4101
|
+
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Is this "ours to fix, try again", not "yours to fix"? The question is not
|
|
4104
|
+
* mechanical (did a handler run) — it is what a developer reading the message
|
|
4105
|
+
* does next. `502`/`504` are the reverse proxy in front of the published origin
|
|
4106
|
+
* answering for a control plane that is down or slow, with an HTML body this
|
|
4107
|
+
* file's own `requestBinding` cannot parse, so the message would otherwise
|
|
4108
|
+
* degrade to a bare "the URL returned 502". `503` is `resolveAccount`'s own
|
|
4109
|
+
* fail-closed refusal when it could not reach the token store — the caller's
|
|
4110
|
+
* credential was never actually checked, and its posture is emphatic this is NOT
|
|
4111
|
+
* a "no". **`500` belongs beside them, not with the 4xx arms.** It is an
|
|
4112
|
+
* unhandled throw: nothing decided the request's merits, and the bind may even
|
|
4113
|
+
* have half-happened if the throw landed after a commit. On this route the
|
|
4114
|
+
* body is the same degraded "the URL returned 500" a 502/504 produces — an
|
|
4115
|
+
* internal path echoed back with no advice — so `refused (500)` would send a
|
|
4116
|
+
* developer to re-check ownership, the repository name and whether it is bound
|
|
4117
|
+
* elsewhere: every 4xx question, none of them this status's actual cause.
|
|
4118
|
+
*
|
|
4119
|
+
* So this is every `5xx`, not an enumerated set of three — the boundary is
|
|
4120
|
+
* "did any handler decide yes or no", and a 4xx is the only family that ever did.
|
|
4121
|
+
*/
|
|
4122
|
+
get unavailable() {
|
|
4123
|
+
return this.status >= 500 && this.status < 600;
|
|
4124
|
+
}
|
|
4125
|
+
};
|
|
4126
|
+
async function requestBinding(method, serviceUrl, engagementId, repository, credential, noun) {
|
|
4127
|
+
const url = `${serviceUrl.replace(/\/+$/, "")}/engagements/${encodeURIComponent(engagementId)}/ci-bindings/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}`;
|
|
4128
|
+
let res;
|
|
4129
|
+
try {
|
|
4130
|
+
res = await fetch(url, { method, headers: { authorization: `Bearer ${credential}` } });
|
|
4131
|
+
} catch (err) {
|
|
4132
|
+
throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. Nothing was ${noun}.`);
|
|
4133
|
+
}
|
|
4134
|
+
if (res.status === 204)
|
|
4135
|
+
return;
|
|
4136
|
+
const body = await res.json().catch(() => null);
|
|
4137
|
+
const message = typeof body?.message === "string" ? body.message : `${url} returned ${res.status}.`;
|
|
4138
|
+
throw new CiBindRefused(res.status, message);
|
|
4139
|
+
}
|
|
4140
|
+
async function setCiBinding(action, serviceUrl, engagementId, repository, deps = {}) {
|
|
4141
|
+
const noun = action === "bind" ? "bound" : "unbound";
|
|
4142
|
+
const signInDeps = {
|
|
4143
|
+
...deps,
|
|
4144
|
+
reason: deps.reason ?? (action === "bind" ? "trust this repository for this engagement's CI" : "stop trusting this repository for this engagement's CI")
|
|
4145
|
+
};
|
|
4146
|
+
let credential = await obtainCredential(serviceUrl, signInDeps);
|
|
4147
|
+
for (; ; ) {
|
|
4148
|
+
try {
|
|
4149
|
+
await requestBinding(action === "bind" ? "PUT" : "DELETE", serviceUrl, engagementId, repository, credential.credential, noun);
|
|
4150
|
+
return;
|
|
4151
|
+
} catch (err) {
|
|
4152
|
+
if (!(err instanceof CiBindRefused) || !err.authRefused)
|
|
4153
|
+
throw err;
|
|
4154
|
+
const replacement = await replaceRefusedCredential(serviceUrl, credential, signInDeps);
|
|
4155
|
+
if (replacement === null) {
|
|
4156
|
+
throw new SignInRefused(`ci-${action}-${err.status}`, `${err.message} ${refusedCredentialRemedy(credential.source)} Nothing was ${noun}.`);
|
|
4157
|
+
}
|
|
4158
|
+
process.stdout.write(`[halfcycle] Your saved Halfcycle sign-in was refused \u2014 signing you in again.
|
|
4159
|
+
`);
|
|
4160
|
+
credential = replacement;
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function bindCiRepository(serviceUrl, engagementId, repository, deps = {}) {
|
|
4165
|
+
return setCiBinding("bind", serviceUrl, engagementId, repository, deps);
|
|
4166
|
+
}
|
|
4167
|
+
function unbindCiRepository(serviceUrl, engagementId, repository, deps = {}) {
|
|
4168
|
+
return setCiBinding("unbind", serviceUrl, engagementId, repository, deps);
|
|
4169
|
+
}
|
|
4170
|
+
|
|
3840
4171
|
// dist/cli-contract.js
|
|
3841
|
-
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase"];
|
|
4172
|
+
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "ci"];
|
|
3842
4173
|
var CONTRACTS = [
|
|
3843
|
-
// `install`, `check-drift
|
|
3844
|
-
// flags. They are declared so the verb set has ONE home — a remedy
|
|
3845
|
-
// this CLI does not have is the same defect as one missing a flag.
|
|
4174
|
+
// `install`, `check-drift`, `build-record` and `ci` take positionals and no
|
|
4175
|
+
// required flags. They are declared so the verb set has ONE home — a remedy
|
|
4176
|
+
// naming a verb this CLI does not have is the same defect as one missing a flag.
|
|
4177
|
+
//
|
|
4178
|
+
// `ci`'s two forms (`ci bind <owner>/<repo>`, `ci unbind <owner>/<repo>`) take a
|
|
4179
|
+
// sub-action and a repository as POSITIONALS, not flags — this table answers only
|
|
4180
|
+
// *which flags must an invocation carry*, and neither form has one (T-11,
|
|
4181
|
+
// `ci-oidc-token-exchange`). The route it calls declares no wire shape either
|
|
4182
|
+
// (path segments in, `204` out — T-05), so there is no schema for an argument
|
|
4183
|
+
// here to disagree with.
|
|
3846
4184
|
{ verb: "install", required: [], conditional: [] },
|
|
3847
4185
|
{ verb: "check-drift", required: [], conditional: [] },
|
|
3848
4186
|
{ verb: "build-record", required: [], conditional: [] },
|
|
4187
|
+
{ verb: "ci", required: [], conditional: [] },
|
|
3849
4188
|
{
|
|
3850
4189
|
verb: "open-phase",
|
|
3851
4190
|
required: [
|
|
@@ -3879,7 +4218,7 @@ var CONTRACTS = [
|
|
|
3879
4218
|
{
|
|
3880
4219
|
flag: "--verdict",
|
|
3881
4220
|
takesValue: true,
|
|
3882
|
-
missingMessage: `halfcycle close-phase: --verdict must be "clean" or "defects" \u2014 it is the acceptance walk's own verdict
|
|
4221
|
+
missingMessage: `halfcycle close-phase: --verdict must be "clean" or "defects" \u2014 it is the acceptance walk's own verdict and nothing derives it for you.`
|
|
3883
4222
|
},
|
|
3884
4223
|
{
|
|
3885
4224
|
flag: "--actor",
|
|
@@ -4038,10 +4377,9 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
|
|
|
4038
4377
|
fact("Claude Code", probes.claudeCodeVersion()),
|
|
4039
4378
|
fact("Project", project, name),
|
|
4040
4379
|
fact("Status", status, installed === void 0 ? "new" : "installed"),
|
|
4041
|
-
// `Website`, NOT `Plane`
|
|
4042
|
-
//
|
|
4043
|
-
//
|
|
4044
|
-
// answers *which Halfcycle is this*, and an address is the answer they recognise.
|
|
4380
|
+
// `Website`, NOT `Plane` — see this function's docblock. (No issue number here:
|
|
4381
|
+
// a comment beside a property survives into the published entry, unlike a
|
|
4382
|
+
// leading docblock, which esbuild drops.)
|
|
4045
4383
|
fact("Website", probes.websiteHost(env))
|
|
4046
4384
|
].filter((f) => f !== void 0);
|
|
4047
4385
|
return {
|
|
@@ -4121,6 +4459,8 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
4121
4459
|
halfcycle build-record <phase> [--repo <root>]
|
|
4122
4460
|
halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
|
|
4123
4461
|
halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
4462
|
+
halfcycle ci bind <owner>/<repo> [--repo <root>]
|
|
4463
|
+
halfcycle ci unbind <owner>/<repo> [--repo <root>]
|
|
4124
4464
|
|
|
4125
4465
|
--quiet / -q: print no opening banner (any command)
|
|
4126
4466
|
--verbose / -v: print full run detail (paths written, merged, skipped) on install
|
|
@@ -4135,7 +4475,7 @@ function isHalfcycleMonorepo(dir) {
|
|
|
4135
4475
|
}
|
|
4136
4476
|
async function main() {
|
|
4137
4477
|
const [cmd, ...rest] = args;
|
|
4138
|
-
const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase)$/.test(cmd);
|
|
4478
|
+
const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase|ci)$/.test(cmd);
|
|
4139
4479
|
const installArm = cmd === "install" || cmd === void 0 || bareTarget;
|
|
4140
4480
|
const positionals = cmd === "install" ? rest : args;
|
|
4141
4481
|
const targetRepo = installArm ? positionals[0] ?? process.cwd() : process.cwd();
|
|
@@ -4176,7 +4516,9 @@ ${USAGE}`);
|
|
|
4176
4516
|
}
|
|
4177
4517
|
const pinned = readPinnedEngagement(targetRepo);
|
|
4178
4518
|
const requestedId = engagementIdArg ?? pinned?.engagementId;
|
|
4179
|
-
const
|
|
4519
|
+
const held = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
|
|
4520
|
+
const heldAnswer = held !== void 0 ? await askAccount(held.serviceUrl, held.token) : void 0;
|
|
4521
|
+
const reusable = heldAnswer?.refused === true ? void 0 : held;
|
|
4180
4522
|
let engagementId;
|
|
4181
4523
|
let credential;
|
|
4182
4524
|
let notThisAccountMessage;
|
|
@@ -4322,7 +4664,7 @@ ${USAGE}`);
|
|
|
4322
4664
|
`);
|
|
4323
4665
|
}
|
|
4324
4666
|
reportClaudeCodeVersion(verbose);
|
|
4325
|
-
const identity = actingIdentity ?? (credential ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
4667
|
+
const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
4326
4668
|
const written = result.writtenPaths.length;
|
|
4327
4669
|
const merged = result.mergedPaths.length;
|
|
4328
4670
|
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
@@ -4593,6 +4935,69 @@ halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard e
|
|
|
4593
4935
|
return;
|
|
4594
4936
|
}
|
|
4595
4937
|
process.stderr.write(`halfcycle close-phase failed: ${err instanceof Error ? err.message : String(err)}
|
|
4938
|
+
`);
|
|
4939
|
+
process.exit(1);
|
|
4940
|
+
}
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
if (cmd === "ci") {
|
|
4944
|
+
const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
|
|
4945
|
+
const positionals2 = rest.filter((token, i) => !token.startsWith("--") && !(i > 0 && rest[i - 1] === "--repo"));
|
|
4946
|
+
const action = positionals2[0];
|
|
4947
|
+
const repositoryArg = positionals2[1];
|
|
4948
|
+
if (action !== "bind" && action !== "unbind") {
|
|
4949
|
+
process.stderr.write("halfcycle ci: name bind or unbind.\n halfcycle ci bind <owner>/<repo> [--repo <root>]\n halfcycle ci unbind <owner>/<repo> [--repo <root>]\n");
|
|
4950
|
+
process.exit(2);
|
|
4951
|
+
return;
|
|
4952
|
+
}
|
|
4953
|
+
if (repositoryArg === void 0) {
|
|
4954
|
+
process.stderr.write(`halfcycle ci ${action}: name the repository, as owner/repo.
|
|
4955
|
+
`);
|
|
4956
|
+
process.exit(2);
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
const repository = parseCiRepositoryArg(repositoryArg);
|
|
4960
|
+
if (repository === null) {
|
|
4961
|
+
process.stderr.write(`halfcycle ci ${action}: "${repositoryArg}" is not a repository \u2014 give it as owner/repo, exactly as GitHub spells it, for example acme/widgets.
|
|
4962
|
+
`);
|
|
4963
|
+
process.exit(2);
|
|
4964
|
+
return;
|
|
4965
|
+
}
|
|
4966
|
+
const pin = readBundlePin(repoRoot);
|
|
4967
|
+
if (pin === null || pin.engagementId === "") {
|
|
4968
|
+
process.stderr.write(`halfcycle ci ${action}: ${join11(repoRoot, ".halfcycle", "bundle.json")} names no Halfcycle engagement. Run \`npx halfcycle\` here first.
|
|
4969
|
+
`);
|
|
4970
|
+
process.exit(1);
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
try {
|
|
4974
|
+
const controlOrigin = resolveControlOrigin(process.env);
|
|
4975
|
+
const label = `${repository.owner}/${repository.repo}`;
|
|
4976
|
+
if (action === "bind") {
|
|
4977
|
+
await bindCiRepository(controlOrigin.origin, pin.engagementId, repository);
|
|
4978
|
+
process.stdout.write(`[halfcycle] ${label} now trusts this engagement's CI \u2014 a workflow there granting both \`contents: read\` and \`id-token: write\` can authenticate with no stored secret. Both lines: declaring any permission replaces the defaults, so naming only the identity token stops a private repository checking out.
|
|
4979
|
+
`);
|
|
4980
|
+
} else {
|
|
4981
|
+
await unbindCiRepository(controlOrigin.origin, pin.engagementId, repository);
|
|
4982
|
+
process.stdout.write(`[halfcycle] ${label} no longer trusts this engagement's CI.
|
|
4983
|
+
`);
|
|
4984
|
+
}
|
|
4985
|
+
process.exit(0);
|
|
4986
|
+
} catch (err) {
|
|
4987
|
+
if (err instanceof SignInRefused) {
|
|
4988
|
+
process.stderr.write(`halfcycle ci ${action}: ${err.message}
|
|
4989
|
+
`);
|
|
4990
|
+
process.exit(1);
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
if (err instanceof CiBindRefused) {
|
|
4994
|
+
const label = err.unavailable ? `temporarily unavailable (${err.status})` : `refused (${err.status})`;
|
|
4995
|
+
process.stderr.write(`halfcycle ci ${action}: ${label}. ${err.message}
|
|
4996
|
+
`);
|
|
4997
|
+
process.exit(1);
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
process.stderr.write(`halfcycle ci ${action} failed: ${err instanceof Error ? err.message : String(err)}
|
|
4596
5001
|
`);
|
|
4597
5002
|
process.exit(1);
|
|
4598
5003
|
}
|