halfcycle 0.4.5 → 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/.claude-plugin/plugin.json +1 -1
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts +3 -1
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/build-record/write.d.ts +5 -1
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +1178 -85
- package/dist/cli.js.map +3 -3
- package/dist/draw/index.d.ts +91 -0
- package/dist/draw/index.d.ts.map +1 -0
- package/dist/draw/write.d.ts +232 -0
- package/dist/draw/write.d.ts.map +1 -0
- package/dist/index.js +27 -11
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts.map +1 -1
- package/dist/missing-route.d.ts +36 -0
- package/dist/missing-route.d.ts.map +1 -0
- package/dist/system-report.d.ts +168 -0
- package/dist/system-report.d.ts.map +1 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
1379
|
-
super(`${
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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((
|
|
3672
|
+
const bindProblem = await new Promise((resolve5) => {
|
|
3328
3673
|
const onError = (err) => {
|
|
3329
|
-
|
|
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
|
-
|
|
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((
|
|
3695
|
+
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve5) => {
|
|
3351
3696
|
if (pending !== null) {
|
|
3352
|
-
|
|
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
|
-
|
|
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((
|
|
3722
|
+
return new Promise((resolve5) => {
|
|
3378
3723
|
server.closeAllConnections();
|
|
3379
|
-
server.close(() =>
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
3609
|
-
const timer = setTimeout(
|
|
3953
|
+
await new Promise((resolve5) => {
|
|
3954
|
+
const timer = setTimeout(resolve5, started.pollIntervalMs);
|
|
3610
3955
|
interrupt = () => {
|
|
3611
3956
|
clearTimeout(timer);
|
|
3612
|
-
|
|
3957
|
+
resolve5();
|
|
3613
3958
|
};
|
|
3614
3959
|
});
|
|
3615
3960
|
} else {
|
|
3616
|
-
await new Promise((
|
|
3617
|
-
interrupt =
|
|
3618
|
-
void sleep(started.pollIntervalMs).then(
|
|
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((
|
|
4241
|
-
rl.once("close", () =>
|
|
4242
|
-
rl.question(question2).then(
|
|
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,
|
|
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
|
-
|
|
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
|
+

|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
6757
|
+
current = join12(current, segment);
|
|
5706
6758
|
try {
|
|
5707
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5751
|
-
const pinPath =
|
|
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(
|
|
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(
|
|
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 =
|
|
5793
|
-
if (
|
|
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 =
|
|
6868
|
+
const path = join12(targetRepo, rel);
|
|
5817
6869
|
try {
|
|
5818
6870
|
if (next === null) {
|
|
5819
|
-
|
|
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) ? "" :
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
7248
|
+
const abs = join12(targetRepo, path);
|
|
6197
7249
|
let isFile;
|
|
6198
7250
|
try {
|
|
6199
|
-
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 =
|
|
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
|
-
|
|
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 =
|
|
7282
|
+
const abs = join12(targetRepo, dir);
|
|
6231
7283
|
try {
|
|
6232
|
-
if (!
|
|
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 =
|
|
7301
|
+
const path = join12(targetRepo, rel);
|
|
6250
7302
|
let before;
|
|
6251
7303
|
try {
|
|
6252
|
-
before =
|
|
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 (
|
|
7332
|
+
if (lstatSync3(dir).isSymbolicLink())
|
|
6281
7333
|
return { kind: "failed", dir, reason: "it is a symbolic link" };
|
|
6282
|
-
|
|
6283
|
-
|
|
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 (!
|
|
7348
|
+
if (!lstatSync3(join12(root, dirRel)).isDirectory())
|
|
6297
7349
|
return;
|
|
6298
|
-
names = readdirSync(
|
|
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 (
|
|
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 =
|
|
6320
|
-
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(
|
|
6321
|
-
return `${under ? `~/${rel.split(
|
|
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
|
|
6378
|
-
import { basename as basename3, join as
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
6552
|
-
const home =
|
|
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");
|