@acta-dev/cli 1.0.0 → 1.2.0

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.
Files changed (2) hide show
  1. package/dist/index.js +366 -13
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { realpathSync } from "fs";
5
5
  import { fileURLToPath } from "url";
6
- import { defineCommand as defineCommand9, runMain } from "citty";
6
+ import { defineCommand as defineCommand10, runMain } from "citty";
7
7
 
8
8
  // src/commands/build.ts
9
9
  import { buildArtifacts } from "@acta-dev/core";
@@ -250,9 +250,9 @@ var LINK_DESCRIPTIONS = {
250
250
  };
251
251
  function table(header, rows) {
252
252
  const head = `| ${header.join(" | ")} |`;
253
- const sep = `| ${header.map(() => "---").join(" | ")} |`;
253
+ const sep2 = `| ${header.map(() => "---").join(" | ")} |`;
254
254
  const body = rows.map((r) => `| ${r.join(" | ")} |`).join("\n");
255
- return [head, sep, body].join("\n");
255
+ return [head, sep2, body].join("\n");
256
256
  }
257
257
  function renderSkill() {
258
258
  const config = resolveConfig2({}, { rootDir: process.cwd() });
@@ -1215,13 +1215,365 @@ function formatShowDate(value) {
1215
1215
  return `${year}-${month}-${day}`;
1216
1216
  }
1217
1217
 
1218
+ // src/commands/site.ts
1219
+ import { spawn } from "child_process";
1220
+ import { createReadStream } from "fs";
1221
+ import { stat } from "fs/promises";
1222
+ import { createServer } from "http";
1223
+ import { createRequire } from "module";
1224
+ import { dirname as dirname2, extname, join as join6, relative as relative2, resolve as resolve3, sep } from "path";
1225
+ import { buildArtifacts as buildArtifacts2 } from "@acta-dev/core";
1226
+ import { defineCommand as defineCommand8 } from "citty";
1227
+ import kleur6 from "kleur";
1228
+ function resolveSiteOptions(config, args, cwd = process.cwd()) {
1229
+ return {
1230
+ outDir: args.out ? resolve3(cwd, args.out) : config.resolvedSite.outDir,
1231
+ base: args.base ?? config.site.base,
1232
+ site: args.site ?? config.site.url
1233
+ };
1234
+ }
1235
+ function resolveSiteServeOptions(args) {
1236
+ const port = args.port === void 0 ? 4321 : Number(args.port);
1237
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
1238
+ throw new Error("Expected --port to be an integer between 0 and 65535.");
1239
+ }
1240
+ return {
1241
+ host: args.host ?? "127.0.0.1",
1242
+ port
1243
+ };
1244
+ }
1245
+ function buildSiteEnv(config, options) {
1246
+ const env = {
1247
+ ...process.env,
1248
+ ASTRO_TELEMETRY_DISABLED: "1",
1249
+ ACTA_PROJECT_ROOT: config.rootDir,
1250
+ ACTA_DIST_DIR: config.resolvedBuild.outDir,
1251
+ ACTA_SITE_OUT: options.outDir
1252
+ };
1253
+ if (options.base !== void 0) env.ACTA_BASE = options.base;
1254
+ if (options.site !== void 0) env.ACTA_SITE = options.site;
1255
+ return env;
1256
+ }
1257
+ var siteCommand = defineCommand8({
1258
+ meta: {
1259
+ name: "site",
1260
+ description: "Build a deployable static web viewer from your docs into the site output dir"
1261
+ },
1262
+ args: {
1263
+ out: {
1264
+ type: "string",
1265
+ description: "Output directory for the generated site (default: .acta/site)"
1266
+ },
1267
+ base: {
1268
+ type: "string",
1269
+ description: "Base path for hosting under a subpath (e.g. /my-repo for project Pages)"
1270
+ },
1271
+ site: {
1272
+ type: "string",
1273
+ description: "Absolute site URL used for canonical links and sitemaps"
1274
+ },
1275
+ "skip-build": {
1276
+ type: "boolean",
1277
+ description: "Reuse existing artifacts instead of running `acta build` first",
1278
+ default: false
1279
+ },
1280
+ serve: {
1281
+ type: "boolean",
1282
+ description: "Serve the generated site locally after building it",
1283
+ default: false
1284
+ },
1285
+ host: {
1286
+ type: "string",
1287
+ description: "Host for --serve (default: 127.0.0.1)"
1288
+ },
1289
+ port: {
1290
+ type: "string",
1291
+ description: "Port for --serve (default: 4321)"
1292
+ },
1293
+ config: {
1294
+ type: "string",
1295
+ alias: "c",
1296
+ description: "Path to acta.config.ts"
1297
+ },
1298
+ json: {
1299
+ type: "boolean",
1300
+ description: "Print the result as JSON",
1301
+ default: false
1302
+ }
1303
+ },
1304
+ async run({ args }) {
1305
+ const { config } = await resolveContext({ config: args.config });
1306
+ const json = Boolean(args.json);
1307
+ const serve = Boolean(args.serve);
1308
+ if (json && serve) {
1309
+ return exitUsage("`acta site --serve` cannot be combined with --json.");
1310
+ }
1311
+ let serveOptions;
1312
+ if (serve) {
1313
+ try {
1314
+ serveOptions = resolveSiteServeOptions(args);
1315
+ } catch (error) {
1316
+ return exitUsage(error instanceof Error ? error.message : String(error));
1317
+ }
1318
+ }
1319
+ let documentCount = 0;
1320
+ if (!args["skip-build"]) {
1321
+ if (!json) printLine("Building artifacts...");
1322
+ const { manifest, validation } = await buildArtifacts2({ config });
1323
+ documentCount = manifest.documentCount;
1324
+ if (validation.errorCount > 0 && !json) {
1325
+ printWarn(
1326
+ `Building site with ${validation.errorCount} validation error${validation.errorCount !== 1 ? "s" : ""}. Run \`acta validate\` for details.`
1327
+ );
1328
+ }
1329
+ }
1330
+ const webDir = resolveWebPackageDir();
1331
+ if (!webDir) {
1332
+ return exitFailure(
1333
+ "Could not locate @acta-dev/web. Install it alongside @acta-dev/cli to use `acta site`."
1334
+ );
1335
+ }
1336
+ const astroBin = resolveAstroBin(webDir);
1337
+ if (!astroBin) {
1338
+ return exitFailure("Could not locate the Astro binary inside @acta-dev/web.");
1339
+ }
1340
+ const options = resolveSiteOptions(config, args);
1341
+ if (!json) printLine("Building static viewer...");
1342
+ const env = buildSiteEnv(config, options);
1343
+ const code = await runAstroBuild(astroBin, webDir, env, json);
1344
+ if (code !== 0) {
1345
+ return exitFailure(`Astro build failed with exit code ${code}.`);
1346
+ }
1347
+ if (json) {
1348
+ printJson({
1349
+ ok: true,
1350
+ outDir: options.outDir,
1351
+ base: options.base ?? null,
1352
+ site: options.site ?? null,
1353
+ documentCount
1354
+ });
1355
+ return;
1356
+ }
1357
+ printLine();
1358
+ printSuccess("Site built");
1359
+ printLine(` ${kleur6.bold("Output:")} ${options.outDir}`);
1360
+ if (options.base) printLine(` ${kleur6.bold("Base:")} ${options.base}`);
1361
+ if (serveOptions) {
1362
+ try {
1363
+ const preview = await serveStaticSite({
1364
+ root: options.outDir,
1365
+ base: options.base,
1366
+ ...serveOptions
1367
+ });
1368
+ printLine(` ${kleur6.bold("Serving:")} ${preview.url}`);
1369
+ printLine();
1370
+ printLine(kleur6.dim("Press Ctrl+C to stop the preview server."));
1371
+ await waitForShutdown(preview);
1372
+ return;
1373
+ } catch (error) {
1374
+ const message = error instanceof Error ? error.message : String(error);
1375
+ return exitFailure(message);
1376
+ }
1377
+ }
1378
+ printLine();
1379
+ printLine(kleur6.dim("Deploy the contents of the output directory to any static host."));
1380
+ }
1381
+ });
1382
+ function resolveWebPackageDir() {
1383
+ const require2 = createRequire(import.meta.url);
1384
+ try {
1385
+ return dirname2(require2.resolve("@acta-dev/web/package.json"));
1386
+ } catch {
1387
+ return void 0;
1388
+ }
1389
+ }
1390
+ function resolveAstroBin(webDir) {
1391
+ const require2 = createRequire(join6(webDir, "package.json"));
1392
+ try {
1393
+ const astroPkgJsonPath = require2.resolve("astro/package.json");
1394
+ const astroPkg = require2("astro/package.json");
1395
+ const binRel = typeof astroPkg.bin === "string" ? astroPkg.bin : astroPkg.bin?.astro;
1396
+ if (!binRel) return void 0;
1397
+ return join6(dirname2(astroPkgJsonPath), binRel);
1398
+ } catch {
1399
+ return void 0;
1400
+ }
1401
+ }
1402
+ function runAstroBuild(astroBin, webDir, env, json) {
1403
+ return new Promise((resolvePromise) => {
1404
+ const child = spawn(process.execPath, [astroBin, "build"], {
1405
+ cwd: webDir,
1406
+ env,
1407
+ // Astro logs go to stderr so JSON on stdout stays clean.
1408
+ stdio: json ? ["ignore", "ignore", "inherit"] : "inherit"
1409
+ });
1410
+ child.on("close", (code) => resolvePromise(code ?? 1));
1411
+ child.on("error", () => resolvePromise(1));
1412
+ });
1413
+ }
1414
+ var mimeTypes = {
1415
+ ".css": "text/css; charset=utf-8",
1416
+ ".html": "text/html; charset=utf-8",
1417
+ ".ico": "image/x-icon",
1418
+ ".jpeg": "image/jpeg",
1419
+ ".jpg": "image/jpeg",
1420
+ ".js": "text/javascript; charset=utf-8",
1421
+ ".json": "application/json; charset=utf-8",
1422
+ ".mjs": "text/javascript; charset=utf-8",
1423
+ ".png": "image/png",
1424
+ ".svg": "image/svg+xml",
1425
+ ".txt": "text/plain; charset=utf-8",
1426
+ ".wasm": "application/wasm",
1427
+ ".webp": "image/webp"
1428
+ };
1429
+ function previewUrl(host, port, base) {
1430
+ const basePath = normalizeBasePath(base);
1431
+ const path = basePath === "/" ? "/" : `${basePath}/`;
1432
+ return `http://${host}:${port}${path}`;
1433
+ }
1434
+ async function serveStaticSite(options) {
1435
+ const root = resolve3(options.root);
1436
+ const basePath = normalizeBasePath(options.base);
1437
+ const server = createServer(async (request, response) => {
1438
+ const result = await resolveStaticSiteResponse({
1439
+ root,
1440
+ base: basePath,
1441
+ method: request.method ?? "GET",
1442
+ url: request.url ?? "/"
1443
+ });
1444
+ if (!result.path) {
1445
+ if (request.method === "HEAD") {
1446
+ response.statusCode = result.status;
1447
+ response.setHeader("Content-Type", result.contentType);
1448
+ response.end();
1449
+ return;
1450
+ }
1451
+ sendText(response, result.status, result.text ?? "Not Found");
1452
+ return;
1453
+ }
1454
+ response.statusCode = result.status;
1455
+ response.setHeader("Content-Type", result.contentType);
1456
+ if (result.contentLength !== void 0) {
1457
+ response.setHeader("Content-Length", String(result.contentLength));
1458
+ }
1459
+ if (request.method === "HEAD") {
1460
+ response.end();
1461
+ return;
1462
+ }
1463
+ createReadStream(result.path).pipe(response);
1464
+ });
1465
+ await new Promise((resolvePromise, reject) => {
1466
+ server.once("error", reject);
1467
+ server.listen(options.port, options.host, () => {
1468
+ server.off("error", reject);
1469
+ resolvePromise();
1470
+ });
1471
+ }).catch((error) => {
1472
+ if (isAddressInUse(error)) {
1473
+ throw new Error(
1474
+ `Port ${options.port} is already in use on ${options.host}. Try a different --port.`
1475
+ );
1476
+ }
1477
+ throw error;
1478
+ });
1479
+ const address = server.address();
1480
+ const port = typeof address === "object" && address ? address.port : options.port;
1481
+ return {
1482
+ server,
1483
+ url: previewUrl(options.host, port, options.base),
1484
+ close: () => new Promise((resolvePromise, reject) => {
1485
+ server.close((error) => error ? reject(error) : resolvePromise());
1486
+ })
1487
+ };
1488
+ }
1489
+ async function resolveStaticSiteResponse(options) {
1490
+ if (options.method !== "GET" && options.method !== "HEAD") {
1491
+ return {
1492
+ status: 405,
1493
+ contentType: "text/plain; charset=utf-8",
1494
+ text: "Method Not Allowed"
1495
+ };
1496
+ }
1497
+ const root = resolve3(options.root);
1498
+ const basePath = normalizeBasePath(options.base);
1499
+ const filePath = await resolveRequestPath(root, basePath, options.url);
1500
+ if (!filePath) {
1501
+ return { status: 404, contentType: "text/plain; charset=utf-8", text: "Not Found" };
1502
+ }
1503
+ try {
1504
+ const fileStat = await stat(filePath);
1505
+ const finalPath = fileStat.isDirectory() ? join6(filePath, "index.html") : filePath;
1506
+ const finalStat = fileStat.isDirectory() ? await stat(finalPath) : fileStat;
1507
+ if (!finalStat.isFile()) {
1508
+ return { status: 404, contentType: "text/plain; charset=utf-8", text: "Not Found" };
1509
+ }
1510
+ return {
1511
+ status: 200,
1512
+ contentType: mimeTypes[extname(finalPath)] ?? "application/octet-stream",
1513
+ contentLength: finalStat.size,
1514
+ path: finalPath
1515
+ };
1516
+ } catch {
1517
+ return { status: 404, contentType: "text/plain; charset=utf-8", text: "Not Found" };
1518
+ }
1519
+ }
1520
+ async function resolveRequestPath(root, basePath, requestUrl) {
1521
+ let pathname;
1522
+ try {
1523
+ pathname = decodeURIComponent(new URL(requestUrl, "http://localhost").pathname);
1524
+ } catch {
1525
+ return void 0;
1526
+ }
1527
+ if (basePath !== "/") {
1528
+ if (pathname === basePath) {
1529
+ pathname = "/";
1530
+ } else if (pathname.startsWith(`${basePath}/`)) {
1531
+ pathname = pathname.slice(basePath.length);
1532
+ } else {
1533
+ return void 0;
1534
+ }
1535
+ }
1536
+ const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
1537
+ const candidate = resolve3(root, `.${normalized}`);
1538
+ const rel = relative2(root, candidate);
1539
+ if (rel === ".." || rel.startsWith(`..${sep}`) || rel === "" || rel.startsWith("/") || rel === ".") {
1540
+ return rel === "." || rel === "" ? root : void 0;
1541
+ }
1542
+ return candidate;
1543
+ }
1544
+ function sendText(response, status, text) {
1545
+ response.statusCode = status;
1546
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
1547
+ response.end(text);
1548
+ }
1549
+ function normalizeBasePath(base) {
1550
+ if (!base || base === "/") return "/";
1551
+ const withLeading = base.startsWith("/") ? base : `/${base}`;
1552
+ return withLeading.replace(/\/+$/, "") || "/";
1553
+ }
1554
+ function waitForShutdown(preview) {
1555
+ return new Promise((resolvePromise) => {
1556
+ const shutdown = async () => {
1557
+ process.off("SIGINT", shutdown);
1558
+ process.off("SIGTERM", shutdown);
1559
+ await preview.close();
1560
+ resolvePromise();
1561
+ };
1562
+ process.once("SIGINT", shutdown);
1563
+ process.once("SIGTERM", shutdown);
1564
+ });
1565
+ }
1566
+ function isAddressInUse(error) {
1567
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
1568
+ }
1569
+
1218
1570
  // src/commands/validate.ts
1219
1571
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
1220
- import { join as join6 } from "path";
1572
+ import { join as join7 } from "path";
1221
1573
  import { validateLoadedProject } from "@acta-dev/core";
1222
- import { defineCommand as defineCommand8 } from "citty";
1223
- import kleur6 from "kleur";
1224
- var validateCommand = defineCommand8({
1574
+ import { defineCommand as defineCommand9 } from "citty";
1575
+ import kleur7 from "kleur";
1576
+ var validateCommand = defineCommand9({
1225
1577
  meta: {
1226
1578
  name: "validate",
1227
1579
  description: "Validate frontmatter, IDs, links, sections and repository rules"
@@ -1253,16 +1605,16 @@ var validateCommand = defineCommand8({
1253
1605
  }
1254
1606
  if (args.ci) {
1255
1607
  await mkdir2(config.resolvedBuild.outDir, { recursive: true });
1256
- const outPath = join6(config.resolvedBuild.outDir, "validation.json");
1608
+ const outPath = join7(config.resolvedBuild.outDir, "validation.json");
1257
1609
  await writeFile4(outPath, `${JSON.stringify(result, null, 2)}
1258
1610
  `, "utf8");
1259
1611
  if (result.errors.length > 0) {
1260
1612
  for (const issue of result.errors) {
1261
- printLine(`${kleur6.red("error")} ${issue.documentId ?? ""} ${issue.message}`);
1613
+ printLine(`${kleur7.red("error")} ${issue.documentId ?? ""} ${issue.message}`);
1262
1614
  }
1263
1615
  }
1264
1616
  for (const issue of result.warnings) {
1265
- printLine(`${kleur6.yellow("warn")} ${issue.documentId ?? ""} ${issue.message}`);
1617
+ printLine(`${kleur7.yellow("warn")} ${issue.documentId ?? ""} ${issue.message}`);
1266
1618
  }
1267
1619
  printLine(`Written ${outPath}`);
1268
1620
  process.exit(result.valid ? 0 : 1);
@@ -1275,12 +1627,12 @@ var validateCommand = defineCommand8({
1275
1627
  const warnings = result.issues.filter((i) => i.severity === "warning");
1276
1628
  if (errors.length > 0) {
1277
1629
  printLine();
1278
- printLine(kleur6.bold("Errors:"));
1630
+ printLine(kleur7.bold("Errors:"));
1279
1631
  printIssues(errors);
1280
1632
  }
1281
1633
  if (warnings.length > 0) {
1282
1634
  printLine();
1283
- printLine(kleur6.bold("Warnings:"));
1635
+ printLine(kleur7.bold("Warnings:"));
1284
1636
  printIssues(warnings);
1285
1637
  }
1286
1638
  printLine();
@@ -1291,7 +1643,7 @@ var validateCommand = defineCommand8({
1291
1643
  });
1292
1644
 
1293
1645
  // src/index.ts
1294
- var main = defineCommand9({
1646
+ var main = defineCommand10({
1295
1647
  meta: {
1296
1648
  name: "acta",
1297
1649
  version: "0.0.0",
@@ -1305,6 +1657,7 @@ var main = defineCommand9({
1305
1657
  validate: validateCommand,
1306
1658
  graph: graphCommand,
1307
1659
  build: buildCommand,
1660
+ site: siteCommand,
1308
1661
  renumber: renumberCommand
1309
1662
  }
1310
1663
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acta-dev/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Acta CLI — TypeScript-first docs-as-code tooling for ADR and spec documents in Git. Provides the `acta` binary.",
5
5
  "keywords": [
6
6
  "adr",
@@ -45,7 +45,8 @@
45
45
  "citty": "^0.2.2",
46
46
  "kleur": "^4.1.5",
47
47
  "yaml": "^2.8.3",
48
- "@acta-dev/core": "1.0.0"
48
+ "@acta-dev/core": "1.1.0",
49
+ "@acta-dev/web": "1.0.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "execa": "^9.6.1",