halfcycle 0.4.4 → 0.4.6

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/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // dist/bin.js
4
- import { existsSync as existsSync7, readFileSync as readFileSync11, realpathSync as realpathSync3, statSync, writeSync as writeSync2 } from "node:fs";
4
+ import { existsSync as existsSync7, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync, writeSync as writeSync2 } from "node:fs";
5
5
  import { homedir as homedir3 } from "node:os";
6
- import { join as join12 } from "node:path";
6
+ import { join as join14 } from "node:path";
7
7
 
8
8
  // dist/install.js
9
9
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -1307,6 +1307,349 @@ var phaseRecordResponseSchema = z12.discriminatedUnion("outcome", [
1307
1307
  phaseRecordRefusedSchema
1308
1308
  ]);
1309
1309
 
1310
+ // ../events/dist/system-description.js
1311
+ import { z as z13 } from "zod";
1312
+ var SYSTEM_DESCRIPTION_FORMAT = 1;
1313
+ var SYSTEM_PARTS_MAX = 60;
1314
+ var SYSTEM_CONNECTIONS_MAX = 400;
1315
+ var SYSTEM_SLOT_MAX = 11;
1316
+ var SYSTEM_OWNS_MAX = 20;
1317
+ var SYSTEM_OWNED_PATTERN_MAX = 200;
1318
+ var SYSTEM_PART_NAME_MAX = 60;
1319
+ var SYSTEM_PART_DESCRIPTION_MAX = 160;
1320
+ var SYSTEM_CONNECTION_LABEL_MAX = 60;
1321
+ var SYSTEM_PART_ID_PATTERN = /^[a-z][a-z0-9-]{0,39}$/;
1322
+ var SYSTEM_LANES = ["clients", "edge", "services", "data", "external"];
1323
+ var systemLaneSchema = z13.enum(SYSTEM_LANES, {
1324
+ error: `must be one of ${SYSTEM_LANES.join(", ")}`
1325
+ });
1326
+ var SYSTEM_CONNECTION_KINDS = ["call", "event", "write", "external"];
1327
+ var systemConnectionKindSchema = z13.enum(SYSTEM_CONNECTION_KINDS, {
1328
+ error: `must be one of ${SYSTEM_CONNECTION_KINDS.join(", ")}`
1329
+ });
1330
+ var LINE_SEPARATOR = /[\u2028\u2029]/;
1331
+ function descriptionText(max) {
1332
+ return statementText(max).refine((v) => v.trim().length > 0, { message: "must not be empty or only spaces" }).refine((v) => !LINE_SEPARATOR.test(v), {
1333
+ message: "must be one line: it holds a line or paragraph separator"
1334
+ });
1335
+ }
1336
+ var systemPartIdSchema = z13.string().regex(SYSTEM_PART_ID_PATTERN, {
1337
+ message: "must be a short id: a lowercase letter, then lowercase letters, digits or hyphens, 40 characters at most"
1338
+ }).refine((v) => !looksLikeSecret(v), { message: "looks like a password or key; choose another id" });
1339
+ function hasEmptySegment(pattern) {
1340
+ return pattern.split("/").some((segment) => segment.length === 0);
1341
+ }
1342
+ function hasDotSegment(pattern) {
1343
+ return pattern.split("/").some((segment) => segment === "." || segment === "..");
1344
+ }
1345
+ function hasPartialDoubleStar(pattern) {
1346
+ return pattern.split("/").some((segment) => segment.includes("**") && segment !== "**");
1347
+ }
1348
+ var SEGMENT_CHARACTERS = /^[A-Za-z0-9._\-*]*$/;
1349
+ function hasForeignCharacter(pattern) {
1350
+ return pattern.split("/").some((segment) => !SEGMENT_CHARACTERS.test(segment));
1351
+ }
1352
+ var systemOwnedPatternSchema = z13.string().max(SYSTEM_OWNED_PATTERN_MAX, { message: `must be at most ${SYSTEM_OWNED_PATTERN_MAX} characters` }).refine((v) => !hasEmptySegment(v), {
1353
+ message: "must not start or end with a slash, or have two slashes together"
1354
+ }).refine((v) => !hasDotSegment(v), { message: 'must not have a "." or ".." folder in it' }).refine((v) => !hasPartialDoubleStar(v), { message: '"**" must stand alone between slashes' }).refine((v) => !hasForeignCharacter(v), {
1355
+ message: 'may use only letters, digits, ".", "_", "-" and "*" between slashes'
1356
+ }).refine((v) => !looksLikeSecret(v), {
1357
+ message: "this path looks like a credential; list a shorter folder above it"
1358
+ });
1359
+ var systemPartSchema = z13.object({
1360
+ id: systemPartIdSchema,
1361
+ name: descriptionText(SYSTEM_PART_NAME_MAX),
1362
+ description: descriptionText(SYSTEM_PART_DESCRIPTION_MAX),
1363
+ lane: systemLaneSchema,
1364
+ slot: z13.number({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).int({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).max(SYSTEM_SLOT_MAX, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }),
1365
+ outside: z13.boolean(),
1366
+ owns: z13.array(systemOwnedPatternSchema).max(SYSTEM_OWNS_MAX, { message: `can list at most ${SYSTEM_OWNS_MAX} paths` })
1367
+ }).strict();
1368
+ var systemConnectionSchema = z13.object({
1369
+ from: systemPartIdSchema,
1370
+ to: systemPartIdSchema,
1371
+ kind: systemConnectionKindSchema,
1372
+ label: descriptionText(SYSTEM_CONNECTION_LABEL_MAX)
1373
+ }).strict();
1374
+ var systemDescriptionSchema = z13.object({
1375
+ format: z13.literal(SYSTEM_DESCRIPTION_FORMAT, { error: `must be ${SYSTEM_DESCRIPTION_FORMAT}` }),
1376
+ parts: z13.array(systemPartSchema).min(1, { message: "must list at least one part" }).max(SYSTEM_PARTS_MAX, { message: `can hold at most ${SYSTEM_PARTS_MAX} parts (five lanes of twelve slots)` }),
1377
+ connections: z13.array(systemConnectionSchema).max(SYSTEM_CONNECTIONS_MAX, { message: `can hold at most ${SYSTEM_CONNECTIONS_MAX} connections` })
1378
+ }).strict();
1379
+ var systemDescriptionProblemSchema = z13.object({
1380
+ path: z13.string(),
1381
+ message: z13.string()
1382
+ }).strict();
1383
+ function isRecord(value) {
1384
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1385
+ }
1386
+ function dotted(path) {
1387
+ return path.map((key) => String(key)).join(".");
1388
+ }
1389
+ function isMissingKey(raw, path) {
1390
+ if (path.length === 0)
1391
+ return false;
1392
+ let parent = raw;
1393
+ for (const key of path.slice(0, -1)) {
1394
+ if (!isRecord(parent) && !Array.isArray(parent))
1395
+ return false;
1396
+ parent = parent[key];
1397
+ }
1398
+ return isRecord(parent) && !(String(path[path.length - 1]) in parent);
1399
+ }
1400
+ function shapeProblems(raw) {
1401
+ const parsed = systemDescriptionSchema.safeParse(raw);
1402
+ if (parsed.success)
1403
+ return [];
1404
+ const problems = [];
1405
+ for (const issue of parsed.error.issues) {
1406
+ if (issue.code === "unrecognized_keys") {
1407
+ for (const key of issue.keys) {
1408
+ problems.push({ path: dotted([...issue.path, key]), message: "is not a field a description has" });
1409
+ }
1410
+ } else if (isMissingKey(raw, issue.path)) {
1411
+ problems.push({ path: dotted(issue.path), message: "is required" });
1412
+ } else {
1413
+ problems.push({ path: dotted(issue.path), message: issue.message });
1414
+ }
1415
+ }
1416
+ return problems;
1417
+ }
1418
+ function shown(value) {
1419
+ return looksLikeSecret(value) ? "a value that looks like a credential" : `"${value}"`;
1420
+ }
1421
+ function quoted(value) {
1422
+ return typeof value === "string" ? shown(value) : "another part";
1423
+ }
1424
+ function crossFieldProblems(raw) {
1425
+ const problems = [];
1426
+ if (!isRecord(raw))
1427
+ return problems;
1428
+ const parts = Array.isArray(raw.parts) ? raw.parts : [];
1429
+ const connections = Array.isArray(raw.connections) ? raw.connections : [];
1430
+ const ids = /* @__PURE__ */ new Set();
1431
+ const placed = /* @__PURE__ */ new Map();
1432
+ const owners = /* @__PURE__ */ new Map();
1433
+ parts.forEach((part, i) => {
1434
+ if (!isRecord(part))
1435
+ return;
1436
+ if (typeof part.id === "string") {
1437
+ if (ids.has(part.id)) {
1438
+ problems.push({ path: `parts.${i}.id`, message: `another part already has the id ${shown(part.id)}` });
1439
+ }
1440
+ ids.add(part.id);
1441
+ }
1442
+ if (typeof part.lane === "string" && typeof part.slot === "number") {
1443
+ const place = `${part.lane}\0${part.slot}`;
1444
+ const earlier = placed.get(place);
1445
+ if (earlier !== void 0) {
1446
+ problems.push({
1447
+ path: `parts.${i}.slot`,
1448
+ message: `${quoted(parts[earlier].id)} already sits in lane ${shown(part.lane)} at slot ${part.slot}`
1449
+ });
1450
+ } else {
1451
+ placed.set(place, i);
1452
+ }
1453
+ }
1454
+ if (typeof part.outside === "boolean" && Array.isArray(part.owns)) {
1455
+ if (part.outside && part.owns.length > 0) {
1456
+ problems.push({ path: `parts.${i}.owns`, message: "must be empty for a part outside the repository" });
1457
+ }
1458
+ if (!part.outside && part.owns.length === 0) {
1459
+ problems.push({ path: `parts.${i}.owns`, message: "must list at least one path this part owns" });
1460
+ }
1461
+ }
1462
+ if (Array.isArray(part.owns)) {
1463
+ part.owns.forEach((pattern, k) => {
1464
+ if (typeof pattern !== "string")
1465
+ return;
1466
+ const owner = owners.get(pattern);
1467
+ if (owner === void 0) {
1468
+ owners.set(pattern, i);
1469
+ } else if (owner === i) {
1470
+ problems.push({ path: `parts.${i}.owns.${k}`, message: `${shown(pattern)} is listed twice in this part` });
1471
+ } else {
1472
+ problems.push({
1473
+ path: `parts.${i}.owns.${k}`,
1474
+ message: `${shown(pattern)} is already owned by ${quoted(parts[owner].id)}`
1475
+ });
1476
+ }
1477
+ });
1478
+ }
1479
+ });
1480
+ const pairs = /* @__PURE__ */ new Set();
1481
+ connections.forEach((connection, i) => {
1482
+ if (!isRecord(connection))
1483
+ return;
1484
+ const { from, to } = connection;
1485
+ if (typeof from === "string" && !ids.has(from)) {
1486
+ problems.push({ path: `connections.${i}.from`, message: `names no part: ${shown(from)}` });
1487
+ }
1488
+ if (typeof to === "string" && !ids.has(to)) {
1489
+ problems.push({ path: `connections.${i}.to`, message: `names no part: ${shown(to)}` });
1490
+ }
1491
+ if (typeof from === "string" && typeof to === "string") {
1492
+ if (from === to) {
1493
+ problems.push({ path: `connections.${i}.to`, message: `must name a different part from "from" (${shown(from)})` });
1494
+ }
1495
+ const pair = `${from}\0${to}`;
1496
+ if (pairs.has(pair)) {
1497
+ problems.push({ path: `connections.${i}`, message: `another connection already goes from ${shown(from)} to ${shown(to)}` });
1498
+ }
1499
+ pairs.add(pair);
1500
+ }
1501
+ });
1502
+ return problems;
1503
+ }
1504
+ function checkSystemDescription(value) {
1505
+ const problems = [...shapeProblems(value), ...crossFieldProblems(value)];
1506
+ if (problems.length > 0)
1507
+ return { ok: false, problems };
1508
+ return { ok: true, description: systemDescriptionSchema.parse(value) };
1509
+ }
1510
+ function segmentMatches(pattern, segment) {
1511
+ const literals = pattern.split("*");
1512
+ if (literals.length === 1)
1513
+ return pattern === segment;
1514
+ const first = literals[0];
1515
+ const last = literals[literals.length - 1];
1516
+ if (!segment.startsWith(first))
1517
+ return false;
1518
+ let position = first.length;
1519
+ for (const literal of literals.slice(1, -1)) {
1520
+ const found = segment.indexOf(literal, position + 1);
1521
+ if (found === -1)
1522
+ return false;
1523
+ position = found + literal.length;
1524
+ }
1525
+ return segment.length - last.length >= position + 1 && segment.endsWith(last);
1526
+ }
1527
+ function patternOwns(pattern, path) {
1528
+ const want = pattern.split("/");
1529
+ const have = path.split("/");
1530
+ const width = have.length + 1;
1531
+ const seen = new Uint8Array((want.length + 1) * width);
1532
+ const from = (i, j) => {
1533
+ if (i === want.length)
1534
+ return true;
1535
+ const cell = i * width + j;
1536
+ if (seen[cell] !== 0)
1537
+ return seen[cell] === 2;
1538
+ const segment = want[i];
1539
+ const result = segment === "**" ? from(i + 1, j) || j < have.length && from(i, j + 1) : j < have.length && segmentMatches(segment, have[j]) && from(i + 1, j + 1);
1540
+ seen[cell] = result ? 2 : 1;
1541
+ return result;
1542
+ };
1543
+ return from(0, 0);
1544
+ }
1545
+ function specificity(pattern) {
1546
+ const segments = pattern.split("/");
1547
+ const firstWildcard = segments.findIndex((segment) => segment.includes("*"));
1548
+ return {
1549
+ leadingLiterals: firstWildcard === -1 ? segments.length : firstWildcard,
1550
+ wildcards: segments.filter((segment) => segment.includes("*")).length,
1551
+ literalCharacters: pattern.replace(/[*/]/g, "").length
1552
+ };
1553
+ }
1554
+ function outranks(a, b) {
1555
+ if (a.rank.leadingLiterals !== b.rank.leadingLiterals)
1556
+ return b.rank.leadingLiterals - a.rank.leadingLiterals;
1557
+ if (a.rank.wildcards !== b.rank.wildcards)
1558
+ return a.rank.wildcards - b.rank.wildcards;
1559
+ if (a.rank.literalCharacters !== b.rank.literalCharacters)
1560
+ return b.rank.literalCharacters - a.rank.literalCharacters;
1561
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
1562
+ }
1563
+ function ownerOf(path, description) {
1564
+ let best = null;
1565
+ for (const part of description.parts) {
1566
+ for (const pattern of part.owns) {
1567
+ if (!patternOwns(pattern, path))
1568
+ continue;
1569
+ const candidate = { id: part.id, rank: specificity(pattern) };
1570
+ if (best === null || outranks(candidate, best) < 0)
1571
+ best = candidate;
1572
+ }
1573
+ }
1574
+ return best === null ? null : best.id;
1575
+ }
1576
+ var systemReportRecordedSchema = z13.object({
1577
+ outcome: z13.literal("recorded"),
1578
+ version: z13.number().int().min(1),
1579
+ reportedAt: utcIso8601
1580
+ }).strict();
1581
+ var systemReportRefusedSchema = z13.object({
1582
+ outcome: z13.literal("refused"),
1583
+ problems: z13.array(systemDescriptionProblemSchema).min(1)
1584
+ }).strict();
1585
+ var systemReportUnavailableSchema = z13.object({
1586
+ outcome: z13.literal("unavailable")
1587
+ }).strict();
1588
+ var systemReportResultSchema = z13.discriminatedUnion("outcome", [
1589
+ systemReportRecordedSchema,
1590
+ systemReportRefusedSchema,
1591
+ systemReportUnavailableSchema
1592
+ ]);
1593
+ var SYSTEM_DRAW_SVG_MAX_BYTES = 2 * 1024 * 1024;
1594
+ var SYSTEM_DRAW_MERMAID_MAX_BYTES = 256 * 1024;
1595
+ var SYSTEM_DRAW_PNG_MAX_BYTES = 8 * 1024 * 1024;
1596
+ var SYSTEM_DIAGRAM_MARKER_LINE = "<!-- drawn by Halfcycle from docs/system.json -->";
1597
+ var systemDrawRequestSchema = z13.object({
1598
+ version: z13.number().int().min(1),
1599
+ picture: z13.literal("architecture")
1600
+ }).strict();
1601
+ function utf8ByteLength2(text) {
1602
+ let bytes = 0;
1603
+ for (let i = 0; i < text.length; i++) {
1604
+ const unit = text.charCodeAt(i);
1605
+ if (unit < 128)
1606
+ bytes += 1;
1607
+ else if (unit < 2048)
1608
+ bytes += 2;
1609
+ else if (unit >= 55296 && unit <= 56319 && i + 1 < text.length) {
1610
+ const next = text.charCodeAt(i + 1);
1611
+ if (next >= 56320 && next <= 57343) {
1612
+ bytes += 4;
1613
+ i++;
1614
+ } else {
1615
+ bytes += 3;
1616
+ }
1617
+ } else
1618
+ bytes += 3;
1619
+ }
1620
+ return bytes;
1621
+ }
1622
+ var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
1623
+ function base64DecodedLength(text) {
1624
+ const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
1625
+ return text.length / 4 * 3 - padding;
1626
+ }
1627
+ var SYSTEM_PICTURE_THEMES = ["broadsheet", "flightdeck"];
1628
+ var systemPictureSchema = z13.object({
1629
+ svg: z13.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_SVG_MAX_BYTES, { message: "the picture is larger than 2 MB" }),
1630
+ png: z13.string().min(1).refine((v) => v.length % 4 === 0 && BASE64.test(v), { message: "the image is not valid base64" }).refine((v) => base64DecodedLength(v) <= SYSTEM_DRAW_PNG_MAX_BYTES, { message: "the image is larger than 8 MB" })
1631
+ }).strict();
1632
+ var systemPicturesSchema = z13.object(Object.fromEntries(SYSTEM_PICTURE_THEMES.map((theme) => [theme, systemPictureSchema]))).strict();
1633
+ var systemDrawDrawnSchema = z13.object({
1634
+ outcome: z13.literal("drawn"),
1635
+ mermaid: z13.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_MERMAID_MAX_BYTES, {
1636
+ message: "the diagram text is larger than 256 KB"
1637
+ }),
1638
+ pictures: systemPicturesSchema
1639
+ }).strict();
1640
+ var systemDrawRefusedSchema = z13.object({
1641
+ outcome: z13.literal("refused"),
1642
+ reason: z13.string().min(1)
1643
+ }).strict();
1644
+ var systemDrawUnavailableSchema = z13.object({
1645
+ outcome: z13.literal("unavailable")
1646
+ }).strict();
1647
+ var systemDrawResultSchema = z13.discriminatedUnion("outcome", [
1648
+ systemDrawDrawnSchema,
1649
+ systemDrawRefusedSchema,
1650
+ systemDrawUnavailableSchema
1651
+ ]);
1652
+
1310
1653
  // dist/engagement-credential.js
1311
1654
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1312
1655
  import { platform as platform2 } from "node:os";
@@ -1375,8 +1718,8 @@ var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
1375
1718
  var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
1376
1719
  var InvalidEngagementIdError = class extends Error {
1377
1720
  constructor(engagementId) {
1378
- const shown = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
1379
- super(`${shown} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "npx halfcycle" again: this repository is then set up as a new Halfcycle project.`);
1721
+ const shown2 = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
1722
+ super(`${shown2} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "npx halfcycle" again: this repository is then set up as a new Halfcycle project.`);
1380
1723
  this.name = "InvalidEngagementIdError";
1381
1724
  }
1382
1725
  };
@@ -1700,7 +2043,7 @@ var UnreadableManifestError = class extends Error {
1700
2043
  }
1701
2044
  };
1702
2045
  var HEX64 = /^[0-9a-f]{64}$/;
1703
- function isRecord(value) {
2046
+ function isRecord2(value) {
1704
2047
  return value !== null && typeof value === "object" && !Array.isArray(value);
1705
2048
  }
1706
2049
  function stringArray(value, field) {
@@ -1721,7 +2064,7 @@ function bool(value, field) {
1721
2064
  return value;
1722
2065
  }
1723
2066
  function appendedTextRecord(value, field) {
1724
- if (!isRecord(value))
2067
+ if (!isRecord2(value))
1725
2068
  throw new UnreadableManifestError(`"${field}" is not an object`);
1726
2069
  onlyKeys(value, ["created", "appended"], `"${field}"`);
1727
2070
  return {
@@ -1741,7 +2084,7 @@ function parseManifest(text) {
1741
2084
  } catch {
1742
2085
  throw new UnreadableManifestError("it is not valid JSON");
1743
2086
  }
1744
- if (!isRecord(raw))
2087
+ if (!isRecord2(raw))
1745
2088
  throw new UnreadableManifestError("it is not a JSON object");
1746
2089
  onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "gitignore", "gitattributes"], "the manifest");
1747
2090
  if (raw["format"] !== INSTALL_MANIFEST_FORMAT) {
@@ -1750,7 +2093,7 @@ function parseManifest(text) {
1750
2093
  if (!Array.isArray(raw["files"]))
1751
2094
  throw new UnreadableManifestError('"files" is not a list');
1752
2095
  const files = raw["files"].map((entry) => {
1753
- if (!isRecord(entry) || typeof entry["path"] !== "string") {
2096
+ if (!isRecord2(entry) || typeof entry["path"] !== "string") {
1754
2097
  throw new UnreadableManifestError('a "files" entry has no path');
1755
2098
  }
1756
2099
  if (entry["perMachine"] === true) {
@@ -1764,7 +2107,7 @@ function parseManifest(text) {
1764
2107
  return { path: entry["path"], sha256: entry["sha256"] };
1765
2108
  });
1766
2109
  const s = raw["settings"];
1767
- if (!isRecord(s))
2110
+ if (!isRecord2(s))
1768
2111
  throw new UnreadableManifestError('"settings" is missing');
1769
2112
  onlyKeys(s, [
1770
2113
  "created",
@@ -1791,7 +2134,7 @@ function parseManifest(text) {
1791
2134
  let mcp;
1792
2135
  if (raw["mcp"] !== void 0) {
1793
2136
  const m = raw["mcp"];
1794
- if (!isRecord(m))
2137
+ if (!isRecord2(m))
1795
2138
  throw new UnreadableManifestError('"mcp" is not an object');
1796
2139
  onlyKeys(m, ["created", "adopted", "mcpServersCreated", "entrySha256"], '"mcp"');
1797
2140
  if (m["entrySha256"] !== void 0 && (typeof m["entrySha256"] !== "string" || !HEX64.test(m["entrySha256"]))) {
@@ -2097,6 +2440,8 @@ var WRITE_ALLOWLIST = [
2097
2440
  BUNDLE_PIN_REL,
2098
2441
  ".halfcycle/bin",
2099
2442
  "docs/build-records",
2443
+ // Architecture pictures the draw command writes from Halfcycle's answer. The installer never writes here; keep the entry so this path stays declared as one Halfcycle writes.
2444
+ "docs/diagrams",
2100
2445
  "test/fixtures/captured",
2101
2446
  MCP_REGISTRATION_REL,
2102
2447
  ".gitattributes",
@@ -3324,14 +3669,14 @@ async function bindLoopback(env = process.env, engagementId) {
3324
3669
  else
3325
3670
  pending = callback;
3326
3671
  }, engagementId));
3327
- const bindProblem = await new Promise((resolve4) => {
3672
+ const bindProblem = await new Promise((resolve5) => {
3328
3673
  const onError = (err) => {
3329
- resolve4(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
3674
+ resolve5(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
3330
3675
  };
3331
3676
  server.once("error", onError);
3332
3677
  server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
3333
3678
  server.removeListener("error", onError);
3334
- resolve4(null);
3679
+ resolve5(null);
3335
3680
  });
3336
3681
  });
3337
3682
  if (bindProblem !== null) {
@@ -3347,9 +3692,9 @@ async function bindLoopback(env = process.env, engagementId) {
3347
3692
  bound: true,
3348
3693
  target: { port: address.port, state },
3349
3694
  boundAddress: address.address,
3350
- waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve4) => {
3695
+ waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve5) => {
3351
3696
  if (pending !== null) {
3352
- resolve4(pending);
3697
+ resolve5(pending);
3353
3698
  return;
3354
3699
  }
3355
3700
  const startedAt = Date.now();
@@ -3360,7 +3705,7 @@ async function bindLoopback(env = process.env, engagementId) {
3360
3705
  clearTimeout(timer);
3361
3706
  deliver = null;
3362
3707
  abandon = null;
3363
- resolve4(value);
3708
+ resolve5(value);
3364
3709
  };
3365
3710
  const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
3366
3711
  deliver = finish;
@@ -3374,9 +3719,9 @@ async function bindLoopback(env = process.env, engagementId) {
3374
3719
  };
3375
3720
  }
3376
3721
  function closeServer(server) {
3377
- return new Promise((resolve4) => {
3722
+ return new Promise((resolve5) => {
3378
3723
  server.closeAllConnections();
3379
- server.close(() => resolve4());
3724
+ server.close(() => resolve5());
3380
3725
  });
3381
3726
  }
3382
3727
 
@@ -3399,7 +3744,7 @@ function defaultWrite(text) {
3399
3744
  process.stdout.write(text);
3400
3745
  }
3401
3746
  function defaultSleep(ms) {
3402
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3747
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3403
3748
  }
3404
3749
  function browserOpenCommand(os, url) {
3405
3750
  if (os === "darwin")
@@ -3446,12 +3791,12 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
3446
3791
  };
3447
3792
  }
3448
3793
  const { command, args: args2, windowsVerbatimArguments } = browserOpenCommand(os, url);
3449
- return new Promise((resolve4) => {
3794
+ return new Promise((resolve5) => {
3450
3795
  let child;
3451
3796
  try {
3452
3797
  child = spawnImpl(command, args2, { stdio: "ignore", env, windowsVerbatimArguments });
3453
3798
  } catch {
3454
- resolve4({ opened: false, reason: `${command} could not be run` });
3799
+ resolve5({ opened: false, reason: `${command} could not be run` });
3455
3800
  return;
3456
3801
  }
3457
3802
  let settled = false;
@@ -3460,7 +3805,7 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
3460
3805
  return;
3461
3806
  settled = true;
3462
3807
  clearTimeout(timer);
3463
- resolve4(attempt);
3808
+ resolve5(attempt);
3464
3809
  };
3465
3810
  const timer = setTimeout(() => {
3466
3811
  child.unref();
@@ -3605,17 +3950,17 @@ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
3605
3950
  continue;
3606
3951
  }
3607
3952
  if (sleep === void 0) {
3608
- await new Promise((resolve4) => {
3609
- const timer = setTimeout(resolve4, started.pollIntervalMs);
3953
+ await new Promise((resolve5) => {
3954
+ const timer = setTimeout(resolve5, started.pollIntervalMs);
3610
3955
  interrupt = () => {
3611
3956
  clearTimeout(timer);
3612
- resolve4();
3957
+ resolve5();
3613
3958
  };
3614
3959
  });
3615
3960
  } else {
3616
- await new Promise((resolve4) => {
3617
- interrupt = resolve4;
3618
- void sleep(started.pollIntervalMs).then(resolve4);
3961
+ await new Promise((resolve5) => {
3962
+ interrupt = resolve5;
3963
+ void sleep(started.pollIntervalMs).then(resolve5);
3619
3964
  });
3620
3965
  }
3621
3966
  interrupt = null;
@@ -4237,9 +4582,9 @@ function wrongAccountVisibility(act) {
4237
4582
  async function askLine(question2) {
4238
4583
  const rl = createInterface({ input: process.stdin, output: process.stdout });
4239
4584
  try {
4240
- return await new Promise((resolve4) => {
4241
- rl.once("close", () => resolve4(""));
4242
- rl.question(question2).then(resolve4, () => resolve4(""));
4585
+ return await new Promise((resolve5) => {
4586
+ rl.once("close", () => resolve5(""));
4587
+ rl.question(question2).then(resolve5, () => resolve5(""));
4243
4588
  });
4244
4589
  } finally {
4245
4590
  rl.close();
@@ -5098,10 +5443,14 @@ function assertInsideOutDir(outDir, candidate, identity) {
5098
5443
  throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
5099
5444
  }
5100
5445
  }
5101
- function createExclusiveOnDisk(path, text) {
5446
+ function createExclusiveOnDisk(path, data) {
5447
+ const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
5102
5448
  const fd = openSync(path, "wx");
5103
5449
  try {
5104
- writeSync(fd, text);
5450
+ let written = 0;
5451
+ while (written < bytes.length) {
5452
+ written += writeSync(fd, bytes, written, bytes.length - written);
5453
+ }
5105
5454
  } catch (err) {
5106
5455
  closeSync(fd);
5107
5456
  rmSync3(path, { force: true });
@@ -5218,6 +5567,22 @@ function replaceOne(io, tmp, finalPath, text) {
5218
5567
 
5219
5568
  // dist/build-record/obtain.js
5220
5569
  import { relative as relative3 } from "node:path";
5570
+
5571
+ // dist/missing-route.js
5572
+ var MESSAGE = {
5573
+ GET: /^Route GET .* not found\.$/s,
5574
+ POST: /^Route POST .* not found\.$/s
5575
+ };
5576
+ function isMissingRoute(method, status, body) {
5577
+ if (status !== 404 || typeof body !== "object" || body === null)
5578
+ return false;
5579
+ const { error, message } = body;
5580
+ if (error !== "Not Found" || typeof message !== "string")
5581
+ return false;
5582
+ return MESSAGE[method].test(message);
5583
+ }
5584
+
5585
+ // dist/build-record/obtain.js
5221
5586
  var REFUSAL_FOLLOW_UP = {
5222
5587
  "still-open": "Nothing was written. The record is produced when the phase closes.",
5223
5588
  "reopened": "Nothing was written. A new record is produced when the phase closes again.",
@@ -5229,12 +5594,6 @@ var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
5229
5594
  function buildRecordUrl(credential, phase) {
5230
5595
  return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
5231
5596
  }
5232
- function isMissingRoute(status, body) {
5233
- if (status !== 404 || typeof body !== "object" || body === null)
5234
- return false;
5235
- const { error, message } = body;
5236
- return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
5237
- }
5238
5597
  async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
5239
5598
  const url = buildRecordUrl(credential, phase);
5240
5599
  let res;
@@ -5291,7 +5650,7 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
5291
5650
  headline: latestCloseHeadline(answer.record)
5292
5651
  };
5293
5652
  }
5294
- if (isMissingRoute(res.status, raw)) {
5653
+ if (isMissingRoute("GET", res.status, raw)) {
5295
5654
  return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
5296
5655
  }
5297
5656
  const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
@@ -5532,12 +5891,705 @@ function describeIssuePath(path) {
5532
5891
  return path.map(String).join(".") || "(the close)";
5533
5892
  }
5534
5893
 
5894
+ // dist/system-report.js
5895
+ import { execFileSync as execFileSync5 } from "node:child_process";
5896
+ import { readFileSync as readFileSync9 } from "node:fs";
5897
+ import { join as join10 } from "node:path";
5898
+ var SYSTEM_DESCRIPTION_REL = "docs/system.json";
5899
+ function readDescriptionFile(repoRoot) {
5900
+ let text;
5901
+ try {
5902
+ text = readFileSync9(join10(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
5903
+ } catch (err) {
5904
+ const code = err.code;
5905
+ return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
5906
+ }
5907
+ try {
5908
+ return { kind: "read", value: JSON.parse(text) };
5909
+ } catch (err) {
5910
+ return { kind: "not-json", error: err instanceof Error ? err.message : String(err) };
5911
+ }
5912
+ }
5913
+ function problemPlace(raw, path) {
5914
+ if (path === "")
5915
+ return SYSTEM_DESCRIPTION_REL;
5916
+ const [list, index] = path.split(".");
5917
+ const position = Number(index);
5918
+ if (!Number.isInteger(position) || position < 0)
5919
+ return path;
5920
+ const items = typeof raw === "object" && raw !== null ? raw[list ?? ""] : void 0;
5921
+ const item = Array.isArray(items) ? items[position] : void 0;
5922
+ const fields = typeof item === "object" && item !== null ? item : {};
5923
+ if (list === "parts") {
5924
+ const id = fields["id"];
5925
+ return typeof id === "string" && id !== "" ? `${path} (part "${id}")` : `${path} (part ${position + 1})`;
5926
+ }
5927
+ if (list === "connections") {
5928
+ const from = fields["from"];
5929
+ const to = fields["to"];
5930
+ return typeof from === "string" && typeof to === "string" ? `${path} (connection "${from}" to "${to}")` : `${path} (connection ${position + 1})`;
5931
+ }
5932
+ return path;
5933
+ }
5934
+ function problemLines(raw, problems) {
5935
+ return problems.map((p) => ` ${problemPlace(raw, p.path)}: ${p.message}
5936
+ `).join("");
5937
+ }
5938
+ var listTrackedFiles = (repoRoot) => {
5939
+ try {
5940
+ const out = execFileSync5("git", ["-C", repoRoot, "ls-files", "-z"], {
5941
+ encoding: "utf-8",
5942
+ env: gitEnvForNamedRepo(),
5943
+ stdio: ["ignore", "pipe", "ignore"],
5944
+ maxBuffer: 256 * 1024 * 1024
5945
+ });
5946
+ return out.split("\0").filter((p) => p !== "");
5947
+ } catch {
5948
+ return null;
5949
+ }
5950
+ };
5951
+ function unownedPatterns(description, tracked) {
5952
+ if (tracked === null)
5953
+ return null;
5954
+ const found = [];
5955
+ for (const part of description.parts) {
5956
+ for (const pattern of part.owns) {
5957
+ const alone = {
5958
+ format: description.format,
5959
+ parts: [{ ...part, owns: [pattern] }],
5960
+ connections: []
5961
+ };
5962
+ if (!tracked.some((path) => ownerOf(path, alone) !== null))
5963
+ found.push({ partId: part.id, pattern });
5964
+ }
5965
+ }
5966
+ return found;
5967
+ }
5968
+ function unownedPatternLines(unowned) {
5969
+ return unowned.map((u) => `halfcycle system: warning: "${u.pattern}" (part "${u.partId}") matches no file in this repository. Correct the path, or remove it if the code moved.
5970
+ `).join("");
5971
+ }
5972
+ function systemReportUrl(credential) {
5973
+ return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/system`;
5974
+ }
5975
+ function serviceHost(serviceUrl) {
5976
+ try {
5977
+ return new URL(serviceUrl).host;
5978
+ } catch {
5979
+ return serviceUrl;
5980
+ }
5981
+ }
5982
+ async function reportSystemDescription(credential, description) {
5983
+ let res;
5984
+ let text;
5985
+ try {
5986
+ res = await fetch(systemReportUrl(credential), {
5987
+ method: "POST",
5988
+ headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
5989
+ body: JSON.stringify(description)
5990
+ });
5991
+ text = await res.text();
5992
+ } catch {
5993
+ return {
5994
+ kind: "unavailable",
5995
+ why: `could not reach Halfcycle at ${serviceHost(credential.serviceUrl)}. Check this machine's connection, then run this again.`
5996
+ };
5997
+ }
5998
+ let raw = null;
5999
+ try {
6000
+ raw = JSON.parse(text);
6001
+ } catch {
6002
+ raw = null;
6003
+ }
6004
+ const parsed = systemReportResultSchema.safeParse(raw);
6005
+ if (parsed.success) {
6006
+ const answer = parsed.data;
6007
+ if (answer.outcome === "recorded") {
6008
+ return { kind: "recorded", version: answer.version, reportedAt: answer.reportedAt };
6009
+ }
6010
+ if (answer.outcome === "refused")
6011
+ return { kind: "refused", problems: answer.problems };
6012
+ return {
6013
+ kind: "unavailable",
6014
+ why: "Halfcycle could not record it just now. Nothing was recorded; run this again in a minute."
6015
+ };
6016
+ }
6017
+ if (isMissingRoute("POST", res.status, raw))
6018
+ return { kind: "no-route" };
6019
+ const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
6020
+ if (res.ok) {
6021
+ return {
6022
+ kind: "unavailable",
6023
+ why: "Halfcycle sent an answer this command could not read. Nothing was recorded; try again later."
6024
+ };
6025
+ }
6026
+ return {
6027
+ kind: "unavailable",
6028
+ why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`}`
6029
+ };
6030
+ }
6031
+ async function checkAndReportDescription(repoRoot, deps, verb, beforeSend = () => null) {
6032
+ const p = `halfcycle ${verb}:`;
6033
+ const file = readDescriptionFile(repoRoot);
6034
+ if (file.kind === "missing") {
6035
+ return {
6036
+ kind: "stopped",
6037
+ stderr: `${p} there is no ${SYSTEM_DESCRIPTION_REL} in this project yet. Run the Layer 2 architecture step to write it, then run this again.
6038
+ `
6039
+ };
6040
+ }
6041
+ if (file.kind === "unreadable") {
6042
+ return {
6043
+ kind: "stopped",
6044
+ stderr: `${p} ${SYSTEM_DESCRIPTION_REL} could not be read. Check that it is a file you can open, then run this again.
6045
+ `
6046
+ };
6047
+ }
6048
+ if (file.kind === "not-json") {
6049
+ return {
6050
+ kind: "stopped",
6051
+ stderr: `${p} ${SYSTEM_DESCRIPTION_REL} is not valid JSON (${file.error}). Fix it, then run this again. Nothing was sent.
6052
+ `
6053
+ };
6054
+ }
6055
+ const checked = checkSystemDescription(file.value);
6056
+ if (!checked.ok) {
6057
+ const n = checked.problems.length;
6058
+ return {
6059
+ kind: "stopped",
6060
+ stderr: `${p} ${SYSTEM_DESCRIPTION_REL} has ${n} ${n === 1 ? "problem" : "problems"}, so nothing was sent:
6061
+ ` + problemLines(file.value, checked.problems)
6062
+ };
6063
+ }
6064
+ const unowned = unownedPatterns(checked.description, deps.listTracked(repoRoot));
6065
+ const warnings = unowned === null ? "" : unownedPatternLines(unowned);
6066
+ const refusal = beforeSend();
6067
+ if (refusal !== null)
6068
+ return { kind: "stopped", stderr: warnings + refusal };
6069
+ let credential;
6070
+ try {
6071
+ credential = deps.readCredential(repoRoot);
6072
+ } catch {
6073
+ return {
6074
+ kind: "stopped",
6075
+ stderr: warnings + `${p} this project is not connected to Halfcycle on this machine. Run ${HALFCYCLE_COMMAND} here first, then run this again. Nothing was sent.
6076
+ `
6077
+ };
6078
+ }
6079
+ const outcome = await deps.report(credential, checked.description);
6080
+ switch (outcome.kind) {
6081
+ case "recorded":
6082
+ return { kind: "recorded", version: outcome.version, credential, warnings };
6083
+ case "refused": {
6084
+ const n = outcome.problems.length;
6085
+ return {
6086
+ kind: "stopped",
6087
+ stderr: warnings + `${p} Halfcycle refused ${SYSTEM_DESCRIPTION_REL} (${n} ${n === 1 ? "problem" : "problems"}); nothing was recorded:
6088
+ ` + problemLines(file.value, outcome.problems)
6089
+ };
6090
+ }
6091
+ case "no-route":
6092
+ return { kind: "no-route", warnings };
6093
+ case "unavailable":
6094
+ return { kind: "stopped", stderr: warnings + `${p} ${outcome.why}
6095
+ ` };
6096
+ }
6097
+ }
6098
+ async function runSystem(repoRoot, deps) {
6099
+ const stage = await checkAndReportDescription(repoRoot, deps, "system");
6100
+ switch (stage.kind) {
6101
+ case "stopped":
6102
+ return { stdout: "", stderr: stage.stderr, exitCode: 1 };
6103
+ case "no-route":
6104
+ return {
6105
+ stdout: "",
6106
+ stderr: stage.warnings + "halfcycle system: this Halfcycle service cannot take a system description yet. Nothing was recorded.\n",
6107
+ exitCode: 1
6108
+ };
6109
+ case "recorded":
6110
+ return {
6111
+ stdout: `[halfcycle] System description recorded: version ${stage.version}.
6112
+ `,
6113
+ stderr: stage.warnings,
6114
+ exitCode: 0
6115
+ };
6116
+ }
6117
+ }
6118
+
6119
+ // dist/draw/write.js
6120
+ import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync10, readSync, realpathSync as realpathSync3, rmSync as rmSync4 } from "node:fs";
6121
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join11, relative as relative4, resolve as resolve3, sep } from "node:path";
6122
+ var DOCS_DIR = "docs";
6123
+ var DIAGRAMS_DIR = "diagrams";
6124
+ var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
6125
+ var ARCHITECTURE_NAME = "architecture";
6126
+ var PICTURE_NAMES = {
6127
+ broadsheet: ARCHITECTURE_NAME,
6128
+ flightdeck: `${ARCHITECTURE_NAME}.flightdeck`
6129
+ };
6130
+ var EMBEDDED_LOOK = "broadsheet";
6131
+ var DIAGRAM_MARKER_LINE = SYSTEM_DIAGRAM_MARKER_LINE;
6132
+ var PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
6133
+ function svgOf(look) {
6134
+ return `${look}.svg`;
6135
+ }
6136
+ function pngOf(look) {
6137
+ return `${look}.png`;
6138
+ }
6139
+ var DIAGRAM_FILES = [
6140
+ ...SYSTEM_PICTURE_THEMES.map(svgOf),
6141
+ ...SYSTEM_PICTURE_THEMES.map(pngOf),
6142
+ "mmd"
6143
+ ];
6144
+ var FILE_NAMES = Object.fromEntries([
6145
+ ...SYSTEM_PICTURE_THEMES.flatMap((look) => [
6146
+ [svgOf(look), `${PICTURE_NAMES[look]}.svg`],
6147
+ [pngOf(look), `${PICTURE_NAMES[look]}.png`]
6148
+ ]),
6149
+ ["mmd", `${ARCHITECTURE_NAME}.mmd`]
6150
+ ]);
6151
+ var EMBED_IMAGE_REL = `${DIAGRAMS_DIR}/${FILE_NAMES[pngOf(EMBEDDED_LOOK)]}`;
6152
+ function diagramPaths(repoRoot) {
6153
+ const root = resolve3(repoRoot);
6154
+ const dir = join11(root, DOCS_DIR, DIAGRAMS_DIR);
6155
+ const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join11(dir, FILE_NAMES[which])]));
6156
+ return { repoRoot: root, dir, file };
6157
+ }
6158
+ function diagramRel(which) {
6159
+ return `${DIAGRAM_DIR_REL}/${FILE_NAMES[which]}`;
6160
+ }
6161
+ var DIAGRAM_FILES_AS_LISTED = [
6162
+ ...SYSTEM_PICTURE_THEMES.flatMap((look) => [pngOf(look), svgOf(look)]),
6163
+ "mmd"
6164
+ ];
6165
+ function spokenList(items) {
6166
+ if (items.length <= 1)
6167
+ return items.join("");
6168
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
6169
+ }
6170
+ function lstatOrNull2(path) {
6171
+ try {
6172
+ return lstatSync2(path);
6173
+ } catch {
6174
+ return null;
6175
+ }
6176
+ }
6177
+ function isInside(outer, inner) {
6178
+ const rel = relative4(outer, inner);
6179
+ return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
6180
+ }
6181
+ function checkDiagramPlace(paths) {
6182
+ let root;
6183
+ try {
6184
+ root = realpathSync3(paths.repoRoot);
6185
+ } catch {
6186
+ return { ok: false, why: `the folder ${paths.repoRoot} could not be opened. Check the path, then run this again.` };
6187
+ }
6188
+ let ancestor = paths.dir;
6189
+ while (lstatOrNull2(ancestor) === null) {
6190
+ const parent = dirname5(ancestor);
6191
+ if (parent === ancestor)
6192
+ break;
6193
+ ancestor = parent;
6194
+ }
6195
+ let real;
6196
+ try {
6197
+ real = realpathSync3(ancestor);
6198
+ } catch {
6199
+ return {
6200
+ ok: false,
6201
+ why: `${DIAGRAM_DIR_REL} leads to a place that does not exist. Remove the link in the way, then run this again.`
6202
+ };
6203
+ }
6204
+ if (!isInside(root, real)) {
6205
+ return {
6206
+ ok: false,
6207
+ why: `${DIAGRAM_DIR_REL} leads outside this repository, so nothing was written. Remove the link in the way, then run this again.`
6208
+ };
6209
+ }
6210
+ let isFolder = false;
6211
+ try {
6212
+ isFolder = lstatSync2(real).isDirectory();
6213
+ } catch {
6214
+ isFolder = false;
6215
+ }
6216
+ if (!isFolder) {
6217
+ const name = relative4(root, real).split("\\").join("/");
6218
+ return {
6219
+ ok: false,
6220
+ why: `${name === "" ? "this repository" : name} is not a folder, so ${DIAGRAM_DIR_REL} cannot be made. Move it, then run this again.`
6221
+ };
6222
+ }
6223
+ return { ok: true };
6224
+ }
6225
+ function startsWithMarkerLine(bytes) {
6226
+ const head = Buffer.from(bytes.subarray(0, DIAGRAM_MARKER_LINE.length + 2)).toString("utf-8");
6227
+ return head.startsWith(`${DIAGRAM_MARKER_LINE}
6228
+ `) || head.startsWith(`${DIAGRAM_MARKER_LINE}\r
6229
+ `);
6230
+ }
6231
+ function readHead(path) {
6232
+ const fd = openSync2(path, "r");
6233
+ try {
6234
+ const head = Buffer.alloc(DIAGRAM_MARKER_LINE.length + 2);
6235
+ const got = readSync(fd, head, 0, head.length, 0);
6236
+ return head.subarray(0, got);
6237
+ } finally {
6238
+ closeSync2(fd);
6239
+ }
6240
+ }
6241
+ function readExistingSet(paths) {
6242
+ const found = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, null]));
6243
+ for (const which of DIAGRAM_FILES) {
6244
+ const stat = lstatOrNull2(paths.file[which]);
6245
+ if (stat === null)
6246
+ continue;
6247
+ if (!stat.isFile())
6248
+ return { ok: false, inTheWay: which, temp: false };
6249
+ try {
6250
+ found[which] = readFileSync10(paths.file[which]);
6251
+ } catch {
6252
+ return { ok: false, inTheWay: which, temp: false };
6253
+ }
6254
+ }
6255
+ for (const look of SYSTEM_PICTURE_THEMES) {
6256
+ const svg = found[svgOf(look)];
6257
+ if (svg !== null && !startsWithMarkerLine(svg))
6258
+ return { ok: false, inTheWay: svgOf(look), temp: false };
6259
+ }
6260
+ for (const look of SYSTEM_PICTURE_THEMES) {
6261
+ if (found[pngOf(look)] !== null && found[svgOf(look)] === null)
6262
+ return { ok: false, inTheWay: pngOf(look), temp: false };
6263
+ }
6264
+ if (found.mmd !== null && found[svgOf(EMBEDDED_LOOK)] === null)
6265
+ return { ok: false, inTheWay: "mmd", temp: false };
6266
+ const temp = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, null]));
6267
+ for (const which of DIAGRAM_FILES) {
6268
+ const stat = lstatOrNull2(tempPath(paths, which));
6269
+ if (stat === null)
6270
+ continue;
6271
+ if (!stat.isFile())
6272
+ return { ok: false, inTheWay: which, temp: true };
6273
+ try {
6274
+ temp[which] = readHead(tempPath(paths, which));
6275
+ } catch {
6276
+ return { ok: false, inTheWay: which, temp: true };
6277
+ }
6278
+ }
6279
+ for (const look of SYSTEM_PICTURE_THEMES) {
6280
+ const svg = temp[svgOf(look)];
6281
+ if (svg !== null && !startsWithMarkerLine(svg))
6282
+ return { ok: false, inTheWay: svgOf(look), temp: true };
6283
+ }
6284
+ const markedSvgBeside = (look) => found[svgOf(look)] !== null || temp[svgOf(look)] !== null;
6285
+ for (const look of SYSTEM_PICTURE_THEMES) {
6286
+ if (temp[pngOf(look)] !== null && !markedSvgBeside(look))
6287
+ return { ok: false, inTheWay: pngOf(look), temp: true };
6288
+ }
6289
+ if (temp.mmd !== null && !markedSvgBeside(EMBEDDED_LOOK))
6290
+ return { ok: false, inTheWay: "mmd", temp: true };
6291
+ return { ok: true, existing: found, ourTemps: DIAGRAM_FILES.filter((which) => temp[which] !== null) };
6292
+ }
6293
+ function tempPath(paths, which) {
6294
+ return `${paths.file[which]}.tmp`;
6295
+ }
6296
+ function inTheWayLine(which, temp = false) {
6297
+ return `${diagramRel(which)}${temp ? ".tmp" : ""} is already there and was not drawn by this command. Move it or delete it, then run this again. Nothing was written.`;
6298
+ }
6299
+ function checkPictures(drawn) {
6300
+ const set = {};
6301
+ for (const look of SYSTEM_PICTURE_THEMES) {
6302
+ const picture = drawn.pictures[look];
6303
+ const pngFor = diagramRel(pngOf(look));
6304
+ const svgFor = diagramRel(svgOf(look));
6305
+ const png = Buffer.from(picture.png, "base64");
6306
+ if (png.length > SYSTEM_DRAW_PNG_MAX_BYTES)
6307
+ return { ok: false, why: `the image for ${pngFor} is larger than 8 MB` };
6308
+ if (png.length < PNG_SIGNATURE.length || !PNG_SIGNATURE.every((b, i) => png[i] === b)) {
6309
+ return { ok: false, why: `the image for ${pngFor} is not a PNG` };
6310
+ }
6311
+ const svg = Buffer.from(picture.svg, "utf-8");
6312
+ if (svg.length > SYSTEM_DRAW_SVG_MAX_BYTES)
6313
+ return { ok: false, why: `the picture for ${svgFor} is larger than 2 MB` };
6314
+ const body = picture.svg.endsWith("\n") ? picture.svg.slice(0, -1) : picture.svg;
6315
+ if (!picture.svg.startsWith(`${DIAGRAM_MARKER_LINE}
6316
+ <svg`) || !body.endsWith("</svg>")) {
6317
+ return { ok: false, why: `the picture for ${svgFor} is not a complete SVG drawn by Halfcycle` };
6318
+ }
6319
+ set[pngOf(look)] = png;
6320
+ set[svgOf(look)] = svg;
6321
+ }
6322
+ const mmd = Buffer.from(drawn.mermaid, "utf-8");
6323
+ if (mmd.length > SYSTEM_DRAW_MERMAID_MAX_BYTES)
6324
+ return { ok: false, why: "the diagram text is larger than 256 KB" };
6325
+ if (!drawn.mermaid.startsWith("flowchart"))
6326
+ return { ok: false, why: "the diagram text is not a Mermaid flowchart" };
6327
+ set.mmd = mmd;
6328
+ return { ok: true, set };
6329
+ }
6330
+ function removeQuietly(path) {
6331
+ try {
6332
+ rmSync4(path, { force: true });
6333
+ } catch {
6334
+ return;
6335
+ }
6336
+ }
6337
+ function sameBytes(a, b) {
6338
+ return a !== null && Buffer.from(a).equals(b);
6339
+ }
6340
+ function writeDiagramSet(repoRoot, set, io = NODE_RECORD_PAIR_IO) {
6341
+ const paths = diagramPaths(repoRoot);
6342
+ const place = checkDiagramPlace(paths);
6343
+ if (!place.ok)
6344
+ return { kind: "unsafe", why: place.why };
6345
+ const read = readExistingSet(paths);
6346
+ if (!read.ok)
6347
+ return { kind: "in-the-way", file: read.inTheWay, temp: read.temp };
6348
+ const existing = read.existing;
6349
+ const changed = DIAGRAM_FILES.filter((which) => !sameBytes(existing[which], set[which]));
6350
+ if (changed.length === 0)
6351
+ return { kind: "up-to-date" };
6352
+ try {
6353
+ mkdirSync7(paths.dir, { recursive: true });
6354
+ } catch {
6355
+ return { kind: "failed", file: changed[0], removed: [] };
6356
+ }
6357
+ const made = checkDiagramPlace(paths);
6358
+ if (!made.ok)
6359
+ return { kind: "unsafe", why: made.why };
6360
+ const tmp = (which) => tempPath(paths, which);
6361
+ for (const which of read.ourTemps)
6362
+ removeQuietly(tmp(which));
6363
+ const staged = [];
6364
+ const clearStaged = () => {
6365
+ for (const which of staged)
6366
+ removeQuietly(tmp(which));
6367
+ };
6368
+ for (const which of changed) {
6369
+ try {
6370
+ io.createExclusive(tmp(which), set[which]);
6371
+ staged.push(which);
6372
+ } catch {
6373
+ clearStaged();
6374
+ return { kind: "failed", file: which, removed: [] };
6375
+ }
6376
+ }
6377
+ const renamed = [];
6378
+ for (const which of changed) {
6379
+ try {
6380
+ io.rename(tmp(which), paths.file[which]);
6381
+ renamed.push(which);
6382
+ } catch {
6383
+ clearStaged();
6384
+ const removed = restore(io, paths, renamed, existing);
6385
+ return { kind: "failed", file: which, removed };
6386
+ }
6387
+ }
6388
+ return { kind: "written", changed };
6389
+ }
6390
+ function restore(io, paths, renamed, existing) {
6391
+ const removed = [];
6392
+ for (const which of [...renamed].reverse()) {
6393
+ const finalPath = paths.file[which];
6394
+ const previous = existing[which];
6395
+ const tmp = tempPath(paths, which);
6396
+ try {
6397
+ if (previous === null) {
6398
+ rmSync4(finalPath, { force: true });
6399
+ continue;
6400
+ }
6401
+ rmSync4(tmp, { force: true });
6402
+ io.createExclusive(tmp, previous);
6403
+ io.rename(tmp, finalPath);
6404
+ } catch {
6405
+ removeQuietly(tmp);
6406
+ removeQuietly(finalPath);
6407
+ removed.push(which);
6408
+ }
6409
+ }
6410
+ return removed;
6411
+ }
6412
+
6413
+ // dist/draw/index.js
6414
+ var DRAW_USAGE = `${HALFCYCLE_COMMAND} draw architecture [--repo <root>]`;
6415
+ function parseDrawArgs(argv) {
6416
+ const positionals = [];
6417
+ let repoRoot;
6418
+ for (let i = 0; i < argv.length; i++) {
6419
+ const arg = argv[i];
6420
+ if (arg === "--repo") {
6421
+ const value = argv[i + 1];
6422
+ if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
6423
+ return { ok: false };
6424
+ repoRoot = value;
6425
+ i++;
6426
+ continue;
6427
+ }
6428
+ if (arg.startsWith("-"))
6429
+ return { ok: false };
6430
+ positionals.push(arg);
6431
+ }
6432
+ if (positionals.length !== 1 || positionals[0] !== "architecture")
6433
+ return { ok: false };
6434
+ return { ok: true, repoRoot };
6435
+ }
6436
+ function systemDrawUrl(credential) {
6437
+ return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/system/draw`;
6438
+ }
6439
+ function serviceHost2(serviceUrl) {
6440
+ try {
6441
+ return new URL(serviceUrl).host;
6442
+ } catch {
6443
+ return serviceUrl;
6444
+ }
6445
+ }
6446
+ async function requestDrawing(credential, version) {
6447
+ const body = { version, picture: "architecture" };
6448
+ let res;
6449
+ let text;
6450
+ try {
6451
+ res = await fetch(systemDrawUrl(credential), {
6452
+ method: "POST",
6453
+ headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
6454
+ body: JSON.stringify(body)
6455
+ });
6456
+ text = await res.text();
6457
+ } catch {
6458
+ return {
6459
+ kind: "unavailable",
6460
+ why: `could not reach Halfcycle at ${serviceHost2(credential.serviceUrl)}. Check this machine's connection, then run this again. Nothing was written.`
6461
+ };
6462
+ }
6463
+ let raw = null;
6464
+ try {
6465
+ raw = JSON.parse(text);
6466
+ } catch {
6467
+ raw = null;
6468
+ }
6469
+ const parsed = systemDrawResultSchema.safeParse(raw);
6470
+ if (parsed.success) {
6471
+ const answer = parsed.data;
6472
+ if (answer.outcome === "drawn")
6473
+ return { kind: "drawn", drawn: answer };
6474
+ if (answer.outcome === "refused")
6475
+ return { kind: "refused", reason: answer.reason };
6476
+ return {
6477
+ kind: "unavailable",
6478
+ why: "Halfcycle could not draw the pictures just now. Nothing was written; run this again in a minute."
6479
+ };
6480
+ }
6481
+ if (isMissingRoute("POST", res.status, raw))
6482
+ return { kind: "no-route" };
6483
+ if (res.ok) {
6484
+ return {
6485
+ kind: "unavailable",
6486
+ why: "Halfcycle sent an answer this command could not read. Nothing was written; try again later."
6487
+ };
6488
+ }
6489
+ const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
6490
+ return {
6491
+ kind: "unavailable",
6492
+ why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`} Nothing was written.`
6493
+ };
6494
+ }
6495
+ var P = "halfcycle draw:";
6496
+ var CANNOT_DRAW_YET = `${P} this Halfcycle service cannot draw pictures yet. Nothing was written.
6497
+ `;
6498
+ function pasteLines(mermaid) {
6499
+ const block = mermaid.endsWith("\n") ? mermaid : `${mermaid}
6500
+ `;
6501
+ return `
6502
+ Paste this into a document directly under docs/:
6503
+
6504
+ ![Architecture](${EMBED_IMAGE_REL})
6505
+
6506
+ \`\`\`mermaid
6507
+ ` + block + "```\n";
6508
+ }
6509
+ function localRefusal(repoRoot) {
6510
+ const paths = diagramPaths(repoRoot);
6511
+ const place = checkDiagramPlace(paths);
6512
+ if (!place.ok)
6513
+ return `${P} ${place.why}
6514
+ `;
6515
+ const read = readExistingSet(paths);
6516
+ if (!read.ok)
6517
+ return `${P} ${inTheWayLine(read.inTheWay, read.temp)}
6518
+ `;
6519
+ return null;
6520
+ }
6521
+ async function runDraw(repoRoot, deps) {
6522
+ const stage = await checkAndReportDescription(repoRoot, deps, "draw", () => localRefusal(repoRoot));
6523
+ if (stage.kind === "stopped")
6524
+ return { stdout: "", stderr: stage.stderr, exitCode: 1 };
6525
+ if (stage.kind === "no-route")
6526
+ return { stdout: "", stderr: stage.warnings + CANNOT_DRAW_YET, exitCode: 1 };
6527
+ const warnings = stage.warnings;
6528
+ const outcome = await deps.draw(stage.credential, stage.version);
6529
+ if (outcome.kind === "no-route")
6530
+ return { stdout: "", stderr: warnings + CANNOT_DRAW_YET, exitCode: 1 };
6531
+ if (outcome.kind === "refused") {
6532
+ return {
6533
+ stdout: "",
6534
+ stderr: warnings + `${P} Halfcycle would not draw the pictures: ${outcome.reason} Nothing was written.
6535
+ `,
6536
+ exitCode: 1
6537
+ };
6538
+ }
6539
+ if (outcome.kind === "unavailable")
6540
+ return { stdout: "", stderr: warnings + `${P} ${outcome.why}
6541
+ `, exitCode: 1 };
6542
+ const checked = checkPictures(outcome.drawn);
6543
+ if (!checked.ok) {
6544
+ return {
6545
+ stdout: "",
6546
+ stderr: warnings + `${P} Halfcycle sent pictures this command cannot use (${checked.why}). Nothing was written; try again later.
6547
+ `,
6548
+ exitCode: 1
6549
+ };
6550
+ }
6551
+ const result = deps.write(repoRoot, checked.set);
6552
+ switch (result.kind) {
6553
+ case "up-to-date":
6554
+ return {
6555
+ stdout: `[halfcycle] The pictures in ${DIAGRAM_DIR_REL}/ are up to date.
6556
+ ` + pasteLines(outcome.drawn.mermaid),
6557
+ stderr: warnings,
6558
+ exitCode: 0
6559
+ };
6560
+ case "written":
6561
+ return {
6562
+ stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(diagramRel))}.
6563
+ ` + pasteLines(outcome.drawn.mermaid),
6564
+ stderr: warnings,
6565
+ exitCode: 0
6566
+ };
6567
+ case "in-the-way":
6568
+ return { stdout: "", stderr: warnings + `${P} ${inTheWayLine(result.file, result.temp)}
6569
+ `, exitCode: 1 };
6570
+ case "unsafe":
6571
+ return { stdout: "", stderr: warnings + `${P} ${result.why}
6572
+ `, exitCode: 1 };
6573
+ case "failed": {
6574
+ const lost = result.removed.length === 0 ? "The pictures already there are as they were." : `${spokenList(result.removed.map(diagramRel))} could not be put back as ${result.removed.length === 1 ? "it was, so it was" : "they were, so they were"} removed; the other pictures are as they were.`;
6575
+ return {
6576
+ stdout: "",
6577
+ stderr: warnings + `${P} ${diagramRel(result.file)} could not be written. ${lost} Check that you can write to ${DIAGRAM_DIR_REL}, then run this again.
6578
+ `,
6579
+ exitCode: 1
6580
+ };
6581
+ }
6582
+ }
6583
+ }
6584
+
5535
6585
  // dist/cli-contract.js
5536
- var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "uninstall"];
6586
+ var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "uninstall"];
5537
6587
  var CONTRACTS = [
5538
6588
  { verb: "install", required: [], conditional: [] },
5539
6589
  { verb: "check-drift", required: [], conditional: [] },
5540
6590
  { verb: "build-record", required: [], conditional: [] },
6591
+ { verb: "system", required: [], conditional: [] },
6592
+ { verb: "draw", required: [], conditional: [] },
5541
6593
  { verb: "uninstall", required: [], conditional: [] },
5542
6594
  {
5543
6595
  verb: "open-phase",
@@ -5674,9 +6726,9 @@ function placeholderRefusal(verb, tokens) {
5674
6726
  }
5675
6727
 
5676
6728
  // dist/uninstall.js
5677
- import { lstatSync as lstatSync2, readFileSync as readFileSync9, readdirSync, rmSync as rmSync4, rmdirSync, writeFileSync as writeFileSync6 } from "node:fs";
6729
+ import { lstatSync as lstatSync3, readFileSync as readFileSync11, readdirSync, rmSync as rmSync5, rmdirSync, writeFileSync as writeFileSync6 } from "node:fs";
5678
6730
  import { homedir as homedir2 } from "node:os";
5679
- import { dirname as dirname5, join as join10, relative as relative4, sep } from "node:path";
6731
+ import { dirname as dirname6, join as join12, relative as relative5, sep as sep2 } from "node:path";
5680
6732
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
5681
6733
  function parseUninstallArgs(args2) {
5682
6734
  let removeCredential = false;
@@ -5693,7 +6745,7 @@ function parseUninstallArgs(args2) {
5693
6745
  }
5694
6746
  function lexists(path) {
5695
6747
  try {
5696
- lstatSync2(path);
6748
+ lstatSync3(path);
5697
6749
  return true;
5698
6750
  } catch {
5699
6751
  return false;
@@ -5702,9 +6754,9 @@ function lexists(path) {
5702
6754
  function symlinkOnPath(root, rel) {
5703
6755
  let current = root;
5704
6756
  for (const segment of rel.split("/")) {
5705
- current = join10(current, segment);
6757
+ current = join12(current, segment);
5706
6758
  try {
5707
- if (lstatSync2(current).isSymbolicLink())
6759
+ if (lstatSync3(current).isSymbolicLink())
5708
6760
  return true;
5709
6761
  } catch {
5710
6762
  return false;
@@ -5724,13 +6776,13 @@ function readMergedJson(targetRepo, rule) {
5724
6776
  const { rel } = rule;
5725
6777
  if (symlinkOnPath(targetRepo, rel))
5726
6778
  return { state: "symlink" };
5727
- const path = join10(targetRepo, rel);
6779
+ const path = join12(targetRepo, rel);
5728
6780
  if (!lexists(path))
5729
6781
  return { state: "absent" };
5730
6782
  let text;
5731
6783
  let value;
5732
6784
  try {
5733
- text = readFileSync9(path, "utf-8");
6785
+ text = readFileSync11(path, "utf-8");
5734
6786
  } catch {
5735
6787
  text = "";
5736
6788
  }
@@ -5747,15 +6799,15 @@ function readMergedJson(targetRepo, rule) {
5747
6799
  }
5748
6800
  function validate(options, rules) {
5749
6801
  const { targetRepo, removeCredential, home } = options;
5750
- const manifestPath = join10(targetRepo, INSTALL_MANIFEST_REL);
5751
- const pinPath = join10(targetRepo, BUNDLE_PIN_REL);
6802
+ const manifestPath = join12(targetRepo, INSTALL_MANIFEST_REL);
6803
+ const pinPath = join12(targetRepo, BUNDLE_PIN_REL);
5752
6804
  if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
5753
6805
  throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
5754
6806
  }
5755
6807
  let manifest = null;
5756
6808
  if (lexists(manifestPath)) {
5757
6809
  try {
5758
- manifest = parseManifest(readFileSync9(manifestPath, "utf-8"));
6810
+ manifest = parseManifest(readFileSync11(manifestPath, "utf-8"));
5759
6811
  } catch (err) {
5760
6812
  const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
5761
6813
  throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
@@ -5765,7 +6817,7 @@ function validate(options, rules) {
5765
6817
  let pinReadable = true;
5766
6818
  if (lexists(pinPath)) {
5767
6819
  try {
5768
- const pin = JSON.parse(readFileSync9(pinPath, "utf-8"));
6820
+ const pin = JSON.parse(readFileSync11(pinPath, "utf-8"));
5769
6821
  const id = isPlainObject2(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
5770
6822
  engagementId = typeof id === "string" && id !== "" ? id : void 0;
5771
6823
  } catch {
@@ -5789,8 +6841,8 @@ function validate(options, rules) {
5789
6841
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
5790
6842
  }
5791
6843
  if (engagementId !== void 0) {
5792
- const expectedParent = join10(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
5793
- if (dirname5(engagementStateDir(engagementId, home)) !== expectedParent) {
6844
+ const expectedParent = join12(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
6845
+ if (dirname6(engagementStateDir(engagementId, home)) !== expectedParent) {
5794
6846
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
5795
6847
  }
5796
6848
  }
@@ -5813,10 +6865,10 @@ function mergedFileRules(writeSet) {
5813
6865
  function writeBack(targetRepo, rel, current, next, lists, name) {
5814
6866
  if (next === current)
5815
6867
  return;
5816
- const path = join10(targetRepo, rel);
6868
+ const path = join12(targetRepo, rel);
5817
6869
  try {
5818
6870
  if (next === null) {
5819
- rmSync4(path);
6871
+ rmSync5(path);
5820
6872
  name(lists.removed, rel, rel);
5821
6873
  } else {
5822
6874
  writeFileSync6(path, next, "utf-8");
@@ -5831,7 +6883,7 @@ function noRecord(targetRepo, writeSet) {
5831
6883
  const claim = (rel, header, lines) => {
5832
6884
  let text;
5833
6885
  try {
5834
- text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync9(join10(targetRepo, rel), "utf-8");
6886
+ text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync11(join12(targetRepo, rel), "utf-8");
5835
6887
  } catch {
5836
6888
  text = "";
5837
6889
  }
@@ -6057,7 +7109,7 @@ function lastOccurrence(text, block) {
6057
7109
  }
6058
7110
  function coveredPathExists(targetRepo, line) {
6059
7111
  const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
6060
- return path !== "" && lexists(join10(targetRepo, path));
7112
+ return path !== "" && lexists(join12(targetRepo, path));
6061
7113
  }
6062
7114
  function uninstall(options) {
6063
7115
  const result = {
@@ -6137,7 +7189,7 @@ function uninstall(options) {
6137
7189
  const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
6138
7190
  trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
6139
7191
  const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0 && result.keptIgnoreLines.length === 0;
6140
- if (checked.manifest === null && !lexists(join10(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
7192
+ if (checked.manifest === null && !lexists(join12(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
6141
7193
  return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
6142
7194
  }
6143
7195
  const credential = handleCredential(options, checked);
@@ -6145,7 +7197,7 @@ function uninstall(options) {
6145
7197
  if (result.failed.length === 0 && !credentialFailed) {
6146
7198
  removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
6147
7199
  try {
6148
- rmSync4(join10(targetRepo, INSTALL_MANIFEST_REL), { force: true });
7200
+ rmSync5(join12(targetRepo, INSTALL_MANIFEST_REL), { force: true });
6149
7201
  named.add(INSTALL_MANIFEST_REL);
6150
7202
  } catch {
6151
7203
  name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
@@ -6180,7 +7232,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
6180
7232
  return;
6181
7233
  const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
6182
7234
  writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
6183
- if (adopted && lexists(join10(targetRepo, rel))) {
7235
+ if (adopted && lexists(join12(targetRepo, rel))) {
6184
7236
  name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
6185
7237
  }
6186
7238
  }
@@ -6193,10 +7245,10 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
6193
7245
  name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
6194
7246
  return;
6195
7247
  }
6196
- const abs = join10(targetRepo, path);
7248
+ const abs = join12(targetRepo, path);
6197
7249
  let isFile;
6198
7250
  try {
6199
- isFile = lstatSync2(abs).isFile();
7251
+ isFile = lstatSync3(abs).isFile();
6200
7252
  } catch {
6201
7253
  return;
6202
7254
  }
@@ -6207,7 +7259,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
6207
7259
  if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
6208
7260
  let bytes;
6209
7261
  try {
6210
- bytes = readFileSync9(abs);
7262
+ bytes = readFileSync11(abs);
6211
7263
  } catch {
6212
7264
  name(lists.failed, path, path);
6213
7265
  return;
@@ -6218,7 +7270,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
6218
7270
  }
6219
7271
  }
6220
7272
  try {
6221
- rmSync4(abs);
7273
+ rmSync5(abs);
6222
7274
  name(lists.removed, path, path);
6223
7275
  } catch {
6224
7276
  name(lists.failed, path, path);
@@ -6227,9 +7279,9 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
6227
7279
  function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
6228
7280
  if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
6229
7281
  return;
6230
- const abs = join10(targetRepo, dir);
7282
+ const abs = join12(targetRepo, dir);
6231
7283
  try {
6232
- if (!lstatSync2(abs).isDirectory() || readdirSync(abs).length > 0)
7284
+ if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
6233
7285
  return;
6234
7286
  } catch {
6235
7287
  return;
@@ -6246,10 +7298,10 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
6246
7298
  name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
6247
7299
  return [];
6248
7300
  }
6249
- const path = join10(targetRepo, rel);
7301
+ const path = join12(targetRepo, rel);
6250
7302
  let before;
6251
7303
  try {
6252
- before = readFileSync9(path, "utf-8");
7304
+ before = readFileSync11(path, "utf-8");
6253
7305
  } catch {
6254
7306
  return [];
6255
7307
  }
@@ -6277,10 +7329,10 @@ function handleCredential(options, checked) {
6277
7329
  if (!lexists(dir))
6278
7330
  return { kind: "not-held", dir };
6279
7331
  try {
6280
- if (lstatSync2(dir).isSymbolicLink())
7332
+ if (lstatSync3(dir).isSymbolicLink())
6281
7333
  return { kind: "failed", dir, reason: "it is a symbolic link" };
6282
- rmSync4(engagementEnvPath(engagementId, home), { force: true });
6283
- rmSync4(notThisAccountMarkerPath(engagementId, home), { force: true });
7334
+ rmSync5(engagementEnvPath(engagementId, home), { force: true });
7335
+ rmSync5(notThisAccountMarkerPath(engagementId, home), { force: true });
6284
7336
  if (readdirSync(dir).length === 0)
6285
7337
  rmdirSync(dir);
6286
7338
  } catch (err) {
@@ -6293,16 +7345,16 @@ function filesUnder(root, rel) {
6293
7345
  const walk = (dirRel) => {
6294
7346
  let names;
6295
7347
  try {
6296
- if (!lstatSync2(join10(root, dirRel)).isDirectory())
7348
+ if (!lstatSync3(join12(root, dirRel)).isDirectory())
6297
7349
  return;
6298
- names = readdirSync(join10(root, dirRel));
7350
+ names = readdirSync(join12(root, dirRel));
6299
7351
  } catch {
6300
7352
  return;
6301
7353
  }
6302
7354
  for (const entry of names.sort()) {
6303
7355
  const childRel = `${dirRel}/${entry}`;
6304
7356
  try {
6305
- if (lstatSync2(join10(root, childRel)).isDirectory())
7357
+ if (lstatSync3(join12(root, childRel)).isDirectory())
6306
7358
  walk(childRel);
6307
7359
  else
6308
7360
  out.push(childRel);
@@ -6316,9 +7368,9 @@ function filesUnder(root, rel) {
6316
7368
  }
6317
7369
  function shownPath(path, home) {
6318
7370
  const base = home ?? homedir2();
6319
- const rel = relative4(base, path);
6320
- const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep);
6321
- return `${under ? `~/${rel.split(sep).join("/")}` : path}/`;
7371
+ const rel = relative5(base, path);
7372
+ const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
7373
+ return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
6322
7374
  }
6323
7375
  function renderUninstallReport(result, home) {
6324
7376
  if (result.outcome === "refused") {
@@ -6374,8 +7426,8 @@ function renderUninstallReport(result, home) {
6374
7426
  }
6375
7427
 
6376
7428
  // dist/banner-facts.js
6377
- import { readFileSync as readFileSync10 } from "node:fs";
6378
- import { basename as basename3, join as join11, resolve as resolve3 } from "node:path";
7429
+ import { readFileSync as readFileSync12 } from "node:fs";
7430
+ import { basename as basename3, join as join13, resolve as resolve4 } from "node:path";
6379
7431
  var BRAND_URL = "halfcycle.ai";
6380
7432
  var TAGLINE = [
6381
7433
  "Fewer cycles.",
@@ -6388,7 +7440,7 @@ var TAGLINE_NARROW = [
6388
7440
  ];
6389
7441
  function bundleVersion() {
6390
7442
  try {
6391
- const pkg = JSON.parse(readFileSync10(join11(BUNDLE_ROOT, "package.json"), "utf-8"));
7443
+ const pkg = JSON.parse(readFileSync12(join13(BUNDLE_ROOT, "package.json"), "utf-8"));
6392
7444
  return typeof pkg.version === "string" ? pkg.version : void 0;
6393
7445
  } catch {
6394
7446
  return void 0;
@@ -6467,7 +7519,7 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
6467
7519
  }
6468
7520
  function safeBasename(dir) {
6469
7521
  try {
6470
- const name = basename3(resolve3(dir)).trim();
7522
+ const name = basename3(resolve4(dir)).trim();
6471
7523
  return name === "" ? "project" : name;
6472
7524
  } catch {
6473
7525
  return "project";
@@ -6528,6 +7580,10 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
6528
7580
  ${HALFCYCLE_COMMAND} close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
6529
7581
  [--thesis "\u2026"] [--outcome "\u2026"] [--deviation "\u2026"]\u2026
6530
7582
  close the phase and write its Build Record into docs/build-records/
7583
+ ${HALFCYCLE_COMMAND} system [--repo <root>]
7584
+ check docs/system.json, the description of your system, and send it to Halfcycle
7585
+ ${DRAW_USAGE}
7586
+ draw the architecture pictures from docs/system.json into docs/diagrams/
6531
7587
  ${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
6532
7588
  take out what the installer added to this project, keeping anything you changed
6533
7589
  ${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
@@ -6540,7 +7596,7 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
6540
7596
  `;
6541
7597
  function isHalfcycleMonorepo(dir) {
6542
7598
  try {
6543
- const pkg = JSON.parse(readFileSync11(join12(dir, "package.json"), "utf-8"));
7599
+ const pkg = JSON.parse(readFileSync13(join14(dir, "package.json"), "utf-8"));
6544
7600
  return pkg.name === "halfcycle-monorepo";
6545
7601
  } catch {
6546
7602
  return false;
@@ -6548,8 +7604,8 @@ function isHalfcycleMonorepo(dir) {
6548
7604
  }
6549
7605
  function isHomeFolder(dir) {
6550
7606
  try {
6551
- const target = realpathSync3(dir);
6552
- const home = realpathSync3(homedir3());
7607
+ const target = realpathSync4(dir);
7608
+ const home = realpathSync4(homedir3());
6553
7609
  return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
6554
7610
  } catch {
6555
7611
  return false;
@@ -6976,6 +8032,43 @@ ${USAGE}`);
6976
8032
  }
6977
8033
  return;
6978
8034
  }
8035
+ if (cmd === "system") {
8036
+ const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
8037
+ const run = await runSystem(repoRoot, {
8038
+ readCredential: (root) => readRepoCredential(root),
8039
+ listTracked: listTrackedFiles,
8040
+ report: reportSystemDescription
8041
+ });
8042
+ if (run.stdout !== "")
8043
+ process.stdout.write(run.stdout);
8044
+ if (run.stderr !== "")
8045
+ process.stderr.write(run.stderr);
8046
+ process.exitCode = run.exitCode;
8047
+ return;
8048
+ }
8049
+ if (cmd === "draw") {
8050
+ const parsed = parseDrawArgs(rest);
8051
+ if (!parsed.ok) {
8052
+ process.stderr.write(`halfcycle draw: usage: ${DRAW_USAGE}
8053
+ `);
8054
+ process.exitCode = 2;
8055
+ return;
8056
+ }
8057
+ const repoRoot = parsed.repoRoot ?? process.cwd();
8058
+ const run = await runDraw(repoRoot, {
8059
+ readCredential: (root) => readRepoCredential(root),
8060
+ listTracked: listTrackedFiles,
8061
+ report: reportSystemDescription,
8062
+ draw: requestDrawing,
8063
+ write: (root, set) => writeDiagramSet(root, set)
8064
+ });
8065
+ if (run.stdout !== "")
8066
+ process.stdout.write(run.stdout);
8067
+ if (run.stderr !== "")
8068
+ process.stderr.write(run.stderr);
8069
+ process.exitCode = run.exitCode;
8070
+ return;
8071
+ }
6979
8072
  if (cmd === "open-phase") {
6980
8073
  const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
6981
8074
  const toProjectScope = rest.includes("--none");