@acta-dev/cli 1.1.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 +213 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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() });
@@ -1217,8 +1217,11 @@ function formatShowDate(value) {
1217
1217
 
1218
1218
  // src/commands/site.ts
1219
1219
  import { spawn } from "child_process";
1220
+ import { createReadStream } from "fs";
1221
+ import { stat } from "fs/promises";
1222
+ import { createServer } from "http";
1220
1223
  import { createRequire } from "module";
1221
- import { dirname as dirname2, join as join6, resolve as resolve3 } from "path";
1224
+ import { dirname as dirname2, extname, join as join6, relative as relative2, resolve as resolve3, sep } from "path";
1222
1225
  import { buildArtifacts as buildArtifacts2 } from "@acta-dev/core";
1223
1226
  import { defineCommand as defineCommand8 } from "citty";
1224
1227
  import kleur6 from "kleur";
@@ -1229,6 +1232,16 @@ function resolveSiteOptions(config, args, cwd = process.cwd()) {
1229
1232
  site: args.site ?? config.site.url
1230
1233
  };
1231
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
+ }
1232
1245
  function buildSiteEnv(config, options) {
1233
1246
  const env = {
1234
1247
  ...process.env,
@@ -1264,6 +1277,19 @@ var siteCommand = defineCommand8({
1264
1277
  description: "Reuse existing artifacts instead of running `acta build` first",
1265
1278
  default: false
1266
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
+ },
1267
1293
  config: {
1268
1294
  type: "string",
1269
1295
  alias: "c",
@@ -1278,6 +1304,18 @@ var siteCommand = defineCommand8({
1278
1304
  async run({ args }) {
1279
1305
  const { config } = await resolveContext({ config: args.config });
1280
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
+ }
1281
1319
  let documentCount = 0;
1282
1320
  if (!args["skip-build"]) {
1283
1321
  if (!json) printLine("Building artifacts...");
@@ -1320,6 +1358,23 @@ var siteCommand = defineCommand8({
1320
1358
  printSuccess("Site built");
1321
1359
  printLine(` ${kleur6.bold("Output:")} ${options.outDir}`);
1322
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
+ }
1323
1378
  printLine();
1324
1379
  printLine(kleur6.dim("Deploy the contents of the output directory to any static host."));
1325
1380
  }
@@ -1356,6 +1411,161 @@ function runAstroBuild(astroBin, webDir, env, json) {
1356
1411
  child.on("error", () => resolvePromise(1));
1357
1412
  });
1358
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
+ }
1359
1569
 
1360
1570
  // src/commands/validate.ts
1361
1571
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acta-dev/cli",
3
- "version": "1.1.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",