halfcycle 0.3.12 → 0.3.13
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 +73 -0
- package/dist/banner-facts.d.ts.map +1 -0
- package/dist/banner.d.ts +188 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/bin.js +436 -49
- package/dist/bin.js.map +3 -3
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/install.d.ts +9 -0
- package/dist/install.d.ts.map +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)) {
|
|
@@ -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
|
|
|
@@ -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();
|
|
@@ -3059,11 +3066,351 @@ var PLACEHOLDERS = {
|
|
|
3059
3066
|
"--evidence": '"\u2026"'
|
|
3060
3067
|
};
|
|
3061
3068
|
|
|
3069
|
+
// dist/banner.js
|
|
3070
|
+
var MARK_FULL = {
|
|
3071
|
+
width: 32,
|
|
3072
|
+
discRows: 7,
|
|
3073
|
+
lines: [
|
|
3074
|
+
" :+oshhhhso+:",
|
|
3075
|
+
" :sdMMMMMMMMMMMMds:",
|
|
3076
|
+
" :hMMMMMMMMMMMMMMMMMMh:",
|
|
3077
|
+
" oMMMMMMMMMMMMMMMMMMMMMMo",
|
|
3078
|
+
" oMMMMMMMMMMMMMMMMMMMMMMMMo",
|
|
3079
|
+
" NMMMMMMMMMMMMMMMMMMMMMMMMN",
|
|
3080
|
+
" :ssssssssssssssssssssssssss:",
|
|
3081
|
+
" +dddddddddddddddddddddddddd+",
|
|
3082
|
+
" Nd dN",
|
|
3083
|
+
" oMy yMo",
|
|
3084
|
+
" oMh: :hMo",
|
|
3085
|
+
" :hMy- -yMh:",
|
|
3086
|
+
" :sdNys++--++syNds:",
|
|
3087
|
+
" :+oshhhhso+:"
|
|
3088
|
+
]
|
|
3089
|
+
};
|
|
3090
|
+
var MARK_NARROW = {
|
|
3091
|
+
width: 24,
|
|
3092
|
+
discRows: 5,
|
|
3093
|
+
lines: [
|
|
3094
|
+
" -ohdMMMMdho-",
|
|
3095
|
+
" +dMMMMMMMMMMMMd+",
|
|
3096
|
+
" yMMMMMMMMMMMMMMMMy",
|
|
3097
|
+
" sMMMMMMMMMMMMMMMMMMs",
|
|
3098
|
+
" yhhhhhhhhhhhhhhhhhhy",
|
|
3099
|
+
" NdhhhhhhhhhhhhhhhhdN",
|
|
3100
|
+
" sd ds",
|
|
3101
|
+
" yd: :dy",
|
|
3102
|
+
" +dy+ +yd+",
|
|
3103
|
+
" -ohhhhhhhho-"
|
|
3104
|
+
]
|
|
3105
|
+
};
|
|
3106
|
+
var UNICODE_GLYPHS = {
|
|
3107
|
+
rule: "\u2500",
|
|
3108
|
+
swatch: "\u2584",
|
|
3109
|
+
dot: "\xB7",
|
|
3110
|
+
dash: "\u2014"
|
|
3111
|
+
};
|
|
3112
|
+
var ASCII_GLYPHS = {
|
|
3113
|
+
rule: "-",
|
|
3114
|
+
swatch: "#",
|
|
3115
|
+
dot: "|",
|
|
3116
|
+
dash: "-"
|
|
3117
|
+
};
|
|
3118
|
+
function toAscii(text) {
|
|
3119
|
+
let out = text;
|
|
3120
|
+
for (const key of Object.keys(UNICODE_GLYPHS)) {
|
|
3121
|
+
out = out.split(UNICODE_GLYPHS[key]).join(ASCII_GLYPHS[key]);
|
|
3122
|
+
}
|
|
3123
|
+
return out.replace(/[^ -~]/g, "?");
|
|
3124
|
+
}
|
|
3125
|
+
var INK = {
|
|
3126
|
+
plain: "",
|
|
3127
|
+
dim: "2",
|
|
3128
|
+
bold: "1",
|
|
3129
|
+
/** Yellow — the nearest ANSI seat for the brand's gold, used for labels and the URL. */
|
|
3130
|
+
key: "33",
|
|
3131
|
+
keyBold: "33;1"
|
|
3132
|
+
};
|
|
3133
|
+
var SWATCH_SGR = ["90", "31", "32", "33", "34", "35", "36", "37"];
|
|
3134
|
+
var ESC = "\x1B";
|
|
3135
|
+
var RESET = `${ESC}[0m`;
|
|
3136
|
+
function paint(segments, colour) {
|
|
3137
|
+
return segments.map((s) => colour && s.sgr !== "" ? `${ESC}[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
3138
|
+
}
|
|
3139
|
+
var GAP = 4;
|
|
3140
|
+
var FULL_LAYOUT = { art: MARK_FULL, label: 13, rule: 28, palette: true, minColumns: 76 };
|
|
3141
|
+
var NARROW_LAYOUT = { art: MARK_NARROW, label: 10, rule: 20, palette: false, minColumns: 48 };
|
|
3142
|
+
var DEFAULT_COLUMNS = 80;
|
|
3143
|
+
function usableColumns(environment) {
|
|
3144
|
+
const columns = environment.columns;
|
|
3145
|
+
return columns !== void 0 && columns > 0 ? columns : DEFAULT_COLUMNS;
|
|
3146
|
+
}
|
|
3147
|
+
function isSet(value) {
|
|
3148
|
+
return value !== void 0 && value !== "";
|
|
3149
|
+
}
|
|
3150
|
+
function plainTextRequested(env) {
|
|
3151
|
+
return isSet(env["NO_COLOR"]) || env["TERM"] === "dumb";
|
|
3152
|
+
}
|
|
3153
|
+
function decideCut(environment = {}) {
|
|
3154
|
+
const env = environment.env ?? {};
|
|
3155
|
+
if (environment.quiet === true)
|
|
3156
|
+
return "none";
|
|
3157
|
+
if (environment.isTTY !== true)
|
|
3158
|
+
return "line";
|
|
3159
|
+
if (plainTextRequested(env))
|
|
3160
|
+
return "line";
|
|
3161
|
+
const columns = usableColumns(environment);
|
|
3162
|
+
if (columns >= FULL_LAYOUT.minColumns)
|
|
3163
|
+
return "full";
|
|
3164
|
+
if (columns >= NARROW_LAYOUT.minColumns)
|
|
3165
|
+
return "narrow";
|
|
3166
|
+
return "line";
|
|
3167
|
+
}
|
|
3168
|
+
function decideColour(environment = {}) {
|
|
3169
|
+
const env = environment.env ?? {};
|
|
3170
|
+
const force = env["FORCE_COLOR"];
|
|
3171
|
+
if (isSet(force))
|
|
3172
|
+
return force !== "0";
|
|
3173
|
+
if (plainTextRequested(env))
|
|
3174
|
+
return false;
|
|
3175
|
+
return environment.isTTY === true;
|
|
3176
|
+
}
|
|
3177
|
+
function decideUnicode(environment = {}) {
|
|
3178
|
+
const env = environment.env ?? {};
|
|
3179
|
+
if ((environment.platform ?? "") !== "win32") {
|
|
3180
|
+
const locale = env["LC_ALL"] ?? env["LC_CTYPE"] ?? env["LANG"] ?? "";
|
|
3181
|
+
return locale === "" || /utf-?8/i.test(locale);
|
|
3182
|
+
}
|
|
3183
|
+
return isSet(env["WT_SESSION"]) || isSet(env["TERMINUS_SUBLIME"]) || env["ConEmuTask"] === "{cmd::Cmder}" || env["TERM_PROGRAM"] === "vscode";
|
|
3184
|
+
}
|
|
3185
|
+
function padEnd(text, width) {
|
|
3186
|
+
return text + " ".repeat(Math.max(0, width - text.length));
|
|
3187
|
+
}
|
|
3188
|
+
function clip(text, width) {
|
|
3189
|
+
if (width < 8 || text.length <= width)
|
|
3190
|
+
return text;
|
|
3191
|
+
return `${text.slice(0, width - 3)}...`;
|
|
3192
|
+
}
|
|
3193
|
+
function paletteRow(swatch) {
|
|
3194
|
+
const row = [];
|
|
3195
|
+
SWATCH_SGR.forEach((sgr, i) => {
|
|
3196
|
+
if (i > 0)
|
|
3197
|
+
row.push({ text: " ", sgr: INK.plain });
|
|
3198
|
+
row.push({ text: swatch.repeat(2), sgr });
|
|
3199
|
+
});
|
|
3200
|
+
return row;
|
|
3201
|
+
}
|
|
3202
|
+
function factColumn(content, layout, glyphs, text, valueWidth) {
|
|
3203
|
+
const rows = [];
|
|
3204
|
+
const head = text(content.head);
|
|
3205
|
+
const at = head.indexOf("@");
|
|
3206
|
+
rows.push(at < 0 ? [{ text: head, sgr: INK.keyBold }] : [
|
|
3207
|
+
{ text: head.slice(0, at), sgr: INK.keyBold },
|
|
3208
|
+
{ text: "@", sgr: INK.dim },
|
|
3209
|
+
{ text: head.slice(at + 1), sgr: INK.bold }
|
|
3210
|
+
]);
|
|
3211
|
+
rows.push([{ text: glyphs.rule.repeat(layout.rule), sgr: INK.dim }]);
|
|
3212
|
+
const narrow = layout.art === MARK_NARROW;
|
|
3213
|
+
for (const fact2 of content.facts) {
|
|
3214
|
+
const raw = narrow ? fact2.narrow : fact2.value;
|
|
3215
|
+
if (raw === void 0)
|
|
3216
|
+
continue;
|
|
3217
|
+
rows.push([
|
|
3218
|
+
{ text: padEnd(`${text(fact2.label)}:`, layout.label), sgr: INK.key },
|
|
3219
|
+
{ text: clip(text(raw), valueWidth), sgr: INK.plain }
|
|
3220
|
+
]);
|
|
3221
|
+
}
|
|
3222
|
+
if (layout.palette) {
|
|
3223
|
+
rows.push([]);
|
|
3224
|
+
rows.push(paletteRow(glyphs.swatch));
|
|
3225
|
+
}
|
|
3226
|
+
return rows;
|
|
3227
|
+
}
|
|
3228
|
+
function renderBlock(content, layout, environment) {
|
|
3229
|
+
const unicode = decideUnicode(environment);
|
|
3230
|
+
const colour = decideColour(environment);
|
|
3231
|
+
const glyphs = unicode ? UNICODE_GLYPHS : ASCII_GLYPHS;
|
|
3232
|
+
const text = (raw) => unicode ? raw : toAscii(raw);
|
|
3233
|
+
const art = layout.art;
|
|
3234
|
+
const valueWidth = usableColumns(environment) - (art.width + GAP + layout.label);
|
|
3235
|
+
const right = factColumn(content, layout, glyphs, text, valueWidth);
|
|
3236
|
+
const top = Math.max(0, Math.floor((art.lines.length - right.length) / 2));
|
|
3237
|
+
const height = Math.max(art.lines.length, right.length + top);
|
|
3238
|
+
const lines = [];
|
|
3239
|
+
for (let i = 0; i < height; i++) {
|
|
3240
|
+
const artLine = art.lines[i] ?? "";
|
|
3241
|
+
const segments = [];
|
|
3242
|
+
if (artLine.length > 0) {
|
|
3243
|
+
segments.push({ text: artLine, sgr: i < art.discRows ? INK.plain : INK.dim });
|
|
3244
|
+
}
|
|
3245
|
+
const rightRow = right[i - top];
|
|
3246
|
+
if (rightRow !== void 0 && rightRow.length > 0) {
|
|
3247
|
+
segments.push({ text: " ".repeat(Math.max(0, art.width + GAP - artLine.length)), sgr: INK.plain });
|
|
3248
|
+
segments.push(...rightRow);
|
|
3249
|
+
}
|
|
3250
|
+
lines.push(segments);
|
|
3251
|
+
}
|
|
3252
|
+
const tagline = layout.art === MARK_NARROW ? content.tagNarrow ?? content.tag : content.tag;
|
|
3253
|
+
lines.push([]);
|
|
3254
|
+
tagline.forEach((line, i) => {
|
|
3255
|
+
lines.push([{ text: ` ${text(line)}`, sgr: i === 0 ? INK.bold : INK.dim }]);
|
|
3256
|
+
});
|
|
3257
|
+
lines.push([]);
|
|
3258
|
+
lines.push([{ text: ` ${text(content.url)}`, sgr: INK.key }]);
|
|
3259
|
+
return lines.map((segments) => paint(segments, colour)).join("\n");
|
|
3260
|
+
}
|
|
3261
|
+
function renderLine(content, environment) {
|
|
3262
|
+
const unicode = decideUnicode(environment);
|
|
3263
|
+
const dot = unicode ? UNICODE_GLYPHS.dot : ASCII_GLYPHS.dot;
|
|
3264
|
+
const named = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
|
|
3265
|
+
const raw = `${named} ${dot} ${content.url}`;
|
|
3266
|
+
return unicode ? raw : toAscii(raw);
|
|
3267
|
+
}
|
|
3268
|
+
function renderBanner(content, environment = {}) {
|
|
3269
|
+
switch (decideCut(environment)) {
|
|
3270
|
+
case "none":
|
|
3271
|
+
return "";
|
|
3272
|
+
case "line":
|
|
3273
|
+
return renderLine(content, environment);
|
|
3274
|
+
case "narrow":
|
|
3275
|
+
return renderBlock(content, NARROW_LAYOUT, environment);
|
|
3276
|
+
case "full":
|
|
3277
|
+
return renderBlock(content, FULL_LAYOUT, environment);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
function printBanner(sink, content, environment = {}) {
|
|
3281
|
+
const text = renderBanner(content, environment);
|
|
3282
|
+
if (text === "")
|
|
3283
|
+
return;
|
|
3284
|
+
sink.write(`${text}
|
|
3285
|
+
`);
|
|
3286
|
+
}
|
|
3287
|
+
function currentEnvironment(quiet2) {
|
|
3288
|
+
return {
|
|
3289
|
+
columns: process.stdout.columns,
|
|
3290
|
+
isTTY: process.stdout.isTTY === true,
|
|
3291
|
+
quiet: quiet2,
|
|
3292
|
+
env: process.env,
|
|
3293
|
+
platform: process.platform
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// dist/banner-facts.js
|
|
3298
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3299
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3300
|
+
import { basename as basename2, join as join9, resolve as resolve3 } from "node:path";
|
|
3301
|
+
var BRAND_URL = "halfcycle.ai";
|
|
3302
|
+
var TAGLINE = [
|
|
3303
|
+
"Fewer cycles.",
|
|
3304
|
+
"The thinking moves earlier. So does the finish."
|
|
3305
|
+
];
|
|
3306
|
+
var TAGLINE_NARROW = [
|
|
3307
|
+
"Fewer cycles.",
|
|
3308
|
+
"The thinking moves earlier.",
|
|
3309
|
+
"So does the finish."
|
|
3310
|
+
];
|
|
3311
|
+
function bundleVersion() {
|
|
3312
|
+
try {
|
|
3313
|
+
const pkg = JSON.parse(readFileSync8(join9(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
3314
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3315
|
+
} catch {
|
|
3316
|
+
return void 0;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
function claudeCodeVersion() {
|
|
3320
|
+
try {
|
|
3321
|
+
const raw = execFileSync3("claude", ["--version"], {
|
|
3322
|
+
encoding: "utf-8",
|
|
3323
|
+
timeout: 3e3,
|
|
3324
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3325
|
+
});
|
|
3326
|
+
return /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
|
|
3327
|
+
} catch {
|
|
3328
|
+
return void 0;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
var REAL_PROBES = {
|
|
3332
|
+
bundleVersion,
|
|
3333
|
+
claudeCodeVersion,
|
|
3334
|
+
projectName: (dir) => {
|
|
3335
|
+
try {
|
|
3336
|
+
return deriveProjectName(dir);
|
|
3337
|
+
} catch {
|
|
3338
|
+
return void 0;
|
|
3339
|
+
}
|
|
3340
|
+
},
|
|
3341
|
+
commitCount: (dir) => {
|
|
3342
|
+
try {
|
|
3343
|
+
return readCommitCount(dir);
|
|
3344
|
+
} catch {
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
},
|
|
3348
|
+
installedVersion: (dir) => {
|
|
3349
|
+
try {
|
|
3350
|
+
return readBundlePin(dir)?.version;
|
|
3351
|
+
} catch {
|
|
3352
|
+
return void 0;
|
|
3353
|
+
}
|
|
3354
|
+
},
|
|
3355
|
+
planeHost: (env) => {
|
|
3356
|
+
try {
|
|
3357
|
+
const resolved = resolveControlOrigin(env);
|
|
3358
|
+
return resolved.source === "default" ? BRAND_URL : new URL(resolved.origin).host;
|
|
3359
|
+
} catch {
|
|
3360
|
+
return void 0;
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
};
|
|
3364
|
+
function fact(label, value, narrow) {
|
|
3365
|
+
if (value === void 0 || value === "")
|
|
3366
|
+
return void 0;
|
|
3367
|
+
return narrow === void 0 ? { label, value } : { label, value, narrow };
|
|
3368
|
+
}
|
|
3369
|
+
function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBES) {
|
|
3370
|
+
const dot = UNICODE_GLYPHS.dot;
|
|
3371
|
+
const dash = UNICODE_GLYPHS.dash;
|
|
3372
|
+
const version = probes.bundleVersion();
|
|
3373
|
+
const name = probes.projectName(projectDir) ?? safeBasename(projectDir);
|
|
3374
|
+
const commits = probes.commitCount(projectDir);
|
|
3375
|
+
const installed = probes.installedVersion(projectDir);
|
|
3376
|
+
const project = commits === null ? name : `${name} ${dot} git ${dot} ${commits} ${commits === 1 ? "commit" : "commits"}`;
|
|
3377
|
+
const status = installed === void 0 ? `new ${dash} nothing installed yet` : `installed ${dash} v${installed}`;
|
|
3378
|
+
const facts = [
|
|
3379
|
+
fact("Version", version, version),
|
|
3380
|
+
fact("Claude Code", probes.claudeCodeVersion()),
|
|
3381
|
+
fact("Project", project, name),
|
|
3382
|
+
fact("Status", status, installed === void 0 ? "new" : "installed"),
|
|
3383
|
+
fact("Plane", probes.planeHost(env))
|
|
3384
|
+
].filter((f) => f !== void 0);
|
|
3385
|
+
return {
|
|
3386
|
+
head: `halfcycle@${name}`,
|
|
3387
|
+
facts,
|
|
3388
|
+
tag: TAGLINE,
|
|
3389
|
+
tagNarrow: TAGLINE_NARROW,
|
|
3390
|
+
url: BRAND_URL,
|
|
3391
|
+
// `''` when even this package's own manifest could not be read. The one-line cut
|
|
3392
|
+
// drops the whole `vX.Y.Z` clause in that case rather than printing a bare `v`.
|
|
3393
|
+
version: version ?? ""
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
function safeBasename(dir) {
|
|
3397
|
+
try {
|
|
3398
|
+
const name = basename2(resolve3(dir)).trim();
|
|
3399
|
+
return name === "" ? "project" : name;
|
|
3400
|
+
} catch {
|
|
3401
|
+
return "project";
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3062
3405
|
// dist/bin.js
|
|
3063
3406
|
var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
|
|
3407
|
+
var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
|
|
3408
|
+
var VERBOSE_FLAGS = /* @__PURE__ */ new Set(["--verbose", "-v"]);
|
|
3064
3409
|
var rawArgs = process.argv.slice(2);
|
|
3065
3410
|
var assumeYes = rawArgs.some((a) => ASSUME_YES_FLAGS.has(a));
|
|
3066
|
-
var
|
|
3411
|
+
var quiet = rawArgs.some((a) => QUIET_FLAGS.has(a));
|
|
3412
|
+
var verbose = rawArgs.some((a) => VERBOSE_FLAGS.has(a));
|
|
3413
|
+
var args = rawArgs.filter((a) => !ASSUME_YES_FLAGS.has(a) && !QUIET_FLAGS.has(a) && !VERBOSE_FLAGS.has(a));
|
|
3067
3414
|
var MIN_HEADERS_HELPER_VERSION = "2.1.118";
|
|
3068
3415
|
var MIN_HEADER_ROTATION_VERSION = "2.1.193";
|
|
3069
3416
|
function compareVersions(a, b) {
|
|
@@ -3077,10 +3424,10 @@ function compareVersions(a, b) {
|
|
|
3077
3424
|
}
|
|
3078
3425
|
return 0;
|
|
3079
3426
|
}
|
|
3080
|
-
function reportClaudeCodeVersion() {
|
|
3427
|
+
function reportClaudeCodeVersion(verbose2) {
|
|
3081
3428
|
let raw;
|
|
3082
3429
|
try {
|
|
3083
|
-
raw =
|
|
3430
|
+
raw = execFileSync4("claude", ["--version"], {
|
|
3084
3431
|
encoding: "utf-8",
|
|
3085
3432
|
timeout: 5e3,
|
|
3086
3433
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -3093,13 +3440,18 @@ function reportClaudeCodeVersion() {
|
|
|
3093
3440
|
return;
|
|
3094
3441
|
const version = found[1];
|
|
3095
3442
|
if (compareVersions(version, MIN_HEADERS_HELPER_VERSION) >= 0) {
|
|
3096
|
-
|
|
3443
|
+
if (verbose2) {
|
|
3444
|
+
process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
|
|
3097
3445
|
`);
|
|
3446
|
+
}
|
|
3098
3447
|
return;
|
|
3099
3448
|
}
|
|
3100
3449
|
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
3450
|
`);
|
|
3102
3451
|
}
|
|
3452
|
+
function shortId(id) {
|
|
3453
|
+
return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
|
|
3454
|
+
}
|
|
3103
3455
|
var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|client]
|
|
3104
3456
|
install into target (default: the current directory)
|
|
3105
3457
|
--yes / -y: use this machine's saved sign-in without being asked
|
|
@@ -3107,10 +3459,13 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
3107
3459
|
halfcycle build-record <phase-id> [--repo <root>]
|
|
3108
3460
|
halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
|
|
3109
3461
|
halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
3462
|
+
|
|
3463
|
+
--quiet / -q: print no opening banner (any command)
|
|
3464
|
+
--verbose / -v: print full run detail (paths written, merged, skipped) on install
|
|
3110
3465
|
`;
|
|
3111
3466
|
function isHalfcycleMonorepo(dir) {
|
|
3112
3467
|
try {
|
|
3113
|
-
const pkg = JSON.parse(
|
|
3468
|
+
const pkg = JSON.parse(readFileSync9(join10(dir, "package.json"), "utf-8"));
|
|
3114
3469
|
return pkg.name === "halfcycle-monorepo";
|
|
3115
3470
|
} catch {
|
|
3116
3471
|
return false;
|
|
@@ -3118,6 +3473,11 @@ function isHalfcycleMonorepo(dir) {
|
|
|
3118
3473
|
}
|
|
3119
3474
|
async function main() {
|
|
3120
3475
|
const [cmd, ...rest] = args;
|
|
3476
|
+
const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase)$/.test(cmd);
|
|
3477
|
+
const installArm = cmd === "install" || cmd === void 0 || bareTarget;
|
|
3478
|
+
const positionals = cmd === "install" ? rest : args;
|
|
3479
|
+
const targetRepo = installArm ? positionals[0] ?? process.cwd() : process.cwd();
|
|
3480
|
+
printBanner(process.stdout, openingBannerContent(targetRepo), currentEnvironment(quiet));
|
|
3121
3481
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
3122
3482
|
process.stdout.write(`halfcycle \u2014 install and drive the Halfcycle method in a repository.
|
|
3123
3483
|
|
|
@@ -3126,10 +3486,7 @@ ${USAGE}`);
|
|
|
3126
3486
|
process.exit(0);
|
|
3127
3487
|
return;
|
|
3128
3488
|
}
|
|
3129
|
-
|
|
3130
|
-
if (cmd === "install" || cmd === void 0 || bareTarget) {
|
|
3131
|
-
const positionals = cmd === "install" ? rest : args;
|
|
3132
|
-
const targetRepo = positionals[0] ?? process.cwd();
|
|
3489
|
+
if (installArm) {
|
|
3133
3490
|
const engagementIdArg = positionals[1];
|
|
3134
3491
|
const engagementTypeRaw = positionals[2] ?? "client";
|
|
3135
3492
|
if (isHalfcycleMonorepo(targetRepo)) {
|
|
@@ -3151,19 +3508,26 @@ ${USAGE}`);
|
|
|
3151
3508
|
try {
|
|
3152
3509
|
const controlOrigin = resolveControlOrigin(process.env);
|
|
3153
3510
|
const serviceUrl = controlOrigin.origin;
|
|
3154
|
-
|
|
3511
|
+
if (verbose || controlOrigin.source !== "default") {
|
|
3512
|
+
process.stdout.write(`[halfcycle] Halfcycle plane: ${controlOriginNote(controlOrigin)}
|
|
3155
3513
|
`);
|
|
3514
|
+
}
|
|
3156
3515
|
const pinned = readPinnedEngagement(targetRepo);
|
|
3157
3516
|
const requestedId = engagementIdArg ?? pinned?.engagementId;
|
|
3158
3517
|
const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
|
|
3159
3518
|
let engagementId;
|
|
3160
3519
|
let credential;
|
|
3161
3520
|
let actingAccountId;
|
|
3521
|
+
let foundLine = "";
|
|
3162
3522
|
if (reusable !== void 0 && pinned !== null) {
|
|
3163
3523
|
engagementId = pinned.engagementId;
|
|
3164
3524
|
credential = reusable;
|
|
3165
|
-
|
|
3525
|
+
actingAccountId = readBundlePin(targetRepo)?.accountId;
|
|
3526
|
+
foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
|
|
3527
|
+
if (verbose) {
|
|
3528
|
+
process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
|
|
3166
3529
|
`);
|
|
3530
|
+
}
|
|
3167
3531
|
if (pinned.fromLegacyEnvLocal) {
|
|
3168
3532
|
process.stdout.write(`[halfcycle] Its credential is in this repository's .env.local \u2014 an install from before
|
|
3169
3533
|
[halfcycle] credentials moved out of the tree. It is being copied to
|
|
@@ -3185,8 +3549,11 @@ ${USAGE}`);
|
|
|
3185
3549
|
// an absent value, never `undefined` verbatim.
|
|
3186
3550
|
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
3187
3551
|
};
|
|
3188
|
-
|
|
3552
|
+
foundLine = `this repo is pinned to a shared project \u2014 joining it as you (${shortId(joined.engagementId)})`;
|
|
3553
|
+
if (verbose) {
|
|
3554
|
+
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
3555
|
`);
|
|
3556
|
+
}
|
|
3190
3557
|
} else {
|
|
3191
3558
|
const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes });
|
|
3192
3559
|
engagementId = created.engagementId;
|
|
@@ -3204,8 +3571,11 @@ ${USAGE}`);
|
|
|
3204
3571
|
// emission is fail-open on that absence.
|
|
3205
3572
|
controlTelemetryUrl: created.controlTelemetryUrl
|
|
3206
3573
|
};
|
|
3207
|
-
|
|
3574
|
+
foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
3575
|
+
if (verbose) {
|
|
3576
|
+
process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
|
|
3208
3577
|
`);
|
|
3578
|
+
}
|
|
3209
3579
|
}
|
|
3210
3580
|
const result = await install({
|
|
3211
3581
|
targetRepo,
|
|
@@ -3214,17 +3584,19 @@ ${USAGE}`);
|
|
|
3214
3584
|
credential,
|
|
3215
3585
|
accountId: actingAccountId
|
|
3216
3586
|
});
|
|
3217
|
-
|
|
3587
|
+
if (verbose) {
|
|
3588
|
+
process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
|
|
3218
3589
|
`);
|
|
3219
|
-
|
|
3590
|
+
process.stdout.write(`[halfcycle] Written: ${result.writtenPaths.length} paths
|
|
3220
3591
|
`);
|
|
3221
|
-
|
|
3222
|
-
|
|
3592
|
+
if (result.mergedPaths.length > 0) {
|
|
3593
|
+
process.stdout.write(`[halfcycle] Merged: ${result.mergedPaths.join(", ")}
|
|
3223
3594
|
`);
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3595
|
+
}
|
|
3596
|
+
if (result.skippedPaths.length > 0) {
|
|
3597
|
+
process.stdout.write(`[halfcycle] Skipped (already present): ${result.skippedPaths.join(", ")}
|
|
3227
3598
|
`);
|
|
3599
|
+
}
|
|
3228
3600
|
}
|
|
3229
3601
|
if (result.collidedPaths.length > 0) {
|
|
3230
3602
|
process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${result.collidedPaths.join(", ")}
|
|
@@ -3242,22 +3614,37 @@ ${USAGE}`);
|
|
|
3242
3614
|
}
|
|
3243
3615
|
const probe = await probeMcpOrigin(credential.mcpUrl);
|
|
3244
3616
|
if (probe.reached) {
|
|
3245
|
-
|
|
3617
|
+
if (verbose) {
|
|
3618
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
3246
3619
|
`);
|
|
3620
|
+
}
|
|
3247
3621
|
} else {
|
|
3248
3622
|
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
3249
3623
|
[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
3624
|
`);
|
|
3251
3625
|
}
|
|
3252
|
-
|
|
3626
|
+
if (verbose) {
|
|
3627
|
+
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.
|
|
3628
|
+
`);
|
|
3629
|
+
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.
|
|
3630
|
+
`);
|
|
3631
|
+
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).
|
|
3632
|
+
`);
|
|
3633
|
+
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.
|
|
3634
|
+
`);
|
|
3635
|
+
}
|
|
3636
|
+
reportClaudeCodeVersion(verbose);
|
|
3637
|
+
const identity = await describeAccount(credential.serviceUrl, credential.token);
|
|
3638
|
+
const written = result.writtenPaths.length;
|
|
3639
|
+
const merged = result.mergedPaths.length;
|
|
3640
|
+
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId)}
|
|
3253
3641
|
`);
|
|
3254
|
-
process.stdout.write(`[halfcycle]
|
|
3642
|
+
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
3255
3643
|
`);
|
|
3256
|
-
process.stdout.write(`[halfcycle]
|
|
3644
|
+
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, guards armed on every edit
|
|
3257
3645
|
`);
|
|
3258
|
-
process.stdout.write(`[halfcycle]
|
|
3646
|
+
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
3647
|
`);
|
|
3260
|
-
reportClaudeCodeVersion();
|
|
3261
3648
|
try {
|
|
3262
3649
|
const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
|
|
3263
3650
|
process.stdout.write(`[halfcycle] Your board (first visit): ${minted.boardUrl}
|
|
@@ -3282,17 +3669,17 @@ ${USAGE}`);
|
|
|
3282
3669
|
return;
|
|
3283
3670
|
}
|
|
3284
3671
|
if (cmd === "check-drift") {
|
|
3285
|
-
const
|
|
3286
|
-
if (!
|
|
3672
|
+
const targetRepo2 = rest[0];
|
|
3673
|
+
if (!targetRepo2) {
|
|
3287
3674
|
process.stderr.write("halfcycle-bundle: usage: halfcycle-bundle check-drift <target-repo>\n");
|
|
3288
3675
|
process.exit(1);
|
|
3289
3676
|
return;
|
|
3290
3677
|
}
|
|
3291
3678
|
try {
|
|
3292
|
-
const { drifted, installed, current } = checkDrift(
|
|
3679
|
+
const { drifted, installed, current } = checkDrift(targetRepo2);
|
|
3293
3680
|
if (drifted) {
|
|
3294
3681
|
process.stdout.write(`[halfcycle-bundle] DRIFT DETECTED: installed=${installed ?? "none"} current=${current}
|
|
3295
|
-
[halfcycle-bundle] Re-run "halfcycle-bundle install ${
|
|
3682
|
+
[halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo2}" to sync.
|
|
3296
3683
|
`);
|
|
3297
3684
|
process.exit(2);
|
|
3298
3685
|
} else {
|
|
@@ -3317,12 +3704,12 @@ ${USAGE}`);
|
|
|
3317
3704
|
}
|
|
3318
3705
|
const phaseId = Number(phaseArg);
|
|
3319
3706
|
try {
|
|
3320
|
-
const inputPath =
|
|
3321
|
-
const input = JSON.parse(
|
|
3707
|
+
const inputPath = join10(repoRoot, ".workbench", "build-record", `phase-${phaseId}.input.json`);
|
|
3708
|
+
const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
|
|
3322
3709
|
const result = assemblePhaseBuildRecord({
|
|
3323
3710
|
repoRoot,
|
|
3324
|
-
phasesDir:
|
|
3325
|
-
guardEvalLogDir: input.guardEvalLogDir ??
|
|
3711
|
+
phasesDir: join10(repoRoot, "docs", "phases"),
|
|
3712
|
+
guardEvalLogDir: input.guardEvalLogDir ?? join10(repoRoot, ".workbench", "guard-eval-log"),
|
|
3326
3713
|
phaseId,
|
|
3327
3714
|
narrated: input.narrated,
|
|
3328
3715
|
touchedInvariants: input.touchedInvariants
|