halfcycle 0.3.12 → 0.3.14
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/dist/banner-facts.d.ts +77 -0
- package/dist/banner-facts.d.ts.map +1 -0
- package/dist/banner.d.ts +239 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/bin.js +517 -76
- package/dist/bin.js.map +3 -3
- package/dist/confirm-identity.d.ts +30 -4
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/control-origin.d.ts +6 -1
- package/dist/control-origin.d.ts.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +2 -2
- package/dist/install.d.ts +9 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/own-engagement.d.ts +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/bin.js
|
|
4
|
-
import { execFileSync as
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { join as
|
|
4
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
6
|
+
import { join as join10 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
9
9
|
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -704,6 +704,13 @@ function readRootCommit(targetRepoRoot) {
|
|
|
704
704
|
function readRemote(targetRepoRoot) {
|
|
705
705
|
return git(targetRepoRoot, ["remote", "get-url", "origin"]);
|
|
706
706
|
}
|
|
707
|
+
function readCommitCount(targetRepoRoot) {
|
|
708
|
+
const out = git(targetRepoRoot, ["rev-list", "--count", "HEAD"]);
|
|
709
|
+
if (out === null)
|
|
710
|
+
return null;
|
|
711
|
+
const count = Number.parseInt(out, 10);
|
|
712
|
+
return Number.isFinite(count) ? count : null;
|
|
713
|
+
}
|
|
707
714
|
function mintOrReadIdentity(targetRepoRoot) {
|
|
708
715
|
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
709
716
|
if (existsSync(path)) {
|
|
@@ -1618,7 +1625,7 @@ function resolveControlOrigin(env = process.env) {
|
|
|
1618
1625
|
return { origin: DEFAULT_CONTROL_ORIGIN, source: "default" };
|
|
1619
1626
|
}
|
|
1620
1627
|
function controlOriginNote(resolved) {
|
|
1621
|
-
return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the
|
|
1628
|
+
return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the published one)`;
|
|
1622
1629
|
}
|
|
1623
1630
|
|
|
1624
1631
|
// dist/device-signin.js
|
|
@@ -1705,14 +1712,14 @@ async function bindLoopback(env = process.env) {
|
|
|
1705
1712
|
else
|
|
1706
1713
|
pending = callback;
|
|
1707
1714
|
}));
|
|
1708
|
-
const bindProblem = await new Promise((
|
|
1715
|
+
const bindProblem = await new Promise((resolve4) => {
|
|
1709
1716
|
const onError = (err) => {
|
|
1710
|
-
|
|
1717
|
+
resolve4(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
|
|
1711
1718
|
};
|
|
1712
1719
|
server.once("error", onError);
|
|
1713
1720
|
server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
|
|
1714
1721
|
server.removeListener("error", onError);
|
|
1715
|
-
|
|
1722
|
+
resolve4(null);
|
|
1716
1723
|
});
|
|
1717
1724
|
});
|
|
1718
1725
|
if (bindProblem !== null) {
|
|
@@ -1728,9 +1735,9 @@ async function bindLoopback(env = process.env) {
|
|
|
1728
1735
|
bound: true,
|
|
1729
1736
|
target: { port: address.port, state },
|
|
1730
1737
|
boundAddress: address.address,
|
|
1731
|
-
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((
|
|
1738
|
+
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve4) => {
|
|
1732
1739
|
if (pending !== null) {
|
|
1733
|
-
|
|
1740
|
+
resolve4(pending);
|
|
1734
1741
|
return;
|
|
1735
1742
|
}
|
|
1736
1743
|
const startedAt = Date.now();
|
|
@@ -1740,7 +1747,7 @@ async function bindLoopback(env = process.env) {
|
|
|
1740
1747
|
clearInterval(ticker);
|
|
1741
1748
|
clearTimeout(timer);
|
|
1742
1749
|
deliver = null;
|
|
1743
|
-
|
|
1750
|
+
resolve4(value);
|
|
1744
1751
|
};
|
|
1745
1752
|
const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
|
|
1746
1753
|
deliver = finish;
|
|
@@ -1749,9 +1756,9 @@ async function bindLoopback(env = process.env) {
|
|
|
1749
1756
|
};
|
|
1750
1757
|
}
|
|
1751
1758
|
function closeServer(server) {
|
|
1752
|
-
return new Promise((
|
|
1759
|
+
return new Promise((resolve4) => {
|
|
1753
1760
|
server.closeAllConnections();
|
|
1754
|
-
server.close(() =>
|
|
1761
|
+
server.close(() => resolve4());
|
|
1755
1762
|
});
|
|
1756
1763
|
}
|
|
1757
1764
|
|
|
@@ -1765,7 +1772,7 @@ var SignInRefused = class extends Error {
|
|
|
1765
1772
|
}
|
|
1766
1773
|
};
|
|
1767
1774
|
var SIGN_IN_TIMED_OUT = "timed-out";
|
|
1768
|
-
var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle
|
|
1775
|
+
var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle service stopped answering while this terminal was waiting for the browser. Nothing was created. Check your connection and run the command again.";
|
|
1769
1776
|
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.";
|
|
1770
1777
|
var DEADLINE_SLACK_INTERVALS = 1;
|
|
1771
1778
|
var STILL_WAITING_MS = 15e3;
|
|
@@ -1773,7 +1780,7 @@ function defaultWrite(text) {
|
|
|
1773
1780
|
process.stdout.write(text);
|
|
1774
1781
|
}
|
|
1775
1782
|
function defaultSleep(ms) {
|
|
1776
|
-
return new Promise((
|
|
1783
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1777
1784
|
}
|
|
1778
1785
|
function browserOpenCommand(os, url) {
|
|
1779
1786
|
if (os === "darwin")
|
|
@@ -1799,12 +1806,12 @@ async function openBrowser(url, env = process.env) {
|
|
|
1799
1806
|
return { opened: false, reason: "the sign-in address is not an http(s) URL this CLI will open" };
|
|
1800
1807
|
}
|
|
1801
1808
|
const { command, args: args2 } = browserOpenCommand(platform3(), url);
|
|
1802
|
-
return new Promise((
|
|
1809
|
+
return new Promise((resolve4) => {
|
|
1803
1810
|
let child;
|
|
1804
1811
|
try {
|
|
1805
1812
|
child = spawn(command, args2, { stdio: "ignore", env });
|
|
1806
1813
|
} catch (err) {
|
|
1807
|
-
|
|
1814
|
+
resolve4({
|
|
1808
1815
|
opened: false,
|
|
1809
1816
|
reason: `${command} could not be run (${err instanceof Error ? err.message : String(err)})`
|
|
1810
1817
|
});
|
|
@@ -1816,7 +1823,7 @@ async function openBrowser(url, env = process.env) {
|
|
|
1816
1823
|
return;
|
|
1817
1824
|
settled = true;
|
|
1818
1825
|
clearTimeout(timer);
|
|
1819
|
-
|
|
1826
|
+
resolve4(attempt);
|
|
1820
1827
|
};
|
|
1821
1828
|
const timer = setTimeout(() => {
|
|
1822
1829
|
child.unref();
|
|
@@ -1852,7 +1859,7 @@ async function startDeviceAuth(serviceUrl) {
|
|
|
1852
1859
|
body: "{}"
|
|
1853
1860
|
});
|
|
1854
1861
|
} catch (err) {
|
|
1855
|
-
throw new SignInRefused(SIGN_IN_TIMED_OUT, `Could not reach the Halfcycle
|
|
1862
|
+
throw new SignInRefused(SIGN_IN_TIMED_OUT, `Could not reach the Halfcycle service at ${url} to start a sign-in: ${err instanceof Error ? err.message : String(err)}. Nothing was created.`);
|
|
1856
1863
|
}
|
|
1857
1864
|
const body = await res.json().catch(() => null);
|
|
1858
1865
|
if (res.status !== 201) {
|
|
@@ -1861,7 +1868,7 @@ async function startDeviceAuth(serviceUrl) {
|
|
|
1861
1868
|
}
|
|
1862
1869
|
const parsed = deviceAuthStartResponseSchema.safeParse(body);
|
|
1863
1870
|
if (!parsed.success) {
|
|
1864
|
-
throw new SignInRefused(SIGN_IN_TIMED_OUT, `The Halfcycle
|
|
1871
|
+
throw new SignInRefused(SIGN_IN_TIMED_OUT, `The Halfcycle service at ${url} answered a sign-in start this client cannot read. Either that service is older than this installer, or the address is not a Halfcycle service. Nothing was created.`);
|
|
1865
1872
|
}
|
|
1866
1873
|
return parsed.data;
|
|
1867
1874
|
}
|
|
@@ -1936,7 +1943,7 @@ async function collectDecidedCredential(serviceUrl, started, deps) {
|
|
|
1936
1943
|
});
|
|
1937
1944
|
body = await res.json().catch(() => null);
|
|
1938
1945
|
} catch (err) {
|
|
1939
|
-
lastProblem = `The browser finished signing you in, but this terminal could not reach the Halfcycle
|
|
1946
|
+
lastProblem = `The browser finished signing you in, but this terminal could not reach the Halfcycle service to collect the credential: ${err instanceof Error ? err.message : String(err)}. Run the command again.`;
|
|
1940
1947
|
continue;
|
|
1941
1948
|
}
|
|
1942
1949
|
const parsed = devicePollResponseSchema.safeParse(body);
|
|
@@ -2125,7 +2132,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2125
2132
|
body: JSON.stringify(requestBody)
|
|
2126
2133
|
});
|
|
2127
2134
|
} catch (err) {
|
|
2128
|
-
throw new Error(`[bundle install] Could not reach the Halfcycle
|
|
2135
|
+
throw new Error(`[bundle install] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2129
2136
|
}
|
|
2130
2137
|
if (!res.ok) {
|
|
2131
2138
|
const detail = await res.text().catch(() => "");
|
|
@@ -2133,7 +2140,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2133
2140
|
if (refusal) {
|
|
2134
2141
|
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
|
|
2135
2142
|
}
|
|
2136
|
-
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the
|
|
2143
|
+
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
2137
2144
|
throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2138
2145
|
}
|
|
2139
2146
|
const body = await res.json().catch(() => null);
|
|
@@ -2145,14 +2152,14 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2145
2152
|
["sessionToken", body.sessionToken]
|
|
2146
2153
|
]) {
|
|
2147
2154
|
if (value.trim() === "") {
|
|
2148
|
-
throw new Error(`[bundle install] The Halfcycle
|
|
2155
|
+
throw new Error(`[bundle install] The Halfcycle service at ${url} answered with a blank ${field} (the field is present but empty). An install cannot proceed on it: it would be written into this engagement's credential store, read back as a configured value, and every guard evaluation would fail against it for the life of the engagement. ${shape.nothingHappened} and nothing was written. ` + CONTROL_ORIGIN_HINT);
|
|
2149
2156
|
}
|
|
2150
2157
|
}
|
|
2151
2158
|
if (typeof body.mcpUrl !== "string" || body.mcpUrl.trim() === "") {
|
|
2152
|
-
throw new Error(`[bundle install] The Halfcycle
|
|
2159
|
+
throw new Error(`[bundle install] The Halfcycle service at ${url} created an engagement but did not say where its MCP server lives (no mcpUrl on the response). Without that address the install would write an MCP registration pointing nowhere, so nothing was written. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
|
|
2153
2160
|
}
|
|
2154
2161
|
if (typeof body.guardUrl !== "string" || body.guardUrl.trim() === "") {
|
|
2155
|
-
throw new Error(`[bundle install] The Halfcycle
|
|
2162
|
+
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.`);
|
|
2156
2163
|
}
|
|
2157
2164
|
const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
|
|
2158
2165
|
return {
|
|
@@ -2211,12 +2218,284 @@ function refusedCredentialRemedy(source) {
|
|
|
2211
2218
|
case "stored":
|
|
2212
2219
|
return "That was this machine's saved sign-in and it has been discarded. Run the command again to sign in through your browser.";
|
|
2213
2220
|
case "browser-handoff":
|
|
2214
|
-
return "That credential was issued by this sign-in moments ago, so signing in again would produce the same result. This is a problem on the Halfcycle
|
|
2221
|
+
return "That credential was issued by this sign-in moments ago, so signing in again would produce the same result. This is a problem on the Halfcycle service rather than with your account.";
|
|
2215
2222
|
}
|
|
2216
2223
|
}
|
|
2217
2224
|
|
|
2218
2225
|
// dist/confirm-identity.js
|
|
2219
2226
|
import { createInterface } from "node:readline/promises";
|
|
2227
|
+
|
|
2228
|
+
// dist/banner.js
|
|
2229
|
+
var MARK_FULL = {
|
|
2230
|
+
width: 32,
|
|
2231
|
+
discRows: 7,
|
|
2232
|
+
lines: [
|
|
2233
|
+
" :+oshhhhso+:",
|
|
2234
|
+
" :sdMMMMMMMMMMMMds:",
|
|
2235
|
+
" :hMMMMMMMMMMMMMMMMMMh:",
|
|
2236
|
+
" oMMMMMMMMMMMMMMMMMMMMMMo",
|
|
2237
|
+
" oMMMMMMMMMMMMMMMMMMMMMMMMo",
|
|
2238
|
+
" NMMMMMMMMMMMMMMMMMMMMMMMMN",
|
|
2239
|
+
" :ssssssssssssssssssssssssss:",
|
|
2240
|
+
" +dddddddddddddddddddddddddd+",
|
|
2241
|
+
" Nd dN",
|
|
2242
|
+
" oMy yMo",
|
|
2243
|
+
" oMh: :hMo",
|
|
2244
|
+
" :hMy- -yMh:",
|
|
2245
|
+
" :sdNys++--++syNds:",
|
|
2246
|
+
" :+oshhhhso+:"
|
|
2247
|
+
]
|
|
2248
|
+
};
|
|
2249
|
+
var MARK_NARROW = {
|
|
2250
|
+
width: 24,
|
|
2251
|
+
discRows: 5,
|
|
2252
|
+
lines: [
|
|
2253
|
+
" -ohdMMMMdho-",
|
|
2254
|
+
" +dMMMMMMMMMMMMd+",
|
|
2255
|
+
" yMMMMMMMMMMMMMMMMy",
|
|
2256
|
+
" sMMMMMMMMMMMMMMMMMMs",
|
|
2257
|
+
" yhhhhhhhhhhhhhhhhhhy",
|
|
2258
|
+
" NdhhhhhhhhhhhhhhhhdN",
|
|
2259
|
+
" sd ds",
|
|
2260
|
+
" yd: :dy",
|
|
2261
|
+
" +dy+ +yd+",
|
|
2262
|
+
" -ohhhhhhhho-"
|
|
2263
|
+
]
|
|
2264
|
+
};
|
|
2265
|
+
var UNICODE_GLYPHS = {
|
|
2266
|
+
rule: "\u2500",
|
|
2267
|
+
swatch: "\u2584",
|
|
2268
|
+
dot: "\xB7",
|
|
2269
|
+
dash: "\u2014"
|
|
2270
|
+
};
|
|
2271
|
+
var ASCII_GLYPHS = {
|
|
2272
|
+
rule: "-",
|
|
2273
|
+
swatch: "#",
|
|
2274
|
+
dot: "|",
|
|
2275
|
+
dash: "-"
|
|
2276
|
+
};
|
|
2277
|
+
function toAscii(text) {
|
|
2278
|
+
let out = text;
|
|
2279
|
+
for (const key of Object.keys(UNICODE_GLYPHS)) {
|
|
2280
|
+
out = out.split(UNICODE_GLYPHS[key]).join(ASCII_GLYPHS[key]);
|
|
2281
|
+
}
|
|
2282
|
+
return out.replace(/[^ -~]/g, "?");
|
|
2283
|
+
}
|
|
2284
|
+
var INK = {
|
|
2285
|
+
/** `--text` — the terminal's own foreground, never one we picked. */
|
|
2286
|
+
plain: "",
|
|
2287
|
+
/** `--muted` — dimmed default ink, so it is grey against whatever the background is. */
|
|
2288
|
+
dim: "2",
|
|
2289
|
+
/** `--faint` / `--faded` — bright black, the one ANSI grey that is a colour. */
|
|
2290
|
+
faint: "90",
|
|
2291
|
+
/** Not a colour: emphasis, used with whatever ink is already in force. */
|
|
2292
|
+
bold: "1",
|
|
2293
|
+
/** `--signal` — yellow, the nearest ANSI seat for the brand's gold. */
|
|
2294
|
+
key: "33",
|
|
2295
|
+
/** `--signal`, brighter: the one value on a screen that must be found first. */
|
|
2296
|
+
keyBold: "33;1",
|
|
2297
|
+
/** `--good` — the console's affirmative. */
|
|
2298
|
+
good: "32",
|
|
2299
|
+
/** `--bad` — the console's negative. */
|
|
2300
|
+
bad: "31"
|
|
2301
|
+
};
|
|
2302
|
+
var SWATCH_SGR = [
|
|
2303
|
+
INK.dim,
|
|
2304
|
+
INK.faint,
|
|
2305
|
+
INK.plain,
|
|
2306
|
+
INK.key,
|
|
2307
|
+
INK.keyBold,
|
|
2308
|
+
INK.good,
|
|
2309
|
+
INK.bad
|
|
2310
|
+
];
|
|
2311
|
+
var ESC = "\x1B";
|
|
2312
|
+
var RESET = `${ESC}[0m`;
|
|
2313
|
+
function paint(segments, colour) {
|
|
2314
|
+
return segments.map((s) => colour && s.sgr !== "" ? `${ESC}[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
2315
|
+
}
|
|
2316
|
+
function ink(name, text, colour) {
|
|
2317
|
+
const sgr = INK[name];
|
|
2318
|
+
return colour && sgr !== "" ? `${ESC}[${sgr}m${text}${RESET}` : text;
|
|
2319
|
+
}
|
|
2320
|
+
var GAP = 4;
|
|
2321
|
+
var FULL_LAYOUT = { art: MARK_FULL, label: 13, rule: 28, palette: true, minColumns: 76 };
|
|
2322
|
+
var NARROW_LAYOUT = { art: MARK_NARROW, label: 10, rule: 20, palette: false, minColumns: 48 };
|
|
2323
|
+
var DEFAULT_COLUMNS = 80;
|
|
2324
|
+
function usableColumns(environment) {
|
|
2325
|
+
const columns = environment.columns;
|
|
2326
|
+
return columns !== void 0 && columns > 0 ? columns : DEFAULT_COLUMNS;
|
|
2327
|
+
}
|
|
2328
|
+
function isSet(value) {
|
|
2329
|
+
return value !== void 0 && value !== "";
|
|
2330
|
+
}
|
|
2331
|
+
function plainTextRequested(env) {
|
|
2332
|
+
return isSet(env["NO_COLOR"]) || env["TERM"] === "dumb";
|
|
2333
|
+
}
|
|
2334
|
+
function decideCut(environment = {}) {
|
|
2335
|
+
const env = environment.env ?? {};
|
|
2336
|
+
if (environment.quiet === true)
|
|
2337
|
+
return "none";
|
|
2338
|
+
if (environment.isTTY !== true)
|
|
2339
|
+
return "line";
|
|
2340
|
+
if (plainTextRequested(env))
|
|
2341
|
+
return "line";
|
|
2342
|
+
const columns = usableColumns(environment);
|
|
2343
|
+
if (columns >= FULL_LAYOUT.minColumns)
|
|
2344
|
+
return "full";
|
|
2345
|
+
if (columns >= NARROW_LAYOUT.minColumns)
|
|
2346
|
+
return "narrow";
|
|
2347
|
+
return "line";
|
|
2348
|
+
}
|
|
2349
|
+
function decideColour(environment = {}) {
|
|
2350
|
+
const env = environment.env ?? {};
|
|
2351
|
+
const force = env["FORCE_COLOR"];
|
|
2352
|
+
if (isSet(force))
|
|
2353
|
+
return force !== "0";
|
|
2354
|
+
if (plainTextRequested(env))
|
|
2355
|
+
return false;
|
|
2356
|
+
return environment.isTTY === true;
|
|
2357
|
+
}
|
|
2358
|
+
function decideUnicode(environment = {}) {
|
|
2359
|
+
const env = environment.env ?? {};
|
|
2360
|
+
if ((environment.platform ?? "") !== "win32") {
|
|
2361
|
+
const locale = env["LC_ALL"] ?? env["LC_CTYPE"] ?? env["LANG"] ?? "";
|
|
2362
|
+
return locale === "" || /utf-?8/i.test(locale);
|
|
2363
|
+
}
|
|
2364
|
+
return isSet(env["WT_SESSION"]) || isSet(env["TERMINUS_SUBLIME"]) || env["ConEmuTask"] === "{cmd::Cmder}" || env["TERM_PROGRAM"] === "vscode";
|
|
2365
|
+
}
|
|
2366
|
+
function padEnd(text, width) {
|
|
2367
|
+
return text + " ".repeat(Math.max(0, width - text.length));
|
|
2368
|
+
}
|
|
2369
|
+
function clip(text, width) {
|
|
2370
|
+
if (width < 8 || text.length <= width)
|
|
2371
|
+
return text;
|
|
2372
|
+
return `${text.slice(0, width - 3)}...`;
|
|
2373
|
+
}
|
|
2374
|
+
function paletteRow(swatch) {
|
|
2375
|
+
const row = [];
|
|
2376
|
+
SWATCH_SGR.forEach((sgr, i) => {
|
|
2377
|
+
if (i > 0)
|
|
2378
|
+
row.push({ text: " ", sgr: INK.plain });
|
|
2379
|
+
row.push({ text: swatch.repeat(2), sgr });
|
|
2380
|
+
});
|
|
2381
|
+
return row;
|
|
2382
|
+
}
|
|
2383
|
+
function factColumn(content, layout, glyphs, text, valueWidth) {
|
|
2384
|
+
const rows = [];
|
|
2385
|
+
const head = text(content.head);
|
|
2386
|
+
const at = head.indexOf("@");
|
|
2387
|
+
rows.push(at < 0 ? [{ text: head, sgr: INK.keyBold }] : [
|
|
2388
|
+
{ text: head.slice(0, at), sgr: INK.keyBold },
|
|
2389
|
+
{ text: "@", sgr: INK.dim },
|
|
2390
|
+
{ text: head.slice(at + 1), sgr: INK.bold }
|
|
2391
|
+
]);
|
|
2392
|
+
rows.push([{ text: glyphs.rule.repeat(layout.rule), sgr: INK.dim }]);
|
|
2393
|
+
const narrow = layout.art === MARK_NARROW;
|
|
2394
|
+
for (const fact2 of content.facts) {
|
|
2395
|
+
const raw = narrow ? fact2.narrow : fact2.value;
|
|
2396
|
+
if (raw === void 0)
|
|
2397
|
+
continue;
|
|
2398
|
+
rows.push([
|
|
2399
|
+
{ text: padEnd(`${text(fact2.label)}:`, layout.label), sgr: INK.key },
|
|
2400
|
+
{ text: clip(text(raw), valueWidth), sgr: INK.plain }
|
|
2401
|
+
]);
|
|
2402
|
+
}
|
|
2403
|
+
if (layout.palette) {
|
|
2404
|
+
rows.push([]);
|
|
2405
|
+
rows.push(paletteRow(glyphs.swatch));
|
|
2406
|
+
}
|
|
2407
|
+
return rows;
|
|
2408
|
+
}
|
|
2409
|
+
var MARGIN = 2;
|
|
2410
|
+
function lastSentenceStart(line) {
|
|
2411
|
+
const at = line.lastIndexOf(". ");
|
|
2412
|
+
return at < 0 ? 0 : at + 2;
|
|
2413
|
+
}
|
|
2414
|
+
function taglineRow(line, first, last) {
|
|
2415
|
+
const indent = { text: " ", sgr: INK.plain };
|
|
2416
|
+
if (first)
|
|
2417
|
+
return [indent, { text: line, sgr: INK.keyBold }];
|
|
2418
|
+
if (!last)
|
|
2419
|
+
return [indent, { text: line, sgr: INK.dim }];
|
|
2420
|
+
const at = lastSentenceStart(line);
|
|
2421
|
+
if (at === 0)
|
|
2422
|
+
return [indent, { text: line, sgr: INK.key }];
|
|
2423
|
+
return [
|
|
2424
|
+
indent,
|
|
2425
|
+
{ text: line.slice(0, at), sgr: INK.dim },
|
|
2426
|
+
{ text: line.slice(at), sgr: INK.key }
|
|
2427
|
+
];
|
|
2428
|
+
}
|
|
2429
|
+
function renderBlock(content, layout, environment) {
|
|
2430
|
+
const unicode = decideUnicode(environment);
|
|
2431
|
+
const colour = decideColour(environment);
|
|
2432
|
+
const glyphs = unicode ? UNICODE_GLYPHS : ASCII_GLYPHS;
|
|
2433
|
+
const text = (raw) => unicode ? raw : toAscii(raw);
|
|
2434
|
+
const art = layout.art;
|
|
2435
|
+
const valueWidth = usableColumns(environment) - (art.width + GAP + layout.label);
|
|
2436
|
+
const right = factColumn(content, layout, glyphs, text, valueWidth);
|
|
2437
|
+
const top = Math.max(0, Math.floor((art.lines.length - right.length) / 2));
|
|
2438
|
+
const height = Math.max(art.lines.length, right.length + top);
|
|
2439
|
+
const lines = Array.from({ length: MARGIN }, () => []);
|
|
2440
|
+
for (let i = 0; i < height; i++) {
|
|
2441
|
+
const artLine = art.lines[i] ?? "";
|
|
2442
|
+
const segments = [];
|
|
2443
|
+
if (artLine.length > 0) {
|
|
2444
|
+
segments.push({ text: artLine, sgr: i < art.discRows ? INK.plain : INK.dim });
|
|
2445
|
+
}
|
|
2446
|
+
const rightRow = right[i - top];
|
|
2447
|
+
if (rightRow !== void 0 && rightRow.length > 0) {
|
|
2448
|
+
segments.push({ text: " ".repeat(Math.max(0, art.width + GAP - artLine.length)), sgr: INK.plain });
|
|
2449
|
+
segments.push(...rightRow);
|
|
2450
|
+
}
|
|
2451
|
+
lines.push(segments);
|
|
2452
|
+
}
|
|
2453
|
+
const tagline = layout.art === MARK_NARROW ? content.tagNarrow ?? content.tag : content.tag;
|
|
2454
|
+
lines.push([]);
|
|
2455
|
+
tagline.forEach((line, i) => {
|
|
2456
|
+
lines.push(taglineRow(text(line), i === 0, i === tagline.length - 1));
|
|
2457
|
+
});
|
|
2458
|
+
for (let i = 0; i < MARGIN; i++)
|
|
2459
|
+
lines.push([]);
|
|
2460
|
+
return lines.map((segments) => paint(segments, colour)).join("\n");
|
|
2461
|
+
}
|
|
2462
|
+
function renderLine(content, environment) {
|
|
2463
|
+
const unicode = decideUnicode(environment);
|
|
2464
|
+
const dot = unicode ? UNICODE_GLYPHS.dot : ASCII_GLYPHS.dot;
|
|
2465
|
+
const named = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
|
|
2466
|
+
const raw = `${named} ${dot} ${content.url}`;
|
|
2467
|
+
return unicode ? raw : toAscii(raw);
|
|
2468
|
+
}
|
|
2469
|
+
function renderBanner(content, environment = {}) {
|
|
2470
|
+
switch (decideCut(environment)) {
|
|
2471
|
+
case "none":
|
|
2472
|
+
return "";
|
|
2473
|
+
case "line":
|
|
2474
|
+
return renderLine(content, environment);
|
|
2475
|
+
case "narrow":
|
|
2476
|
+
return renderBlock(content, NARROW_LAYOUT, environment);
|
|
2477
|
+
case "full":
|
|
2478
|
+
return renderBlock(content, FULL_LAYOUT, environment);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function printBanner(sink, content, environment = {}) {
|
|
2482
|
+
const text = renderBanner(content, environment);
|
|
2483
|
+
if (text === "")
|
|
2484
|
+
return;
|
|
2485
|
+
sink.write(`${text}
|
|
2486
|
+
`);
|
|
2487
|
+
}
|
|
2488
|
+
function currentEnvironment(quiet2) {
|
|
2489
|
+
return {
|
|
2490
|
+
columns: process.stdout.columns,
|
|
2491
|
+
isTTY: process.stdout.isTTY === true,
|
|
2492
|
+
quiet: quiet2,
|
|
2493
|
+
env: process.env,
|
|
2494
|
+
platform: process.platform
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// dist/confirm-identity.js
|
|
2220
2499
|
var IDENTITY_TIMEOUT_MS = 5e3;
|
|
2221
2500
|
var IDENTITY_DECLINED = "identity-declined";
|
|
2222
2501
|
async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
@@ -2238,14 +2517,14 @@ async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
|
|
|
2238
2517
|
clearTimeout(timer);
|
|
2239
2518
|
}
|
|
2240
2519
|
}
|
|
2241
|
-
function accountLabel(identity, storedAccountId) {
|
|
2520
|
+
function accountLabel(identity, storedAccountId, verbose2 = false) {
|
|
2242
2521
|
const accountId = identity?.accountId ?? storedAccountId;
|
|
2243
2522
|
if (identity?.email !== void 0 && identity.email.trim() !== "") {
|
|
2244
|
-
return accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
|
|
2523
|
+
return verbose2 && accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
|
|
2245
2524
|
}
|
|
2246
2525
|
if (accountId !== void 0)
|
|
2247
2526
|
return `account ${accountId} (no email on record for it)`;
|
|
2248
|
-
return "the account this credential belongs to, which this
|
|
2527
|
+
return "the account this credential belongs to, which this service did not name";
|
|
2249
2528
|
}
|
|
2250
2529
|
function onlyDate(iso) {
|
|
2251
2530
|
if (iso === void 0)
|
|
@@ -2256,10 +2535,10 @@ function onlyDate(iso) {
|
|
|
2256
2535
|
function actClause(act) {
|
|
2257
2536
|
return act.kind === "create" ? `create a NEW project for that account, and install into ${act.targetRepo}` : `join the project this repository is pinned to (${act.engagementId}) as that account, and install into ${act.targetRepo}`;
|
|
2258
2537
|
}
|
|
2259
|
-
async function defaultAsk(
|
|
2538
|
+
async function defaultAsk(question2) {
|
|
2260
2539
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2261
2540
|
try {
|
|
2262
|
-
return await rl.question(
|
|
2541
|
+
return await rl.question(question2);
|
|
2263
2542
|
} finally {
|
|
2264
2543
|
rl.close();
|
|
2265
2544
|
process.stdin.pause();
|
|
@@ -2277,7 +2556,10 @@ function actingAs(credential, identity) {
|
|
|
2277
2556
|
const accountId = identity?.accountId ?? credential.accountId;
|
|
2278
2557
|
return accountId === void 0 ? credential : { ...credential, accountId };
|
|
2279
2558
|
}
|
|
2280
|
-
function
|
|
2559
|
+
function question(colour) {
|
|
2560
|
+
return `[halfcycle] Continue as that account? ${ink("good", "[y]", colour)} yes ${ink("key", "[s]", colour)} sign in as someone else ${ink("faint", "[anything else]", colour)} stop: `;
|
|
2561
|
+
}
|
|
2562
|
+
function armScript(credential, act, label, plainLabel, home) {
|
|
2281
2563
|
switch (credential.source) {
|
|
2282
2564
|
case "browser-handoff":
|
|
2283
2565
|
return {
|
|
@@ -2303,7 +2585,7 @@ function armScript(credential, act, label, home) {
|
|
|
2303
2585
|
noTerminalNote: "",
|
|
2304
2586
|
switchOpening: `[halfcycle] Signing you in through your browser \u2014 this install will use that account instead.
|
|
2305
2587
|
`,
|
|
2306
|
-
stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. HALFCYCLE_TOKEN in this environment is the credential for ${
|
|
2588
|
+
stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. HALFCYCLE_TOKEN in this environment is the credential for ${plainLabel}, and it takes precedence over any saved sign-in \u2014 so unset it, or set it to your own account's credential, before running this again.`,
|
|
2307
2589
|
// The switch is real for THIS run (the new credential is what gets used) and it
|
|
2308
2590
|
// does not persist: `obtainCredential` reads the variable first, so the next run
|
|
2309
2591
|
// is the same variable again. Said here rather than discovered tomorrow.
|
|
@@ -2329,7 +2611,7 @@ function armScript(credential, act, label, home) {
|
|
|
2329
2611
|
`,
|
|
2330
2612
|
switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
|
|
2331
2613
|
`,
|
|
2332
|
-
stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. This machine's saved sign-in is for ${
|
|
2614
|
+
stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. This machine's saved sign-in is for ${plainLabel}. Run this again and answer "s" to sign in as a different account, or remove ${accountStorePath(home)} to be asked for a sign-in next time.`,
|
|
2333
2615
|
afterSwitch: ""
|
|
2334
2616
|
};
|
|
2335
2617
|
}
|
|
@@ -2337,10 +2619,12 @@ function armScript(credential, act, label, home) {
|
|
|
2337
2619
|
}
|
|
2338
2620
|
async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
2339
2621
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
2622
|
+
const colour = deps.colour ?? decideColour({ isTTY: process.stdout.isTTY === true, env: deps.env ?? process.env });
|
|
2340
2623
|
const identity = await describeAccount(serviceUrl, credential.credential, deps.fetchImpl);
|
|
2341
|
-
const
|
|
2624
|
+
const plainLabel = accountLabel(identity, credential.accountId, deps.verbose === true);
|
|
2625
|
+
const label = ink("keyBold", plainLabel, colour);
|
|
2342
2626
|
const acting = actingAs(credential, identity);
|
|
2343
|
-
const script = armScript(credential, act, label, deps.home);
|
|
2627
|
+
const script = armScript(credential, act, label, plainLabel, deps.home);
|
|
2344
2628
|
write(script.preamble);
|
|
2345
2629
|
if (!script.asks)
|
|
2346
2630
|
return acting;
|
|
@@ -2356,14 +2640,15 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
|
2356
2640
|
return acting;
|
|
2357
2641
|
}
|
|
2358
2642
|
const ask = deps.ask ?? defaultAsk;
|
|
2359
|
-
|
|
2643
|
+
write("\n");
|
|
2644
|
+
const decision = readDecision(await ask(question(colour)));
|
|
2360
2645
|
if (decision === "proceed")
|
|
2361
2646
|
return acting;
|
|
2362
2647
|
if (decision === "switch-account") {
|
|
2363
2648
|
write(script.switchOpening);
|
|
2364
2649
|
const replacement = await switchAccount(serviceUrl, deps);
|
|
2365
2650
|
const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
|
|
2366
|
-
write(`[halfcycle] Signed in as ${accountLabel(replacementIdentity, replacement.accountId)}. This install will ${actClause(act)}.
|
|
2651
|
+
write(`[halfcycle] Signed in as ${ink("keyBold", accountLabel(replacementIdentity, replacement.accountId, deps.verbose === true), colour)}. This install will ${actClause(act)}.
|
|
2367
2652
|
`);
|
|
2368
2653
|
if (script.afterSwitch !== "")
|
|
2369
2654
|
write(script.afterSwitch);
|
|
@@ -2875,7 +3160,7 @@ async function openPhase(credential, phase, entry) {
|
|
|
2875
3160
|
body: JSON.stringify({ phase, entry })
|
|
2876
3161
|
});
|
|
2877
3162
|
} catch (err) {
|
|
2878
|
-
throw new Error(`[halfcycle] Could not reach the Halfcycle
|
|
3163
|
+
throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. No phase was opened.`);
|
|
2879
3164
|
}
|
|
2880
3165
|
if (!res.ok) {
|
|
2881
3166
|
const body2 = await res.json().catch(() => null);
|
|
@@ -2928,7 +3213,7 @@ async function closePhase(credential, phase, outcome, close) {
|
|
|
2928
3213
|
body: JSON.stringify({ phase, acceptanceOutcome: outcome, close })
|
|
2929
3214
|
});
|
|
2930
3215
|
} catch (err) {
|
|
2931
|
-
throw new Error(`[halfcycle] Could not reach the Halfcycle
|
|
3216
|
+
throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. The phase was not closed.`);
|
|
2932
3217
|
}
|
|
2933
3218
|
if (!res.ok) {
|
|
2934
3219
|
const body2 = await res.json().catch(() => null);
|
|
@@ -3059,11 +3344,127 @@ var PLACEHOLDERS = {
|
|
|
3059
3344
|
"--evidence": '"\u2026"'
|
|
3060
3345
|
};
|
|
3061
3346
|
|
|
3347
|
+
// dist/banner-facts.js
|
|
3348
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3349
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3350
|
+
import { basename as basename2, join as join9, resolve as resolve3 } from "node:path";
|
|
3351
|
+
var BRAND_URL = "halfcycle.ai";
|
|
3352
|
+
var TAGLINE = [
|
|
3353
|
+
"Fewer cycles.",
|
|
3354
|
+
"The thinking moves earlier. So does the finish."
|
|
3355
|
+
];
|
|
3356
|
+
var TAGLINE_NARROW = [
|
|
3357
|
+
"Fewer cycles.",
|
|
3358
|
+
"The thinking moves earlier.",
|
|
3359
|
+
"So does the finish."
|
|
3360
|
+
];
|
|
3361
|
+
function bundleVersion() {
|
|
3362
|
+
try {
|
|
3363
|
+
const pkg = JSON.parse(readFileSync8(join9(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
3364
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3365
|
+
} catch {
|
|
3366
|
+
return void 0;
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
function claudeCodeVersion() {
|
|
3370
|
+
try {
|
|
3371
|
+
const raw = execFileSync3("claude", ["--version"], {
|
|
3372
|
+
encoding: "utf-8",
|
|
3373
|
+
timeout: 3e3,
|
|
3374
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3375
|
+
});
|
|
3376
|
+
return /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
|
|
3377
|
+
} catch {
|
|
3378
|
+
return void 0;
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
var REAL_PROBES = {
|
|
3382
|
+
bundleVersion,
|
|
3383
|
+
claudeCodeVersion,
|
|
3384
|
+
projectName: (dir) => {
|
|
3385
|
+
try {
|
|
3386
|
+
return deriveProjectName(dir);
|
|
3387
|
+
} catch {
|
|
3388
|
+
return void 0;
|
|
3389
|
+
}
|
|
3390
|
+
},
|
|
3391
|
+
commitCount: (dir) => {
|
|
3392
|
+
try {
|
|
3393
|
+
return readCommitCount(dir);
|
|
3394
|
+
} catch {
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
},
|
|
3398
|
+
installedVersion: (dir) => {
|
|
3399
|
+
try {
|
|
3400
|
+
return readBundlePin(dir)?.version;
|
|
3401
|
+
} catch {
|
|
3402
|
+
return void 0;
|
|
3403
|
+
}
|
|
3404
|
+
},
|
|
3405
|
+
websiteHost: (env) => {
|
|
3406
|
+
try {
|
|
3407
|
+
const resolved = resolveControlOrigin(env);
|
|
3408
|
+
return resolved.source === "default" ? BRAND_URL : new URL(resolved.origin).host;
|
|
3409
|
+
} catch {
|
|
3410
|
+
return void 0;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
function fact(label, value, narrow) {
|
|
3415
|
+
if (value === void 0 || value === "")
|
|
3416
|
+
return void 0;
|
|
3417
|
+
return narrow === void 0 ? { label, value } : { label, value, narrow };
|
|
3418
|
+
}
|
|
3419
|
+
function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBES) {
|
|
3420
|
+
const dot = UNICODE_GLYPHS.dot;
|
|
3421
|
+
const dash = UNICODE_GLYPHS.dash;
|
|
3422
|
+
const version = probes.bundleVersion();
|
|
3423
|
+
const name = probes.projectName(projectDir) ?? safeBasename(projectDir);
|
|
3424
|
+
const commits = probes.commitCount(projectDir);
|
|
3425
|
+
const installed = probes.installedVersion(projectDir);
|
|
3426
|
+
const project = commits === null ? name : `${name} ${dot} git ${dot} ${commits} ${commits === 1 ? "commit" : "commits"}`;
|
|
3427
|
+
const status = installed === void 0 ? `new ${dash} nothing installed yet` : `installed ${dash} v${installed}`;
|
|
3428
|
+
const facts = [
|
|
3429
|
+
fact("Version", version, version),
|
|
3430
|
+
fact("Claude Code", probes.claudeCodeVersion()),
|
|
3431
|
+
fact("Project", project, name),
|
|
3432
|
+
fact("Status", status, installed === void 0 ? "new" : "installed"),
|
|
3433
|
+
// `Website`, NOT `Plane` (W-3, #868). "Plane" is our internal word for the control
|
|
3434
|
+
// plane; the developer reading this has never heard it, and raw method vocabulary
|
|
3435
|
+
// reaching a client surface is a recorded finding class in this product. The row
|
|
3436
|
+
// answers *which Halfcycle is this*, and an address is the answer they recognise.
|
|
3437
|
+
fact("Website", probes.websiteHost(env))
|
|
3438
|
+
].filter((f) => f !== void 0);
|
|
3439
|
+
return {
|
|
3440
|
+
head: `halfcycle@${name}`,
|
|
3441
|
+
facts,
|
|
3442
|
+
tag: TAGLINE,
|
|
3443
|
+
tagNarrow: TAGLINE_NARROW,
|
|
3444
|
+
url: BRAND_URL,
|
|
3445
|
+
// `''` when even this package's own manifest could not be read. The one-line cut
|
|
3446
|
+
// drops the whole `vX.Y.Z` clause in that case rather than printing a bare `v`.
|
|
3447
|
+
version: version ?? ""
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
function safeBasename(dir) {
|
|
3451
|
+
try {
|
|
3452
|
+
const name = basename2(resolve3(dir)).trim();
|
|
3453
|
+
return name === "" ? "project" : name;
|
|
3454
|
+
} catch {
|
|
3455
|
+
return "project";
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3062
3459
|
// dist/bin.js
|
|
3063
3460
|
var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
|
|
3461
|
+
var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
|
|
3462
|
+
var VERBOSE_FLAGS = /* @__PURE__ */ new Set(["--verbose", "-v"]);
|
|
3064
3463
|
var rawArgs = process.argv.slice(2);
|
|
3065
3464
|
var assumeYes = rawArgs.some((a) => ASSUME_YES_FLAGS.has(a));
|
|
3066
|
-
var
|
|
3465
|
+
var quiet = rawArgs.some((a) => QUIET_FLAGS.has(a));
|
|
3466
|
+
var verbose = rawArgs.some((a) => VERBOSE_FLAGS.has(a));
|
|
3467
|
+
var args = rawArgs.filter((a) => !ASSUME_YES_FLAGS.has(a) && !QUIET_FLAGS.has(a) && !VERBOSE_FLAGS.has(a));
|
|
3067
3468
|
var MIN_HEADERS_HELPER_VERSION = "2.1.118";
|
|
3068
3469
|
var MIN_HEADER_ROTATION_VERSION = "2.1.193";
|
|
3069
3470
|
function compareVersions(a, b) {
|
|
@@ -3077,10 +3478,10 @@ function compareVersions(a, b) {
|
|
|
3077
3478
|
}
|
|
3078
3479
|
return 0;
|
|
3079
3480
|
}
|
|
3080
|
-
function reportClaudeCodeVersion() {
|
|
3481
|
+
function reportClaudeCodeVersion(verbose2) {
|
|
3081
3482
|
let raw;
|
|
3082
3483
|
try {
|
|
3083
|
-
raw =
|
|
3484
|
+
raw = execFileSync4("claude", ["--version"], {
|
|
3084
3485
|
encoding: "utf-8",
|
|
3085
3486
|
timeout: 5e3,
|
|
3086
3487
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -3093,13 +3494,18 @@ function reportClaudeCodeVersion() {
|
|
|
3093
3494
|
return;
|
|
3094
3495
|
const version = found[1];
|
|
3095
3496
|
if (compareVersions(version, MIN_HEADERS_HELPER_VERSION) >= 0) {
|
|
3096
|
-
|
|
3497
|
+
if (verbose2) {
|
|
3498
|
+
process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
|
|
3097
3499
|
`);
|
|
3500
|
+
}
|
|
3098
3501
|
return;
|
|
3099
3502
|
}
|
|
3100
3503
|
process.stdout.write(`[halfcycle] WARNING: Claude Code ${version} is older than ${MIN_HEADERS_HELPER_VERSION}, which ignores the credential helper this install wrote. The Halfcycle server will connect with no credential and every call will fail \u2014 upgrade Claude Code, then re-open this folder.
|
|
3101
3504
|
`);
|
|
3102
3505
|
}
|
|
3506
|
+
function shortId(id) {
|
|
3507
|
+
return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
|
|
3508
|
+
}
|
|
3103
3509
|
var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|client]
|
|
3104
3510
|
install into target (default: the current directory)
|
|
3105
3511
|
--yes / -y: use this machine's saved sign-in without being asked
|
|
@@ -3107,10 +3513,13 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
3107
3513
|
halfcycle build-record <phase-id> [--repo <root>]
|
|
3108
3514
|
halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
|
|
3109
3515
|
halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
3516
|
+
|
|
3517
|
+
--quiet / -q: print no opening banner (any command)
|
|
3518
|
+
--verbose / -v: print full run detail (paths written, merged, skipped) on install
|
|
3110
3519
|
`;
|
|
3111
3520
|
function isHalfcycleMonorepo(dir) {
|
|
3112
3521
|
try {
|
|
3113
|
-
const pkg = JSON.parse(
|
|
3522
|
+
const pkg = JSON.parse(readFileSync9(join10(dir, "package.json"), "utf-8"));
|
|
3114
3523
|
return pkg.name === "halfcycle-monorepo";
|
|
3115
3524
|
} catch {
|
|
3116
3525
|
return false;
|
|
@@ -3118,6 +3527,11 @@ function isHalfcycleMonorepo(dir) {
|
|
|
3118
3527
|
}
|
|
3119
3528
|
async function main() {
|
|
3120
3529
|
const [cmd, ...rest] = args;
|
|
3530
|
+
const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase)$/.test(cmd);
|
|
3531
|
+
const installArm = cmd === "install" || cmd === void 0 || bareTarget;
|
|
3532
|
+
const positionals = cmd === "install" ? rest : args;
|
|
3533
|
+
const targetRepo = installArm ? positionals[0] ?? process.cwd() : process.cwd();
|
|
3534
|
+
printBanner(process.stdout, openingBannerContent(targetRepo), currentEnvironment(quiet));
|
|
3121
3535
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
3122
3536
|
process.stdout.write(`halfcycle \u2014 install and drive the Halfcycle method in a repository.
|
|
3123
3537
|
|
|
@@ -3126,10 +3540,7 @@ ${USAGE}`);
|
|
|
3126
3540
|
process.exit(0);
|
|
3127
3541
|
return;
|
|
3128
3542
|
}
|
|
3129
|
-
|
|
3130
|
-
if (cmd === "install" || cmd === void 0 || bareTarget) {
|
|
3131
|
-
const positionals = cmd === "install" ? rest : args;
|
|
3132
|
-
const targetRepo = positionals[0] ?? process.cwd();
|
|
3543
|
+
if (installArm) {
|
|
3133
3544
|
const engagementIdArg = positionals[1];
|
|
3134
3545
|
const engagementTypeRaw = positionals[2] ?? "client";
|
|
3135
3546
|
if (isHalfcycleMonorepo(targetRepo)) {
|
|
@@ -3151,19 +3562,26 @@ ${USAGE}`);
|
|
|
3151
3562
|
try {
|
|
3152
3563
|
const controlOrigin = resolveControlOrigin(process.env);
|
|
3153
3564
|
const serviceUrl = controlOrigin.origin;
|
|
3154
|
-
|
|
3565
|
+
if (verbose || controlOrigin.source !== "default") {
|
|
3566
|
+
process.stdout.write(`[halfcycle] Halfcycle service: ${controlOriginNote(controlOrigin)}
|
|
3155
3567
|
`);
|
|
3568
|
+
}
|
|
3156
3569
|
const pinned = readPinnedEngagement(targetRepo);
|
|
3157
3570
|
const requestedId = engagementIdArg ?? pinned?.engagementId;
|
|
3158
3571
|
const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
|
|
3159
3572
|
let engagementId;
|
|
3160
3573
|
let credential;
|
|
3161
3574
|
let actingAccountId;
|
|
3575
|
+
let foundLine = "";
|
|
3162
3576
|
if (reusable !== void 0 && pinned !== null) {
|
|
3163
3577
|
engagementId = pinned.engagementId;
|
|
3164
3578
|
credential = reusable;
|
|
3165
|
-
|
|
3579
|
+
actingAccountId = readBundlePin(targetRepo)?.accountId;
|
|
3580
|
+
foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
|
|
3581
|
+
if (verbose) {
|
|
3582
|
+
process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
|
|
3166
3583
|
`);
|
|
3584
|
+
}
|
|
3167
3585
|
if (pinned.fromLegacyEnvLocal) {
|
|
3168
3586
|
process.stdout.write(`[halfcycle] Its credential is in this repository's .env.local \u2014 an install from before
|
|
3169
3587
|
[halfcycle] credentials moved out of the tree. It is being copied to
|
|
@@ -3172,7 +3590,7 @@ ${USAGE}`);
|
|
|
3172
3590
|
`);
|
|
3173
3591
|
}
|
|
3174
3592
|
} else if (requestedId !== void 0) {
|
|
3175
|
-
const joined = await joinPinnedEngagement(serviceUrl, requestedId, targetRepo, { assumeYes });
|
|
3593
|
+
const joined = await joinPinnedEngagement(serviceUrl, requestedId, targetRepo, { assumeYes, verbose });
|
|
3176
3594
|
engagementId = joined.engagementId;
|
|
3177
3595
|
actingAccountId = joined.actingAccountId;
|
|
3178
3596
|
credential = {
|
|
@@ -3185,10 +3603,13 @@ ${USAGE}`);
|
|
|
3185
3603
|
// an absent value, never `undefined` verbatim.
|
|
3186
3604
|
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
3187
3605
|
};
|
|
3188
|
-
|
|
3606
|
+
foundLine = `this repo is pinned to a shared project \u2014 joining it as you (${shortId(joined.engagementId)})`;
|
|
3607
|
+
if (verbose) {
|
|
3608
|
+
process.stdout.write(`[halfcycle] Joined engagement ${joined.engagementId} \u2014 this credential is yours. Everyone else's keeps working; nobody was signed out and nothing was pasted.
|
|
3189
3609
|
`);
|
|
3610
|
+
}
|
|
3190
3611
|
} else {
|
|
3191
|
-
const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes });
|
|
3612
|
+
const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes, verbose });
|
|
3192
3613
|
engagementId = created.engagementId;
|
|
3193
3614
|
actingAccountId = created.actingAccountId;
|
|
3194
3615
|
credential = {
|
|
@@ -3204,8 +3625,11 @@ ${USAGE}`);
|
|
|
3204
3625
|
// emission is fail-open on that absence.
|
|
3205
3626
|
controlTelemetryUrl: created.controlTelemetryUrl
|
|
3206
3627
|
};
|
|
3207
|
-
|
|
3628
|
+
foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
3629
|
+
if (verbose) {
|
|
3630
|
+
process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
|
|
3208
3631
|
`);
|
|
3632
|
+
}
|
|
3209
3633
|
}
|
|
3210
3634
|
const result = await install({
|
|
3211
3635
|
targetRepo,
|
|
@@ -3214,17 +3638,19 @@ ${USAGE}`);
|
|
|
3214
3638
|
credential,
|
|
3215
3639
|
accountId: actingAccountId
|
|
3216
3640
|
});
|
|
3217
|
-
|
|
3641
|
+
if (verbose) {
|
|
3642
|
+
process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
|
|
3218
3643
|
`);
|
|
3219
|
-
|
|
3644
|
+
process.stdout.write(`[halfcycle] Written: ${result.writtenPaths.length} paths
|
|
3220
3645
|
`);
|
|
3221
|
-
|
|
3222
|
-
|
|
3646
|
+
if (result.mergedPaths.length > 0) {
|
|
3647
|
+
process.stdout.write(`[halfcycle] Merged: ${result.mergedPaths.join(", ")}
|
|
3223
3648
|
`);
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3649
|
+
}
|
|
3650
|
+
if (result.skippedPaths.length > 0) {
|
|
3651
|
+
process.stdout.write(`[halfcycle] Skipped (already present): ${result.skippedPaths.join(", ")}
|
|
3227
3652
|
`);
|
|
3653
|
+
}
|
|
3228
3654
|
}
|
|
3229
3655
|
if (result.collidedPaths.length > 0) {
|
|
3230
3656
|
process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${result.collidedPaths.join(", ")}
|
|
@@ -3242,22 +3668,37 @@ ${USAGE}`);
|
|
|
3242
3668
|
}
|
|
3243
3669
|
const probe = await probeMcpOrigin(credential.mcpUrl);
|
|
3244
3670
|
if (probe.reached) {
|
|
3245
|
-
|
|
3671
|
+
if (verbose) {
|
|
3672
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
3246
3673
|
`);
|
|
3674
|
+
}
|
|
3247
3675
|
} else {
|
|
3248
3676
|
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
3249
3677
|
[halfcycle] The Halfcycle commands will have no method context until that address answers. This is the SECOND of two origins and the platform supplied it, so it is not the CONTROL origin (${credential.serviceUrl}) that is wrong \u2014 that one just worked. It is recorded as HALFCYCLE_MCP_URL in this engagement's credential store; re-run this installer once the service is up.
|
|
3250
3678
|
`);
|
|
3251
3679
|
}
|
|
3252
|
-
|
|
3680
|
+
if (verbose) {
|
|
3681
|
+
process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_COVERAGE_REQUIRED_KEYS.join(", ")}) are in ${engagementEnvPath(engagementId)}, outside this repository; the hook loads them itself, so you do not export anything and nothing here can be committed.
|
|
3682
|
+
`);
|
|
3683
|
+
process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through .halfcycle/mcp-headers.sh, so there is no token in this tree to commit and nothing for you to export.
|
|
3684
|
+
`);
|
|
3685
|
+
process.stdout.write(`[halfcycle] Expect a workspace trust prompt the first time you open this folder \u2014 the helper is a shell command, and Claude Code will not run it until you accept. Needs Claude Code ${MIN_HEADERS_HELPER_VERSION} or newer (${MIN_HEADER_ROTATION_VERSION}+ to re-authenticate a rotated token without restarting).
|
|
3686
|
+
`);
|
|
3687
|
+
process.stdout.write(`[halfcycle] If Halfcycle tools appear but every call returns 401, the helper could not read HALFCYCLE_TOKEN from ${engagementEnvPath(engagementId)} \u2014 re-run this installer.
|
|
3688
|
+
`);
|
|
3689
|
+
}
|
|
3690
|
+
reportClaudeCodeVersion(verbose);
|
|
3691
|
+
const identity = await describeAccount(credential.serviceUrl, credential.token);
|
|
3692
|
+
const written = result.writtenPaths.length;
|
|
3693
|
+
const merged = result.mergedPaths.length;
|
|
3694
|
+
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
3253
3695
|
`);
|
|
3254
|
-
process.stdout.write(`[halfcycle]
|
|
3696
|
+
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
3255
3697
|
`);
|
|
3256
|
-
process.stdout.write(`[halfcycle]
|
|
3698
|
+
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, guards armed on every edit
|
|
3257
3699
|
`);
|
|
3258
|
-
process.stdout.write(`[halfcycle]
|
|
3700
|
+
process.stdout.write(`[halfcycle] Next: open this folder in Claude Code \u2014 accept the workspace-trust prompt, it is expected \u2014 then run /halfcycle-setup and answer what it asks about your project
|
|
3259
3701
|
`);
|
|
3260
|
-
reportClaudeCodeVersion();
|
|
3261
3702
|
try {
|
|
3262
3703
|
const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
|
|
3263
3704
|
process.stdout.write(`[halfcycle] Your board (first visit): ${minted.boardUrl}
|
|
@@ -3282,17 +3723,17 @@ ${USAGE}`);
|
|
|
3282
3723
|
return;
|
|
3283
3724
|
}
|
|
3284
3725
|
if (cmd === "check-drift") {
|
|
3285
|
-
const
|
|
3286
|
-
if (!
|
|
3726
|
+
const targetRepo2 = rest[0];
|
|
3727
|
+
if (!targetRepo2) {
|
|
3287
3728
|
process.stderr.write("halfcycle-bundle: usage: halfcycle-bundle check-drift <target-repo>\n");
|
|
3288
3729
|
process.exit(1);
|
|
3289
3730
|
return;
|
|
3290
3731
|
}
|
|
3291
3732
|
try {
|
|
3292
|
-
const { drifted, installed, current } = checkDrift(
|
|
3733
|
+
const { drifted, installed, current } = checkDrift(targetRepo2);
|
|
3293
3734
|
if (drifted) {
|
|
3294
3735
|
process.stdout.write(`[halfcycle-bundle] DRIFT DETECTED: installed=${installed ?? "none"} current=${current}
|
|
3295
|
-
[halfcycle-bundle] Re-run "halfcycle-bundle install ${
|
|
3736
|
+
[halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo2}" to sync.
|
|
3296
3737
|
`);
|
|
3297
3738
|
process.exit(2);
|
|
3298
3739
|
} else {
|
|
@@ -3317,12 +3758,12 @@ ${USAGE}`);
|
|
|
3317
3758
|
}
|
|
3318
3759
|
const phaseId = Number(phaseArg);
|
|
3319
3760
|
try {
|
|
3320
|
-
const inputPath =
|
|
3321
|
-
const input = JSON.parse(
|
|
3761
|
+
const inputPath = join10(repoRoot, ".workbench", "build-record", `phase-${phaseId}.input.json`);
|
|
3762
|
+
const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
|
|
3322
3763
|
const result = assemblePhaseBuildRecord({
|
|
3323
3764
|
repoRoot,
|
|
3324
|
-
phasesDir:
|
|
3325
|
-
guardEvalLogDir: input.guardEvalLogDir ??
|
|
3765
|
+
phasesDir: join10(repoRoot, "docs", "phases"),
|
|
3766
|
+
guardEvalLogDir: input.guardEvalLogDir ?? join10(repoRoot, ".workbench", "guard-eval-log"),
|
|
3326
3767
|
phaseId,
|
|
3327
3768
|
narrated: input.narrated,
|
|
3328
3769
|
touchedInvariants: input.touchedInvariants
|