halfcycle 0.3.7
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 +16 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/bin/bin.bundle.mjs +16771 -0
- package/commands/agent-teams-orchestration.md +11 -0
- package/commands/consistency-check.md +11 -0
- package/commands/context-update.md +11 -0
- package/commands/halfcycle-design.md +11 -0
- package/commands/halfcycle-setup.md +11 -0
- package/commands/stack-assembly-worker.md +11 -0
- package/commands/task-execution.md +11 -0
- package/commands/task-graph.md +11 -0
- package/commands/write-spec.md +11 -0
- package/dist/account-credential.d.ts +94 -0
- package/dist/account-credential.d.ts.map +1 -0
- package/dist/bin.d.ts +34 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +3156 -0
- package/dist/bin.js.map +6 -0
- package/dist/build-record/assemble.d.ts +37 -0
- package/dist/build-record/assemble.d.ts.map +1 -0
- package/dist/build-record/index.d.ts +39 -0
- package/dist/build-record/index.d.ts.map +1 -0
- package/dist/build-record/sources.d.ts +80 -0
- package/dist/build-record/sources.d.ts.map +1 -0
- package/dist/build-record/template.d.ts +18 -0
- package/dist/build-record/template.d.ts.map +1 -0
- package/dist/build-record/types.d.ts +163 -0
- package/dist/build-record/types.d.ts.map +1 -0
- package/dist/build-record/write.d.ts +26 -0
- package/dist/build-record/write.d.ts.map +1 -0
- package/dist/cli-contract.d.ts +120 -0
- package/dist/cli-contract.d.ts.map +1 -0
- package/dist/close-phase.d.ts +114 -0
- package/dist/close-phase.d.ts.map +1 -0
- package/dist/create-engagement.d.ts +248 -0
- package/dist/create-engagement.d.ts.map +1 -0
- package/dist/device-signin.d.ts +255 -0
- package/dist/device-signin.d.ts.map +1 -0
- package/dist/engagement-credential.d.ts +267 -0
- package/dist/engagement-credential.d.ts.map +1 -0
- package/dist/identity.d.ts +76 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2083 -0
- package/dist/index.js.map +6 -0
- package/dist/install.d.ts +355 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/loopback-signin.d.ts +173 -0
- package/dist/loopback-signin.d.ts.map +1 -0
- package/dist/mcp-endpoint.d.ts +35 -0
- package/dist/mcp-endpoint.d.ts.map +1 -0
- package/dist/merge-settings.d.ts +57 -0
- package/dist/merge-settings.d.ts.map +1 -0
- package/dist/mint-board-code.d.ts +41 -0
- package/dist/mint-board-code.d.ts.map +1 -0
- package/dist/open-phase.d.ts +107 -0
- package/dist/open-phase.d.ts.map +1 -0
- package/dist/probe-mcp.d.ts +47 -0
- package/dist/probe-mcp.d.ts.map +1 -0
- package/dist/resolve-credential.d.ts +126 -0
- package/dist/resolve-credential.d.ts.map +1 -0
- package/dist/scan.d.ts +131 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/setup/discovery.d.ts +73 -0
- package/dist/setup/discovery.d.ts.map +1 -0
- package/dist/setup/engagement.d.ts +99 -0
- package/dist/setup/engagement.d.ts.map +1 -0
- package/dist/setup/index.d.ts +17 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/manifest.d.ts +155 -0
- package/dist/setup/manifest.d.ts.map +1 -0
- package/dist/setup/rows.d.ts +57 -0
- package/dist/setup/rows.d.ts.map +1 -0
- package/package.json +55 -0
- package/scaffolding/test/fixtures/captured/.gitkeep +0 -0
- package/scaffolding/test/fixtures/captured/manifest.json +4 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BROWSER HANDOFF, client half — `npx halfcycle` signing a machine in (T-08,
|
|
3
|
+
* feature `connect-signup-and-credential`; **loopback added by T-25, #614**).
|
|
4
|
+
*
|
|
5
|
+
* The CLI mints a device code, opens a browser, and waits until a signed-in human
|
|
6
|
+
* approves or refuses. That direction — CLI mints, browser redeems — is the same as
|
|
7
|
+
* the board's enter-code flow.
|
|
8
|
+
*
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
* SINCE T-25 THERE ARE TWO WAYS THE ANSWER COMES BACK, and `signIn` below is where
|
|
11
|
+
* the choice is made:
|
|
12
|
+
*
|
|
13
|
+
* LOOPBACK (the default) — the CLI listens on `127.0.0.1`, the page redirects the
|
|
14
|
+
* browser back to it, and NO CODE IS SHOWN. `loopback-signin.ts` owns the listener.
|
|
15
|
+
*
|
|
16
|
+
* DEVICE CODE (the fallback) — the code is printed, the human compares it with the
|
|
17
|
+
* page, and this file polls. Unchanged, and the right answer whenever the browser
|
|
18
|
+
* is not on this machine.
|
|
19
|
+
*
|
|
20
|
+
* The fallback is chosen from two MEASUREMENTS — a bind that failed, or a browser
|
|
21
|
+
* opener that failed — and never from reading the environment and guessing. See
|
|
22
|
+
* `signIn` and `openBrowser`.
|
|
23
|
+
*
|
|
24
|
+
* ---------------------------------------------------------------------------
|
|
25
|
+
* THE WIRE SHAPES ARE IMPORTED, NEVER RE-DECLARED. Every payload and every `status`
|
|
26
|
+
* value comes from `@halfcycle/events`, which is where this contract lives since
|
|
27
|
+
* this task (INV-003, INV-001 — `packages/events` is on the shippable allowlist).
|
|
28
|
+
* A hand-written `interface StartResponse` here would be a second home for a shape
|
|
29
|
+
* whose other end is a deployed service, and the two would agree until the first
|
|
30
|
+
* time one of them changed.
|
|
31
|
+
*
|
|
32
|
+
* The responses are PARSED, not cast. A cast asserts nothing: a plane that answered
|
|
33
|
+
* `{}` would satisfy `as DeviceAuthStartResponse` and produce a CLI that opened
|
|
34
|
+
* `undefined` in a browser and polled with an empty code for fifteen minutes.
|
|
35
|
+
*
|
|
36
|
+
* ---------------------------------------------------------------------------
|
|
37
|
+
* FOUR WAYS THIS ENDS, and each is a different sentence and a non-zero exit:
|
|
38
|
+
*
|
|
39
|
+
* `declined` — a human refused at the page. Nothing was created.
|
|
40
|
+
* `expired` — the code's window closed on CONTROL's clock before anyone acted.
|
|
41
|
+
* `unknown` — the server is not waiting on this code (never minted, or spent).
|
|
42
|
+
* unreachable — the plane stopped answering mid-poll. This one is OURS to word,
|
|
43
|
+
* because control cannot send a sentence about a request it never
|
|
44
|
+
* received.
|
|
45
|
+
*
|
|
46
|
+
* The first three are control's own words, forwarded verbatim from
|
|
47
|
+
* `@halfcycle/events`, because they were written for exactly this reader and each
|
|
48
|
+
* already names what to do next. Re-wording them here would give the terminal and
|
|
49
|
+
* the confirmation page two different accounts of one event.
|
|
50
|
+
*
|
|
51
|
+
* ---------------------------------------------------------------------------
|
|
52
|
+
* THE EXPIRY IS THE SERVER'S. The poll runs until control answers a terminal state
|
|
53
|
+
* or until the local clock passes the `expiresAt` control declared, plus one poll
|
|
54
|
+
* interval of slack. The client never decides that a code has expired while control
|
|
55
|
+
* still considers it live — it keeps asking, and control's `expired` is what ends
|
|
56
|
+
* the wait. The local deadline exists only so an unreachable plane cannot make this
|
|
57
|
+
* loop unbounded; crossing it is reported as a TIMEOUT, in its own words, and never
|
|
58
|
+
* as "expired", which would be a claim about a server state nobody observed.
|
|
59
|
+
*
|
|
60
|
+
* INV-001: Node builtins, the global `fetch` (Node 20), and `@halfcycle/events`.
|
|
61
|
+
*/
|
|
62
|
+
import { type DeviceAuthStartResponse, type DevicePollApproved } from '@halfcycle/events';
|
|
63
|
+
/**
|
|
64
|
+
* A sign-in that ended without a credential.
|
|
65
|
+
*
|
|
66
|
+
* `status` carries control's own discriminant when control produced one, and the
|
|
67
|
+
* local `'timed-out'` when the plane stopped answering. It is exposed so a caller
|
|
68
|
+
* can branch (`bin.ts` does not, today — it prints and exits — but a refusal a
|
|
69
|
+
* caller cannot tell apart is a refusal that gets retried in a loop by the next
|
|
70
|
+
* person who tries).
|
|
71
|
+
*/
|
|
72
|
+
export declare class SignInRefused extends Error {
|
|
73
|
+
readonly status: string;
|
|
74
|
+
constructor(status: string, message: string);
|
|
75
|
+
}
|
|
76
|
+
/** The local status for "the plane stopped answering". Not a control status. */
|
|
77
|
+
export declare const SIGN_IN_TIMED_OUT = "timed-out";
|
|
78
|
+
/**
|
|
79
|
+
* The sentence for a poll that outlived its local deadline without control ever
|
|
80
|
+
* answering. Distinct from `DEVICE_EXPIRED_MESSAGE` on purpose: that one is a fact
|
|
81
|
+
* control asserted, this one is a fact only the client observed, and telling a
|
|
82
|
+
* developer their code expired when what actually happened is that the server went
|
|
83
|
+
* away sends them to re-run instead of to check their connection.
|
|
84
|
+
*/
|
|
85
|
+
export declare const SIGN_IN_TIMEOUT_MESSAGE: string;
|
|
86
|
+
/**
|
|
87
|
+
* The sentence for a non-interactive run with no credential in the environment.
|
|
88
|
+
*
|
|
89
|
+
* A CI job has no browser and nobody to look at a code, so minting one and waiting
|
|
90
|
+
* fifteen minutes for a human who does not exist is the worst available failure: it
|
|
91
|
+
* burns the job's time and then reports something that reads like a platform
|
|
92
|
+
* outage. It refuses immediately instead, and names the ONE thing a workflow can
|
|
93
|
+
* actually do.
|
|
94
|
+
*/
|
|
95
|
+
export declare const SIGN_IN_NON_INTERACTIVE_MESSAGE: string;
|
|
96
|
+
/** Everything this module touches that a test may need to hold still. */
|
|
97
|
+
export interface SignInDeps {
|
|
98
|
+
/** Where the CLI writes its narration. Defaults to stdout. */
|
|
99
|
+
write?: (text: string) => void;
|
|
100
|
+
/** Sleep between polls. Defaults to a real timer. */
|
|
101
|
+
sleep?: (ms: number) => Promise<void>;
|
|
102
|
+
/** The clock the local deadline is measured on. Defaults to `Date.now`. */
|
|
103
|
+
now?: () => number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The platform command that opens a URL in the user's browser, as `{command, args}`.
|
|
107
|
+
*
|
|
108
|
+
* A PURE FUNCTION so the argv can be asserted on every platform from any platform —
|
|
109
|
+
* the alternative is a test that only ever checks the branch it happens to be
|
|
110
|
+
* running on, which on this repo's CI would mean the Windows and Linux arms were
|
|
111
|
+
* never looked at.
|
|
112
|
+
*
|
|
113
|
+
* NO SHELL, EVER. The URL is an argv element, so a value containing shell
|
|
114
|
+
* metacharacters is an odd URL and not a command. `start` on Windows is the one
|
|
115
|
+
* that would need a shell, so it is invoked through `cmd /c start ""` with an empty
|
|
116
|
+
* title argument — without that empty string, `start` treats a quoted URL as the
|
|
117
|
+
* window title and opens nothing.
|
|
118
|
+
*/
|
|
119
|
+
export declare function browserOpenCommand(os: string, url: string): {
|
|
120
|
+
command: string;
|
|
121
|
+
args: string[];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Is this URL safe to hand to the platform opener?
|
|
125
|
+
*
|
|
126
|
+
* The address comes off the wire, so it is a value a compromised or misconfigured
|
|
127
|
+
* plane controls. Only `http`/`https` are opened; anything else is printed instead.
|
|
128
|
+
* The opener takes argv rather than a shell string, so the exposure was bounded
|
|
129
|
+
* already — this closes the rest of it, where a `file://` URL would open a local
|
|
130
|
+
* document and a handler-scheme URL would launch whatever claims that scheme.
|
|
131
|
+
*/
|
|
132
|
+
export declare function isOpenableUrl(url: string): boolean;
|
|
133
|
+
/** What was MEASURED when this run tried to open a browser. */
|
|
134
|
+
export interface BrowserOpenAttempt {
|
|
135
|
+
/** A browser was launched. Nobody claims the human looked at it. */
|
|
136
|
+
opened: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Why not, in a clause that fits inside one sentence. Present iff `opened` is
|
|
139
|
+
* false, and it always names an OBSERVATION — an exit code, a spawn error, an
|
|
140
|
+
* explicit instruction — never an inference about the kind of machine this is.
|
|
141
|
+
*/
|
|
142
|
+
reason?: string;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Open a browser, and MEASURE whether that worked.
|
|
146
|
+
*
|
|
147
|
+
* ---------------------------------------------------------------------------
|
|
148
|
+
* THIS FUNCTION IS ACCEPTANCE CRITERION 3. The loopback flow needs to know whether a
|
|
149
|
+
* browser actually came up, because that is half of what decides whether the sign-in
|
|
150
|
+
* can complete without a code — and the criterion says the decision must come from a
|
|
151
|
+
* measurement, not from sniffing the environment and guessing.
|
|
152
|
+
*
|
|
153
|
+
* **The previous version could not answer the question, and returned `true` as
|
|
154
|
+
* though it could.** It spawned `detached`, attached an empty `error` sink and
|
|
155
|
+
* returned `true` the instant `spawn` did not throw synchronously — but `spawn`
|
|
156
|
+
* reports a missing binary ASYNCHRONOUSLY, on the `error` event, and reports a
|
|
157
|
+
* non-zero exit later still. So `openBrowser('…')` on a box with no `xdg-open`
|
|
158
|
+
* returned `true`. That was harmless while the answer only chose between two
|
|
159
|
+
* printed sentences and the poll ran either way; it is not harmless now, because a
|
|
160
|
+
* wrong `true` here is a terminal that waits in silence for a browser that never
|
|
161
|
+
* opened.
|
|
162
|
+
*
|
|
163
|
+
* So the child is spawned ATTACHED and watched for whichever comes first:
|
|
164
|
+
* - `error` → could not run it at all (`ENOENT` — no `xdg-open` on PATH).
|
|
165
|
+
* - `exit(≠0)` → it ran and refused (`xdg-open` with no display).
|
|
166
|
+
* - `exit(0)` → handed off (macOS/Windows).
|
|
167
|
+
* - still up after `BROWSER_OPEN_GRACE_MS` → it is the browser (Linux).
|
|
168
|
+
*
|
|
169
|
+
* On the last arm the child is `unref`'d, so a browser that outlives the install
|
|
170
|
+
* cannot hold this process open.
|
|
171
|
+
*
|
|
172
|
+
* ---------------------------------------------------------------------------
|
|
173
|
+
* `HALFCYCLE_NO_BROWSER` IS AN INSTRUCTION, NOT A HEURISTIC, and the distinction is
|
|
174
|
+
* exactly the one criterion 3 draws. It is the developer saying *do not take my
|
|
175
|
+
* browser over*; honouring it is not a guess about the machine, and the reason
|
|
176
|
+
* reported says so in those words. Nothing else in this flow reads the environment
|
|
177
|
+
* to decide which path to take.
|
|
178
|
+
*/
|
|
179
|
+
export declare function openBrowser(url: string, env?: NodeJS.ProcessEnv): Promise<BrowserOpenAttempt>;
|
|
180
|
+
/** True iff this run has no human at the keyboard. */
|
|
181
|
+
export declare function isNonInteractive(env?: NodeJS.ProcessEnv): boolean;
|
|
182
|
+
/** `POST /auth/device/start` — mint a code. Throws `SignInRefused` on a refusal. */
|
|
183
|
+
export declare function startDeviceAuth(serviceUrl: string): Promise<DeviceAuthStartResponse>;
|
|
184
|
+
/**
|
|
185
|
+
* Poll until control answers a terminal state, or until the local deadline.
|
|
186
|
+
*
|
|
187
|
+
* A TRANSIENT NETWORK FAILURE IS NOT A REFUSAL. A dropped packet mid-poll gets
|
|
188
|
+
* another attempt at the next interval, because the developer is standing in front
|
|
189
|
+
* of a browser they have already signed into and the credential is waiting on the
|
|
190
|
+
* server. Only the deadline ends the loop — and it is reported as a timeout, in the
|
|
191
|
+
* client's own words, never as one of control's three refusals.
|
|
192
|
+
*/
|
|
193
|
+
export declare function pollForCredential(serviceUrl: string, started: DeviceAuthStartResponse, deps?: SignInDeps): Promise<DevicePollApproved>;
|
|
194
|
+
/** What a completed sign-in yields. */
|
|
195
|
+
export interface SignedIn {
|
|
196
|
+
accountId: string;
|
|
197
|
+
credential: string;
|
|
198
|
+
/** Control's issue-time expiry, UTC ISO-8601. Information, never a local gate. */
|
|
199
|
+
expiresAt: string;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The sentence for a loopback sign-in whose browser never came back.
|
|
203
|
+
*
|
|
204
|
+
* DISTINCT FROM `SIGN_IN_TIMEOUT_MESSAGE`, which is about a plane that stopped
|
|
205
|
+
* answering. Here the plane was fine and the terminal was waiting on a redirect from
|
|
206
|
+
* the browser — so the recovery is different, and it names the flow that does not
|
|
207
|
+
* need one.
|
|
208
|
+
*/
|
|
209
|
+
export declare const LOOPBACK_TIMEOUT_MESSAGE: string;
|
|
210
|
+
/**
|
|
211
|
+
* The whole terminal experience — **loopback first, the device code as the fallback**
|
|
212
|
+
* (T-25, #614).
|
|
213
|
+
*
|
|
214
|
+
* ---------------------------------------------------------------------------
|
|
215
|
+
* THE TWO PATHS, AND HOW THE CHOICE BETWEEN THEM IS MADE.
|
|
216
|
+
*
|
|
217
|
+
* LOOPBACK — bind `127.0.0.1:0`, put that port in the URL we open, and wait for
|
|
218
|
+
* the confirmation page to send the browser back. **No code is displayed at all**
|
|
219
|
+
* (criterion 1): there is nothing for the human to compare, so printing a code
|
|
220
|
+
* would be printing a value with no use.
|
|
221
|
+
*
|
|
222
|
+
* DEVICE CODE — print the code, open (or print) the page, and poll. Unchanged from
|
|
223
|
+
* T-08, still tested, and still correct for every machine whose browser is not the
|
|
224
|
+
* one this process can talk to: SSH, containers, remote dev boxes, Codespaces.
|
|
225
|
+
*
|
|
226
|
+
* **THE FALLBACK IS CHOSEN BY TWO MEASUREMENTS AND NOTHING ELSE** (criterion 3):
|
|
227
|
+
* `bindLoopback` tried to listen and reports the `EADDRINUSE`/`EACCES` it got, and
|
|
228
|
+
* `openBrowser` spawned the platform opener and reports its exit code or spawn error.
|
|
229
|
+
* Nothing here reads `SSH_CONNECTION`, `DISPLAY`, `SSH_TTY`, `TERM`, `container` or
|
|
230
|
+
* any other variable to infer what kind of machine this is — an inference that is
|
|
231
|
+
* wrong in both directions, since a container with a forwarded browser has no
|
|
232
|
+
* `DISPLAY` and works, and a kiosked desktop has one and does not. The single
|
|
233
|
+
* environment variable consulted, `HALFCYCLE_NO_BROWSER`, is the developer's own
|
|
234
|
+
* instruction not to open a browser, and it is honoured rather than interpreted.
|
|
235
|
+
*
|
|
236
|
+
* **ORDER MATTERS AND IS DELIBERATE.** The listener is bound BEFORE the code is
|
|
237
|
+
* minted, so a machine that cannot listen has burned no code by the time it falls
|
|
238
|
+
* back; and the code is minted once for both paths, so a fallback is never a second
|
|
239
|
+
* `POST /auth/device/start`.
|
|
240
|
+
*
|
|
241
|
+
* **THE FALLBACK OPENS THE UNDECORATED URL.** `started.verificationUrl` is what
|
|
242
|
+
* control composed; the loopback parameters are added only for the loopback attempt.
|
|
243
|
+
* Handing the page a port nothing is listening on would make it redirect the browser
|
|
244
|
+
* to a connection error after a successful approval — a page that looks broken for a
|
|
245
|
+
* sign-in that worked.
|
|
246
|
+
*
|
|
247
|
+
* WHAT IS PRINTED AND WHAT IS NOT, on the device-code path: the USER code is printed
|
|
248
|
+
* — it is a selector a human compares between two screens and yields nothing to
|
|
249
|
+
* whoever reads it, since only the device code can claim the credential. The DEVICE
|
|
250
|
+
* code and the credential are printed nowhere, ever, and appear in no URL.
|
|
251
|
+
*/
|
|
252
|
+
export declare function signIn(serviceUrl: string, deps?: SignInDeps & {
|
|
253
|
+
env?: NodeJS.ProcessEnv;
|
|
254
|
+
}): Promise<SignedIn>;
|
|
255
|
+
//# sourceMappingURL=device-signin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-signin.d.ts","sourceRoot":"","sources":["../src/device-signin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAIH,OAAO,EAKL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAG3B;;;;;;;;GAQG;AACH,qBAAa,aAAc,SAAQ,KAAK;aAEpB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;CAKlB;AAED,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,QAGV,CAAC;AAE3B;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,QAGsC,CAAC;AAQnF,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,qDAAqD;IACrD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAIrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOlD;AAaD,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,kBAAkB,CAAC,CAmE7B;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAG9E;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAsC1F;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,uBAAuB,EAChC,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAmD7B;AAED,uCAAuC;AACvC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAIF,CAAC;AAiEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAsB,MAAM,CAC1B,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,UAAU,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GAClD,OAAO,CAAC,QAAQ,CAAC,CAmFnB"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PER-ENGAGEMENT CREDENTIAL STORE — `~/.halfcycle/engagements/<id>/env`
|
|
3
|
+
* (T-09, feature `connect-signup-and-credential`).
|
|
4
|
+
*
|
|
5
|
+
* This is the credential that authenticates a REPOSITORY to the Halfcycle plane:
|
|
6
|
+
* the session token minted once on create, under the two names its two consumers
|
|
7
|
+
* read, plus the three origins and the engagement id. Until T-09 it lived in the
|
|
8
|
+
* client tree at `.env.local`, gitignored. It now lives OUTSIDE every repository,
|
|
9
|
+
* owner-only, keyed on the engagement.
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------------------------------
|
|
12
|
+
* WHY OUT OF THE TREE AT ALL. Gitignoring a secret is a discipline; putting it
|
|
13
|
+
* where the repository cannot reach it is a structure. `.gitignore` is a tracked
|
|
14
|
+
* file a developer can edit, a `git add -f` overrides it, an editor's "commit all"
|
|
15
|
+
* has ignored it before, and a zip export carries the file regardless. Studio has
|
|
16
|
+
* never had this exposure because its layout put the per-engagement token at
|
|
17
|
+
* machine level from the start (`docs/features/mac-workspace-provider.md:27`),
|
|
18
|
+
* which is described there as making the moat *"structural, not disciplinary"*.
|
|
19
|
+
* This module adopts that layout for Connect. Nothing here is new design; it is
|
|
20
|
+
* one topology's answer applied to the other (INV-022 — Connect reaches it with a
|
|
21
|
+
* home directory and nothing else).
|
|
22
|
+
*
|
|
23
|
+
* ---------------------------------------------------------------------------
|
|
24
|
+
* IT IS AN ENV FILE AND NOT JSON, AND THAT IS FORCED — not a preference.
|
|
25
|
+
*
|
|
26
|
+
* Two consumers read this credential and both are POSIX `sh`:
|
|
27
|
+
*
|
|
28
|
+
* - `.claude/hooks/guard-runner.sh` SOURCES it (`set -a; . "$f"; set +a`) so the
|
|
29
|
+
* runner, which reads only `process.env`, receives its three variables. That is
|
|
30
|
+
* the INV-001 seam: env loading lives in the hook wiring, never in the binary.
|
|
31
|
+
* - `.halfcycle/mcp-headers.sh` READS one `KEY=` line out of it.
|
|
32
|
+
*
|
|
33
|
+
* Neither can parse JSON — there is no `jq` in this environment and `node` is not
|
|
34
|
+
* dependable on a desktop-launched session's PATH (the reason `mcp-headers.sh` is
|
|
35
|
+
* `sh` and not node in the first place). So the machine-level file is `KEY=value`
|
|
36
|
+
* lines, exactly as Studio's is, and the ONE field that must come out of the
|
|
37
|
+
* committed pin — the engagement id — is cut out of JSON with `sed`. See
|
|
38
|
+
* `engagementResolutionShell()` for that, and for why `tr -d '\n'` precedes it.
|
|
39
|
+
*
|
|
40
|
+
* ---------------------------------------------------------------------------
|
|
41
|
+
* THE SEVEN — NOW EIGHT (T-10) — KEYS ARE FOUR THINGS, AND THE COUNT DECIDES WHAT
|
|
42
|
+
* MAY BE COMMITTED. Four public origins (the fourth, `CONTROL_TELEMETRY_URL`,
|
|
43
|
+
* joined at T-10), the engagement id twice, and ONE token under two names —
|
|
44
|
+
* `HALFCYCLE_TOKEN` and `GUARD_SERVICE_TOKEN` are byte-identical, one credential
|
|
45
|
+
* under the two names its two consumers read. So six of the eight keys are not
|
|
46
|
+
* secret at all. **They still do not go into the committed pin.** An earlier draft
|
|
47
|
+
* of the feature spec counted six non-secret keys out of SEVEN, which would have
|
|
48
|
+
* sent `GUARD_SERVICE_TOKEN` to a tracked file — the exact outcome this work exists
|
|
49
|
+
* to prevent — and the safe reading is the one that keeps the credential's key set
|
|
50
|
+
* whole and in one place: everything the shells need is here, and the pin
|
|
51
|
+
* contributes only the id that names this directory. `CONTROL_TELEMETRY_URL`
|
|
52
|
+
* joining the non-secret count does not reopen that count: it is a fourth origin
|
|
53
|
+
* added after the draft that got the arithmetic wrong, not a recount of the same
|
|
54
|
+
* seven.
|
|
55
|
+
*
|
|
56
|
+
* ---------------------------------------------------------------------------
|
|
57
|
+
* OWNER-ONLY, AND WHAT THAT MEANS WHERE IT MEANS NOTHING (phase decision 6).
|
|
58
|
+
*
|
|
59
|
+
* - macOS and Linux: the directory is created `0700` and the file written `0600`,
|
|
60
|
+
* then `chmodSync`'d explicitly AFTER the write — `writeFileSync`'s `mode` is
|
|
61
|
+
* honoured only when the file is CREATED, so a store that already existed at
|
|
62
|
+
* `0644` would keep those bits forever, a permission bug with no symptom.
|
|
63
|
+
* - Windows: POSIX mode bits are meaningless and `chmod` is a near no-op, so the
|
|
64
|
+
* call is SKIPPED rather than issued-and-ignored. Nothing here reads as a
|
|
65
|
+
* protection that is not there. What Windows does get is the part that matters
|
|
66
|
+
* most and is platform-independent: the credential is in `%USERPROFILE%`, not
|
|
67
|
+
* in the repository, so it cannot be committed, pushed or zipped with the
|
|
68
|
+
* product. Per-user ACLs on `%USERPROFILE%` are the OS's own default and are
|
|
69
|
+
* not something this installer sets or can honestly claim to have set. The two
|
|
70
|
+
* generated consumers are POSIX `sh` and run under Git Bash / WSL there.
|
|
71
|
+
*
|
|
72
|
+
* The same posture, for the same reasons, as `account-credential.ts` — which is
|
|
73
|
+
* why `halfcycleHome()` is imported from it rather than re-derived.
|
|
74
|
+
*
|
|
75
|
+
* ---------------------------------------------------------------------------
|
|
76
|
+
* TWO HOMES FOR THIS PATH SHAPE ACROSS THE REPO, AND IT IS A PRESENT CONSTRAINT,
|
|
77
|
+
* NOT AN OVERSIGHT. `packages/mac-workspace-provider/src/registry.ts` has its own
|
|
78
|
+
* `engagementEnvPath()` for the identical path — Studio's writer. `packages/bundle`
|
|
79
|
+
* is shippable and its import allowlist is exactly `{packages/core, packages/events}`
|
|
80
|
+
* (INV-001), and the provider is hosted-only, so the two cannot share a function
|
|
81
|
+
* today. `packages/core` is the one package both may import and is where this
|
|
82
|
+
* belongs eventually; that is the same direction `GUARD_ENV_KEYS`'s docblock in
|
|
83
|
+
* `install.ts` records as #476, and it is deliberately not done mid-wave. What
|
|
84
|
+
* binds the two meanwhile is a test that reads the provider's source text off disk
|
|
85
|
+
* and asserts the shapes agree (`test/credential-out-of-repo.test.ts`) — an `fs`
|
|
86
|
+
* read, not an import, so there is no build edge and nothing crosses the moat.
|
|
87
|
+
*
|
|
88
|
+
* INV-001: Node builtins and this package's own modules only.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* The field the committed pin (`.halfcycle/bundle.json`) carries the engagement id
|
|
92
|
+
* under. Named once here because BOTH readers of it — `readEngagementCredential`'s
|
|
93
|
+
* TS caller and the generated POSIX `sed` — must agree on it, and the pin's writer
|
|
94
|
+
* (`writeBundlePin`) is in `install.ts`. A test installs, then runs the generated
|
|
95
|
+
* shell against the real written pin and compares its answer to
|
|
96
|
+
* `engagementEnvPath()`, so writer and both readers are asserted to agree rather
|
|
97
|
+
* than left to match by eye.
|
|
98
|
+
*/
|
|
99
|
+
export declare const PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
100
|
+
/** `~/.halfcycle/engagements/<id>/` — this engagement's machine-level state dir. */
|
|
101
|
+
export declare function engagementStateDir(engagementId: string, home?: string): string;
|
|
102
|
+
/** `~/.halfcycle/engagements/<id>/env` — the owner-only per-engagement env file. */
|
|
103
|
+
export declare function engagementEnvPath(engagementId: string, home?: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* The three variables the generated guard hook requires — **the bundle's one home
|
|
106
|
+
* for those names.** Anything in this package that needs to name them (the write
|
|
107
|
+
* set below, the installer's report in `bin.ts`) reads them from here rather than
|
|
108
|
+
* retyping them into a string.
|
|
109
|
+
*
|
|
110
|
+
* **This is the SECOND copy across the repo — a PRESENT constraint, not a permanent
|
|
111
|
+
* one (see #476).** The authoritative list is `packages/runner/src/config.ts`, the
|
|
112
|
+
* runner's sole `process.env` read site, and a shippable may not import the runner,
|
|
113
|
+
* which is wired as an external binary contract — so today the two cannot share a
|
|
114
|
+
* constant. **`packages/core` is the one package both shippables may import under
|
|
115
|
+
* INV-001**, so moving this constant there would remove the duplication rather than
|
|
116
|
+
* test around it; that is #476, deliberately not done mid-wave, because a
|
|
117
|
+
* shared-package change during an active wave is a class this repo has been burned
|
|
118
|
+
* by. Do not read the paragraph below as an argument that the duplication is fine.
|
|
119
|
+
*
|
|
120
|
+
* What binds the two copies meanwhile is **structural, not coincidental**:
|
|
121
|
+
* `readConfig()` is a conjunction over the exact required set, and AC1's assertion
|
|
122
|
+
* in `install-guard-config.test.ts` sits strictly downstream of it — it fires the
|
|
123
|
+
* REAL installed wrapper at a recording guard service and asserts the evaluation
|
|
124
|
+
* ARRIVES, through a wrapper that names no variable. So any element the bundle
|
|
125
|
+
* fails to write breaks the conjunction observably, and — the row that decides it —
|
|
126
|
+
* **the runner adding a fourth required variable is caught with the bundle knowing
|
|
127
|
+
* nothing about it**, which a name-to-name comparison cannot do: that would pass on
|
|
128
|
+
* two lists agreeing with each other and with nothing. The one gap is superset
|
|
129
|
+
* drift (the bundle writes a name the runner ignores), which is the harmless
|
|
130
|
+
* direction and is partly caught by the `.env.example` subset check.
|
|
131
|
+
*
|
|
132
|
+
* (Moved here from `install.ts` at T-09 with its reasoning intact: the write set it
|
|
133
|
+
* feeds moved out of the repository, and the names belong beside the file they are
|
|
134
|
+
* written into.)
|
|
135
|
+
*/
|
|
136
|
+
export declare const GUARD_ENV_KEYS: readonly ["GUARD_SERVICE_URL", "GUARD_SERVICE_TOKEN", "GUARD_ENGAGEMENT_ID"];
|
|
137
|
+
/**
|
|
138
|
+
* The eight keys the engagement credential owns in the machine-level env file.
|
|
139
|
+
* These are the ONLY keys the reconcile adds or updates — see `reconcileEnvText`.
|
|
140
|
+
*
|
|
141
|
+
* `CONTROL_TELEMETRY_URL` JOINED THIS LIST AT T-10 (phase decision 13): without it
|
|
142
|
+
* no Connect engagement emitted telemetry, which is why `guard_results` was empty
|
|
143
|
+
* on prod. **It is written as `''` (never omitted) when the plane reports none** —
|
|
144
|
+
* `writeEngagementCredential` in `install.ts` keys this record on
|
|
145
|
+
* `EngagementEnvKey`, so a missing entry is a compile error rather than a silent
|
|
146
|
+
* gap, and `toCredential` there treats a blank value as absent. It is deliberately
|
|
147
|
+
* NOT in `CREDENTIAL_REQUIRED_KEYS` (`install.ts`) — that conjunction decides
|
|
148
|
+
* whether a checkout is treated as holding a credential at all, and putting an
|
|
149
|
+
* eighth, telemetry-only key into it would make every repository installed before
|
|
150
|
+
* this task (which has the other seven and not this one) read as credential-less
|
|
151
|
+
* on its next run and silently join a fresh one. See `DeviceCredential`'s docblock.
|
|
152
|
+
*/
|
|
153
|
+
export declare const ENGAGEMENT_ENV_KEYS: readonly ["HALFCYCLE_SERVICE_URL", "HALFCYCLE_MCP_URL", "HALFCYCLE_TOKEN", "HALFCYCLE_ENGAGEMENT_ID", "GUARD_SERVICE_URL", "GUARD_SERVICE_TOKEN", "GUARD_ENGAGEMENT_ID", "CONTROL_TELEMETRY_URL"];
|
|
154
|
+
export type EngagementEnvKey = (typeof ENGAGEMENT_ENV_KEYS)[number];
|
|
155
|
+
/**
|
|
156
|
+
* Parse `KEY=value` lines. **PARSES, NEVER SOURCES** — a credential store is not
|
|
157
|
+
* code, and `.` would execute whatever the file happens to contain. An optional
|
|
158
|
+
* leading `export ` is tolerated because the shells that source this file accept
|
|
159
|
+
* one, so a hand-edited store must read the same both ways, and one layer of
|
|
160
|
+
* matching quotes is stripped for the same reason (see the encoding note above).
|
|
161
|
+
*
|
|
162
|
+
* (The one exception is `guard-runner.sh`, which genuinely sources rather than
|
|
163
|
+
* parses: it must EXPORT the values into a child process, which is a thing only the
|
|
164
|
+
* shell can do. That file is generated by us, written `0600` outside every
|
|
165
|
+
* repository, and is Studio's adopted contract — a narrower surface than the
|
|
166
|
+
* developer-editable `.env.local` it replaces.)
|
|
167
|
+
*/
|
|
168
|
+
export declare function parseEnvText(raw: string): Record<string, string>;
|
|
169
|
+
/**
|
|
170
|
+
* Reconcile our keys into an existing env-file body BY KEY.
|
|
171
|
+
*
|
|
172
|
+
* Every pre-existing line survives byte-for-byte: a `KEY=value` line whose key is
|
|
173
|
+
* one of ours is updated IN PLACE (so key order is preserved and no duplicate is
|
|
174
|
+
* introduced); one of ours that is absent is appended under the header; and any
|
|
175
|
+
* other line — comments, blanks, a key some other Halfcycle writer put here — is
|
|
176
|
+
* copied through untouched.
|
|
177
|
+
*
|
|
178
|
+
* **`export KEY=` counts as that key.** `parseEnvText` and the installer's legacy
|
|
179
|
+
* migration both tolerate the prefix, and this function did not — so an existing
|
|
180
|
+
* `export HALFCYCLE_TOKEN=old` was left alone and a bare `HALFCYCLE_TOKEN=new`
|
|
181
|
+
* appended below it. Last-assignment-wins meant nothing observably broke, which is
|
|
182
|
+
* why it survived: a superseded token simply stayed on disk, in the one feature
|
|
183
|
+
* whose whole subject is where a credential is allowed to be. The prefix is
|
|
184
|
+
* preserved on the rewritten line rather than normalised away, because "every
|
|
185
|
+
* pre-existing line survives" is the contract and `export` is the developer's.
|
|
186
|
+
*
|
|
187
|
+
* Values are written `shq`-quoted — see the encoding note above; the file is
|
|
188
|
+
* SOURCED by `guard-runner.sh`, so an unquoted value carrying a space executes
|
|
189
|
+
* its own remainder.
|
|
190
|
+
*
|
|
191
|
+
* **The non-destructiveness is not vestigial after the move out of the tree.** This
|
|
192
|
+
* path is Studio's too: `packages/mac-workspace-provider` writes
|
|
193
|
+
* `CONTROL_TELEMETRY_TOKEN` and `HALFCYCLE_GUARD_EVAL_LOG_DIR` into the same file for
|
|
194
|
+
* an engagement it manages. A whole-file overwrite here would silently delete them on
|
|
195
|
+
* any machine where both ever touch one engagement. Reconciling by key costs nothing
|
|
196
|
+
* and removes that whole class.
|
|
197
|
+
*
|
|
198
|
+
* **`CONTROL_TELEMETRY_URL` is deliberately NOT named alongside those two** — this
|
|
199
|
+
* said `CONTROL_TELEMETRY_*` (the whole family) until a T-10 follow-up correction.
|
|
200
|
+
* `CONTROL_TELEMETRY_URL` is one of OURS since T-10 (`ENGAGEMENT_ENV_KEYS` above), so
|
|
201
|
+
* it is reconciled the same as `HALFCYCLE_TOKEN` — updated in place by a Connect
|
|
202
|
+
* install, not preserved as a foreign writer's key. A Studio-written value for it is
|
|
203
|
+
* legitimately overwritten on the next Connect install; only `CONTROL_TELEMETRY_TOKEN`
|
|
204
|
+
* is still genuinely foreign.
|
|
205
|
+
*/
|
|
206
|
+
export declare function reconcileEnvText(existing: string | null, values: Record<string, string>, keys?: readonly string[]): string;
|
|
207
|
+
/**
|
|
208
|
+
* Read this engagement's machine-level credential, or `null` when this machine
|
|
209
|
+
* holds none for it.
|
|
210
|
+
*
|
|
211
|
+
* A missing or unreadable file is `null`, never a throw: the commonest reason to
|
|
212
|
+
* have none is a fresh clone of an engaged repository, which is an ordinary state
|
|
213
|
+
* the caller reports in words rather than a fault.
|
|
214
|
+
*/
|
|
215
|
+
export declare function readEngagementEnv(engagementId: string, home?: string): Record<string, string> | null;
|
|
216
|
+
/** What a write did, in the installer's report vocabulary. */
|
|
217
|
+
export type EnvWriteOutcome = 'written' | 'skipped';
|
|
218
|
+
/**
|
|
219
|
+
* Write this engagement's credential to the machine-level store, owner-only.
|
|
220
|
+
*
|
|
221
|
+
* Idempotent: identical content is `skipped`, so a re-install writes nothing here
|
|
222
|
+
* and the installer's *"a second install writes nothing but the bundle pin"*
|
|
223
|
+
* invariant survives the move out of the tree.
|
|
224
|
+
*
|
|
225
|
+
* The mode is re-applied on the skipped path too — a store that already existed at
|
|
226
|
+
* the wrong mode is exactly the case `writeFileSync`'s `mode` option cannot fix,
|
|
227
|
+
* and a re-install is the natural moment to repair it.
|
|
228
|
+
*/
|
|
229
|
+
export declare function writeEngagementEnv(engagementId: string, values: Record<string, string>, home?: string): EnvWriteOutcome;
|
|
230
|
+
/**
|
|
231
|
+
* The shell function both generated consumers embed to find this file.
|
|
232
|
+
*
|
|
233
|
+
* **ONE authoring home for the resolution, two generated copies**, and that is the
|
|
234
|
+
* deliberate choice over a third installed script the other two would source: a
|
|
235
|
+
* shared script is one more file that can be missing, collide with a developer's
|
|
236
|
+
* own, or be lost by an export — and its absence would fail BOTH consumers at once,
|
|
237
|
+
* silently, which is the failure mode this whole feature is closing. Both copies
|
|
238
|
+
* come from this function, so there is nothing to keep in step by hand, and the
|
|
239
|
+
* path shape itself is composed from the same constants `engagementEnvPath()` uses.
|
|
240
|
+
*
|
|
241
|
+
* **Why `tr -d '\n'` before `sed`.** The pin is written pretty-printed
|
|
242
|
+
* (`JSON.stringify(pin, null, 2)`), so the key and its value are on one line today —
|
|
243
|
+
* but `sed` is line-oriented and a pin reformatted by any other tool would put them
|
|
244
|
+
* on two, at which point a line-wise match silently finds nothing and the developer
|
|
245
|
+
* gets "no credential" on a repository that has one. Flattening first makes the
|
|
246
|
+
* match independent of the file's line breaks. (This repo has paid twice for a
|
|
247
|
+
* pattern that wrapped across lines reading as absent.)
|
|
248
|
+
*
|
|
249
|
+
* **Why not `node`.** `mcp-headers.sh` is `sh` and not node precisely because a
|
|
250
|
+
* desktop-launched session's PATH typically has no version-manager `node`. Using it
|
|
251
|
+
* here would put that back.
|
|
252
|
+
*
|
|
253
|
+
* **IT SETS VARIABLES AND PRINTS NOTHING, and that is not a style choice.** The
|
|
254
|
+
* obvious form — print the path and let the caller take it with `$(…)` — puts the
|
|
255
|
+
* function in a SUBSHELL, so the `HALFCYCLE_ENV_PROBLEM` it sets on the failure
|
|
256
|
+
* path never reaches the caller. Measured: under `set -u` (which `mcp-headers.sh`
|
|
257
|
+
* runs with) the caller then dies on `HALFCYCLE_ENV_PROBLEM: unbound variable`
|
|
258
|
+
* instead of printing the reason it was carrying. So the function assigns
|
|
259
|
+
* `HALFCYCLE_ENV_FILE` / `HALFCYCLE_ENV_PROBLEM` / `HALFCYCLE_ENV_ENGAGEMENT` in
|
|
260
|
+
* the CURRENT shell and returns a status, and callers use `if halfcycle_env_file …`.
|
|
261
|
+
*
|
|
262
|
+
* `$HALFCYCLE_ENV_PROBLEM` names which step failed — `no-pin`, `no-id`, `no-home`,
|
|
263
|
+
* `no-credential` — so each caller says the true thing for its own surface instead
|
|
264
|
+
* of printing one sentence for four different situations.
|
|
265
|
+
*/
|
|
266
|
+
export declare function engagementResolutionShell(): string;
|
|
267
|
+
//# sourceMappingURL=engagement-credential.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engagement-credential.d.ts","sourceRoot":"","sources":["../src/engagement-credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAiBH;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7E;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,cAAc,8EAIjB,CAAC;AAEX;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,mMAOtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAmEpE;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,GAAE,SAAS,MAAM,EAAwB,GAC5C,MAAM,CA8BR;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAM/B;AAED,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,CAAC,EAAE,MAAM,GACZ,eAAe,CAkBjB;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CA0ClD"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project identity — mint or read `.halfcycle/project.json`.
|
|
3
|
+
*
|
|
4
|
+
* A repository is not a machine. Two developers cloning one repository, or one
|
|
5
|
+
* developer with five git worktrees, are ONE project. The identity is committed
|
|
6
|
+
* (it arrives with the code) and carries the repository's root commit hash and
|
|
7
|
+
* remote — the most stable identifier a git repository has, identical across
|
|
8
|
+
* every clone, fork and worktree. The remote travels with it so a fork carrying
|
|
9
|
+
* an inherited identity can be detected rather than silently joining someone
|
|
10
|
+
* else's board. The per-device credential is NOT here, and since T-09 it is not
|
|
11
|
+
* anywhere in this repository — it lives at machine level, outside every checkout
|
|
12
|
+
* (`engagement-credential.ts`). See docs/features/client-install-surface.md
|
|
13
|
+
* (operator decision (au), whose committed-identity-versus-device-credential split
|
|
14
|
+
* survives; only the credential's location changed).
|
|
15
|
+
*
|
|
16
|
+
* INV-001: this file imports only Node builtins.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The disambiguation note stamped into every `project.json` (W8-T-04, F-4).
|
|
20
|
+
*
|
|
21
|
+
* `projectId` sits in the same directory as `bundle.json`'s `engagementId`, and a
|
|
22
|
+
* cold session reading `.halfcycle/` top-down took `projectId` for the engagement
|
|
23
|
+
* id — it is a real uuid, so nothing about the VALUE flagged the mistake. The tool
|
|
24
|
+
* description already names the right file (`services/delivery/src/routes/mcp.ts`,
|
|
25
|
+
* #447), but that only reaches a session that calls the tool rather than reading
|
|
26
|
+
* the directory by hand, which is exactly what happened. This note is read
|
|
27
|
+
* whether or not the session ever calls a tool.
|
|
28
|
+
*
|
|
29
|
+
* It does NOT carry the engagement id itself — operator decision (au) keeps that
|
|
30
|
+
* out of the committed identity, and a comment in `create-engagement.ts` records
|
|
31
|
+
* that adding it here would re-open that decision. So the note can only say what
|
|
32
|
+
* this id is *not*, and where the right one lives; it cannot answer the question
|
|
33
|
+
* directly.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PROJECT_IDENTITY_NOTE: string;
|
|
36
|
+
/** The committed project identity: minted id plus the repo's root fingerprint. */
|
|
37
|
+
export interface ProjectIdentity {
|
|
38
|
+
/** A minted, stable project id (uuid). Assigned once, at first install. */
|
|
39
|
+
projectId: string;
|
|
40
|
+
/**
|
|
41
|
+
* The repository's root (first) commit hash, or null when the tree is not a
|
|
42
|
+
* git repository / has no commits yet. Identical across every clone, fork and
|
|
43
|
+
* worktree; survives renames and remote changes.
|
|
44
|
+
*/
|
|
45
|
+
rootCommit: string | null;
|
|
46
|
+
/** The `origin` remote URL, or null when there is no remote. */
|
|
47
|
+
remote: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* A fixed disambiguation string, always `PROJECT_IDENTITY_NOTE` — see there.
|
|
50
|
+
* Present so a session reading this file directly (not through a tool
|
|
51
|
+
* description) sees the warning too. Reconciled on every install: if an
|
|
52
|
+
* install written before this field existed re-installs, the note is
|
|
53
|
+
* backfilled (`writeProjectIdentity` rewrites iff the serialized bytes
|
|
54
|
+
* differ) — `projectId`/`rootCommit`/`remote` are never touched by that.
|
|
55
|
+
*/
|
|
56
|
+
note: string;
|
|
57
|
+
}
|
|
58
|
+
/** Read the repository's root commit hash (the first commit, reachable from HEAD). */
|
|
59
|
+
export declare function readRootCommit(targetRepoRoot: string): string | null;
|
|
60
|
+
/** Read the `origin` remote URL. */
|
|
61
|
+
export declare function readRemote(targetRepoRoot: string): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Return the existing committed identity if `.halfcycle/project.json` is present,
|
|
64
|
+
* otherwise mint a fresh one. Minting never overwrites the MINTED fields
|
|
65
|
+
* (`projectId`/`rootCommit`/`remote`): an existing identity's own values are
|
|
66
|
+
* returned verbatim so re-install and every clone share one project. `note` is
|
|
67
|
+
* the one exception — it is always stamped to the CURRENT `PROJECT_IDENTITY_NOTE`,
|
|
68
|
+
* on both the mint and the read path, so an install from before this field
|
|
69
|
+
* existed picks up the disambiguation text on its next install rather than
|
|
70
|
+
* staying silent forever.
|
|
71
|
+
*/
|
|
72
|
+
export declare function mintOrReadIdentity(targetRepoRoot: string): {
|
|
73
|
+
identity: ProjectIdentity;
|
|
74
|
+
minted: boolean;
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,QAGF,CAAC;AAEjC,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED,oCAAoC;AACpC,wBAAgB,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAmBzG"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @halfcycle/bundle — public API.
|
|
3
|
+
*
|
|
4
|
+
* The bundle is the Halfcycle Method as an installable Claude Code plugin.
|
|
5
|
+
* It exports the install/wiring generator and the drift-check function.
|
|
6
|
+
* Method text (agents, commands, skills, scaffolding) is shipped as static
|
|
7
|
+
* files in the package; this module exports only the programmatic API.
|
|
8
|
+
*
|
|
9
|
+
* INV-001: imports only from @halfcycle/core, @halfcycle/events, or Node builtins.
|
|
10
|
+
*/
|
|
11
|
+
export { install, checkDrift, readBundlePin, ZONE_B_DIR } from './install.js';
|
|
12
|
+
export type { InstallOptions, InstallResult, BundlePin, EngagementType, DeviceCredential } from './install.js';
|
|
13
|
+
export { mintOrReadIdentity, readRootCommit, readRemote } from './identity.js';
|
|
14
|
+
export type { ProjectIdentity } from './identity.js';
|
|
15
|
+
export { createEngagement, CreateEngagementRefused } from './create-engagement.js';
|
|
16
|
+
export type { CreatedEngagement } from './create-engagement.js';
|
|
17
|
+
export { mintBoardEnterCode } from './mint-board-code.js';
|
|
18
|
+
export type { MintedBoardCode } from './mint-board-code.js';
|
|
19
|
+
export { mergeSettings } from './merge-settings.js';
|
|
20
|
+
export type { ClaudeSettings, HookEntry, HookCommand } from './merge-settings.js';
|
|
21
|
+
export { runBootstrapScan, scanLayers, mergeWithRecords, HALFCYCLE_STATE_FORMAT } from './scan.js';
|
|
22
|
+
export type { HalfcycleStateFile, ScannedLayerEntry, ScannedLayerValue } from './scan.js';
|
|
23
|
+
export * from './setup/index.js';
|
|
24
|
+
export * from './build-record/index.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC9E,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC/G,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAyBhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnG,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAI1F,cAAc,kBAAkB,CAAC;AAKjC,cAAc,yBAAyB,CAAC"}
|