@forge-ts/cli 0.8.0 → 0.14.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.
package/dist/index.js CHANGED
@@ -2,52 +2,15 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createRequire } from "module";
5
- import { defineCommand as defineCommand6, runMain } from "citty";
5
+ import { defineCommand as defineCommand12, runMain } from "citty";
6
6
 
7
- // src/commands/build.ts
8
- import { generateApi } from "@forge-ts/api";
9
- import { loadConfig } from "@forge-ts/core";
10
- import { generate } from "@forge-ts/gen";
7
+ // src/commands/audit.ts
8
+ import {
9
+ formatAuditEvent,
10
+ readAuditLog
11
+ } from "@forge-ts/core";
11
12
  import { defineCommand } from "citty";
12
13
 
13
- // src/logger.ts
14
- var GREEN = "\x1B[32m";
15
- var YELLOW = "\x1B[33m";
16
- var RED = "\x1B[31m";
17
- var BOLD = "\x1B[1m";
18
- var RESET = "\x1B[0m";
19
- function createLogger(options) {
20
- const useColors = options?.colors ?? process.stdout.isTTY ?? false;
21
- function colorize(text, code) {
22
- return useColors ? `${code}${text}${RESET}` : text;
23
- }
24
- function bold(text) {
25
- return useColors ? `${BOLD}${text}${RESET}` : text;
26
- }
27
- return {
28
- info(msg) {
29
- console.log(msg);
30
- },
31
- success(msg) {
32
- const prefix = colorize("\u2713", GREEN);
33
- console.log(`${prefix} ${msg}`);
34
- },
35
- warn(msg) {
36
- const prefix = colorize("warn", YELLOW);
37
- console.warn(`${bold(prefix)} ${msg}`);
38
- },
39
- error(msg) {
40
- const prefix = colorize("error", RED);
41
- console.error(`${bold(prefix)} ${msg}`);
42
- },
43
- step(label, detail, duration) {
44
- const check = colorize("\u2713", GREEN);
45
- const durationStr = duration !== void 0 ? ` (${duration}ms)` : "";
46
- console.log(` ${check} ${bold(label)}: ${detail}${durationStr}`);
47
- }
48
- };
49
- }
50
-
51
14
  // src/output.ts
52
15
  import { randomUUID } from "crypto";
53
16
  import {
@@ -114,6 +77,146 @@ function resolveExitCode(output) {
114
77
  return 1;
115
78
  }
116
79
 
80
+ // src/commands/audit.ts
81
+ function runAudit(args) {
82
+ const rootDir = args.cwd ?? process.cwd();
83
+ const limit = args.limit ?? 20;
84
+ const events = readAuditLog(rootDir, {
85
+ limit,
86
+ eventType: args.type
87
+ });
88
+ const data = {
89
+ success: true,
90
+ count: events.length,
91
+ events
92
+ };
93
+ return {
94
+ operation: "audit",
95
+ success: true,
96
+ data
97
+ };
98
+ }
99
+ function formatAuditHuman(data) {
100
+ if (data.count === 0) {
101
+ return "forge-ts audit: no events found.";
102
+ }
103
+ const lines = [];
104
+ lines.push(`forge-ts audit: ${data.count} event(s)
105
+ `);
106
+ for (const event of data.events) {
107
+ lines.push(` ${formatAuditEvent(event)}`);
108
+ }
109
+ return lines.join("\n");
110
+ }
111
+ var VALID_EVENT_TYPES = [
112
+ "config.lock",
113
+ "config.unlock",
114
+ "config.drift",
115
+ "bypass.create",
116
+ "bypass.expire",
117
+ "rule.change"
118
+ ];
119
+ var auditCommand = defineCommand({
120
+ meta: {
121
+ name: "audit",
122
+ description: "Display the forge-ts audit trail"
123
+ },
124
+ args: {
125
+ cwd: {
126
+ type: "string",
127
+ description: "Project root directory"
128
+ },
129
+ limit: {
130
+ type: "string",
131
+ description: "Maximum events to display (default: 20)"
132
+ },
133
+ type: {
134
+ type: "string",
135
+ description: "Filter by event type (config.lock, config.unlock, config.drift, bypass.create, bypass.expire, rule.change)"
136
+ },
137
+ json: {
138
+ type: "boolean",
139
+ description: "Output as LAFS JSON envelope (agent-friendly)",
140
+ default: false
141
+ },
142
+ human: {
143
+ type: "boolean",
144
+ description: "Output as formatted text (default for TTY)",
145
+ default: false
146
+ },
147
+ quiet: {
148
+ type: "boolean",
149
+ description: "Suppress non-essential output",
150
+ default: false
151
+ }
152
+ },
153
+ run({ args }) {
154
+ const eventType = args.type;
155
+ if (eventType && !VALID_EVENT_TYPES.includes(eventType)) {
156
+ console.error(
157
+ `Error: invalid event type "${eventType}". Valid types: ${VALID_EVENT_TYPES.join(", ")}`
158
+ );
159
+ process.exit(1);
160
+ }
161
+ const output = runAudit({
162
+ cwd: args.cwd,
163
+ limit: args.limit ? Number.parseInt(args.limit, 10) : void 0,
164
+ type: eventType
165
+ });
166
+ const flags = {
167
+ json: args.json,
168
+ human: args.human,
169
+ quiet: args.quiet
170
+ };
171
+ emitResult(output, flags, (data) => formatAuditHuman(data));
172
+ process.exit(resolveExitCode(output));
173
+ }
174
+ });
175
+
176
+ // src/commands/build.ts
177
+ import { generateApi } from "@forge-ts/api";
178
+ import { loadConfig } from "@forge-ts/core";
179
+ import { generate } from "@forge-ts/gen";
180
+ import { defineCommand as defineCommand2 } from "citty";
181
+
182
+ // src/logger.ts
183
+ var GREEN = "\x1B[32m";
184
+ var YELLOW = "\x1B[33m";
185
+ var RED = "\x1B[31m";
186
+ var BOLD = "\x1B[1m";
187
+ var RESET = "\x1B[0m";
188
+ function createLogger(options) {
189
+ const useColors = options?.colors ?? process.stdout.isTTY ?? false;
190
+ function colorize(text, code) {
191
+ return useColors ? `${code}${text}${RESET}` : text;
192
+ }
193
+ function bold(text) {
194
+ return useColors ? `${BOLD}${text}${RESET}` : text;
195
+ }
196
+ return {
197
+ info(msg) {
198
+ console.log(msg);
199
+ },
200
+ success(msg) {
201
+ const prefix = colorize("\u2713", GREEN);
202
+ console.log(`${prefix} ${msg}`);
203
+ },
204
+ warn(msg) {
205
+ const prefix = colorize("warn", YELLOW);
206
+ console.warn(`${bold(prefix)} ${msg}`);
207
+ },
208
+ error(msg) {
209
+ const prefix = colorize("error", RED);
210
+ console.error(`${bold(prefix)} ${msg}`);
211
+ },
212
+ step(label, detail, duration) {
213
+ const check = colorize("\u2713", GREEN);
214
+ const durationStr = duration !== void 0 ? ` (${duration}ms)` : "";
215
+ console.log(` ${check} ${bold(label)}: ${detail}${durationStr}`);
216
+ }
217
+ };
218
+ }
219
+
117
220
  // src/commands/build.ts
118
221
  async function runBuild(args) {
119
222
  const config = await loadConfig(args.cwd);
@@ -217,7 +320,7 @@ async function runBuild(args) {
217
320
  duration: totalMs
218
321
  };
219
322
  }
220
- var buildCommand = defineCommand({
323
+ var buildCommand = defineCommand2({
221
324
  meta: {
222
325
  name: "build",
223
326
  description: "Generate API reference and documentation"
@@ -297,10 +400,180 @@ var buildCommand = defineCommand({
297
400
  }
298
401
  });
299
402
 
403
+ // src/commands/bypass.ts
404
+ import {
405
+ createBypass,
406
+ expireOldBypasses,
407
+ getActiveBypasses,
408
+ getRemainingBudget,
409
+ loadConfig as loadConfig2
410
+ } from "@forge-ts/core";
411
+ import { defineCommand as defineCommand3 } from "citty";
412
+ async function runBypassCreate(args) {
413
+ const config = await loadConfig2(args.cwd);
414
+ const rootDir = config.rootDir;
415
+ expireOldBypasses(rootDir);
416
+ try {
417
+ const bypass = createBypass(rootDir, args.reason, args.rule, config.bypass);
418
+ const remainingBudget = getRemainingBudget(rootDir, config.bypass);
419
+ return {
420
+ operation: "bypass",
421
+ success: true,
422
+ data: {
423
+ success: true,
424
+ bypass,
425
+ remainingBudget,
426
+ dailyBudget: config.bypass.dailyBudget
427
+ },
428
+ duration: 0
429
+ };
430
+ } catch (err) {
431
+ const message = err instanceof Error ? err.message : String(err);
432
+ return {
433
+ operation: "bypass",
434
+ success: false,
435
+ data: {
436
+ success: false,
437
+ bypass: {},
438
+ remainingBudget: 0,
439
+ dailyBudget: config.bypass.dailyBudget
440
+ },
441
+ errors: [
442
+ {
443
+ code: "FORGE_BYPASS_BUDGET_EXHAUSTED",
444
+ message
445
+ }
446
+ ],
447
+ duration: 0
448
+ };
449
+ }
450
+ }
451
+ async function runBypassStatus(args) {
452
+ const config = await loadConfig2(args.cwd);
453
+ const rootDir = config.rootDir;
454
+ const expiredRemoved = expireOldBypasses(rootDir);
455
+ const activeBypasses = getActiveBypasses(rootDir);
456
+ const remainingBudget = getRemainingBudget(rootDir, config.bypass);
457
+ return {
458
+ operation: "bypass",
459
+ success: true,
460
+ data: {
461
+ success: true,
462
+ activeBypasses,
463
+ remainingBudget,
464
+ dailyBudget: config.bypass.dailyBudget,
465
+ expiredRemoved
466
+ },
467
+ duration: 0
468
+ };
469
+ }
470
+ function formatBypassCreateHuman(result) {
471
+ const lines = [];
472
+ if (!result.success) {
473
+ lines.push("forge-ts bypass: FAILED\n");
474
+ lines.push(" Daily bypass budget exhausted.");
475
+ lines.push(` Budget: 0/${result.dailyBudget} remaining`);
476
+ return lines.join("\n");
477
+ }
478
+ lines.push("forge-ts bypass: created\n");
479
+ lines.push(` ID: ${result.bypass.id}`);
480
+ lines.push(` Rule: ${result.bypass.rule}`);
481
+ lines.push(` Reason: ${result.bypass.reason}`);
482
+ lines.push(` Expires: ${result.bypass.expiresAt}`);
483
+ lines.push(` User: ${result.bypass.user}`);
484
+ lines.push(`
485
+ Budget: ${result.remainingBudget}/${result.dailyBudget} remaining today`);
486
+ return lines.join("\n");
487
+ }
488
+ function formatBypassStatusHuman(result) {
489
+ const lines = [];
490
+ lines.push("forge-ts bypass: status\n");
491
+ lines.push(` Budget: ${result.remainingBudget}/${result.dailyBudget} remaining today`);
492
+ if (result.expiredRemoved > 0) {
493
+ lines.push(` Cleaned up ${result.expiredRemoved} expired bypass(es)`);
494
+ }
495
+ if (result.activeBypasses.length === 0) {
496
+ lines.push("\n No active bypasses.");
497
+ } else {
498
+ lines.push(`
499
+ Active bypasses (${result.activeBypasses.length}):`);
500
+ for (const b of result.activeBypasses) {
501
+ lines.push(` - [${b.rule}] ${b.reason} (expires ${b.expiresAt})`);
502
+ }
503
+ }
504
+ return lines.join("\n");
505
+ }
506
+ var bypassCommand = defineCommand3({
507
+ meta: {
508
+ name: "bypass",
509
+ description: "Create or inspect temporary rule bypasses"
510
+ },
511
+ args: {
512
+ cwd: {
513
+ type: "string",
514
+ description: "Project root directory"
515
+ },
516
+ reason: {
517
+ type: "string",
518
+ description: "Mandatory justification for bypassing rules"
519
+ },
520
+ rule: {
521
+ type: "string",
522
+ description: 'Specific rule code to bypass (e.g., "E009"). Defaults to "all"'
523
+ },
524
+ status: {
525
+ type: "boolean",
526
+ description: "Show active bypasses and remaining budget",
527
+ default: false
528
+ },
529
+ json: {
530
+ type: "boolean",
531
+ description: "Output as LAFS JSON envelope (agent-friendly)",
532
+ default: false
533
+ },
534
+ human: {
535
+ type: "boolean",
536
+ description: "Output as formatted text (default for TTY)",
537
+ default: false
538
+ },
539
+ quiet: {
540
+ type: "boolean",
541
+ description: "Suppress non-essential output",
542
+ default: false
543
+ }
544
+ },
545
+ async run({ args }) {
546
+ const flags = {
547
+ json: args.json,
548
+ human: args.human,
549
+ quiet: args.quiet
550
+ };
551
+ if (args.status) {
552
+ const output2 = await runBypassStatus({ cwd: args.cwd });
553
+ emitResult(output2, flags, (data) => formatBypassStatusHuman(data));
554
+ process.exit(resolveExitCode(output2));
555
+ return;
556
+ }
557
+ if (!args.reason) {
558
+ console.error(
559
+ "[forge-ts] error: --reason is required. Provide a justification for the bypass."
560
+ );
561
+ process.exit(1);
562
+ }
563
+ const output = await runBypassCreate({
564
+ cwd: args.cwd,
565
+ reason: args.reason,
566
+ rule: args.rule
567
+ });
568
+ emitResult(output, flags, (data) => formatBypassCreateHuman(data));
569
+ process.exit(resolveExitCode(output));
570
+ }
571
+ });
572
+
300
573
  // src/commands/check.ts
301
- import { loadConfig as loadConfig2 } from "@forge-ts/core";
574
+ import { loadConfig as loadConfig3 } from "@forge-ts/core";
302
575
  import { enforce } from "@forge-ts/enforcer";
303
- import { defineCommand as defineCommand2 } from "citty";
576
+ import { defineCommand as defineCommand4 } from "citty";
304
577
  var RULE_NAMES = {
305
578
  E001: "require-summary",
306
579
  E002: "require-param",
@@ -455,7 +728,7 @@ function buildCheckResult(rawErrors, rawWarnings, exportedSymbolCount, duration,
455
728
  return result;
456
729
  }
457
730
  async function runCheck(args) {
458
- const config = await loadConfig2(args.cwd);
731
+ const config = await loadConfig3(args.cwd);
459
732
  if (args.strict !== void 0) {
460
733
  config.enforce.strict = args.strict;
461
734
  }
@@ -554,7 +827,7 @@ function formatCheckHuman(result) {
554
827
  }
555
828
  }
556
829
  }
557
- if (result.page && result.page.hasMore) {
830
+ if (result.page?.hasMore) {
558
831
  lines.push(
559
832
  `
560
833
  Showing ${result.page.offset + 1}-${result.page.offset + (result.byFile?.length ?? 0)} of ${result.page.total} file(s). Use --offset ${result.page.offset + result.page.limit} to see more.`
@@ -566,7 +839,7 @@ function formatCheckHuman(result) {
566
839
  }
567
840
  return lines.join("\n");
568
841
  }
569
- var checkCommand = defineCommand2({
842
+ var checkCommand = defineCommand4({
570
843
  meta: {
571
844
  name: "check",
572
845
  description: "Lint TSDoc coverage on exported symbols"
@@ -647,12 +920,12 @@ var checkCommand = defineCommand2({
647
920
  // src/commands/docs-dev.ts
648
921
  import { spawn } from "child_process";
649
922
  import { resolve } from "path";
650
- import { loadConfig as loadConfig3 } from "@forge-ts/core";
923
+ import { loadConfig as loadConfig4 } from "@forge-ts/core";
651
924
  import { DEFAULT_TARGET, getAdapter } from "@forge-ts/gen";
652
- import { defineCommand as defineCommand3 } from "citty";
925
+ import { defineCommand as defineCommand5 } from "citty";
653
926
  async function runDocsDev(args) {
654
927
  const logger = createLogger();
655
- const config = await loadConfig3(args.cwd);
928
+ const config = await loadConfig4(args.cwd);
656
929
  const target = args.target ?? config.gen.ssgTarget ?? DEFAULT_TARGET;
657
930
  const adapter = getAdapter(target);
658
931
  const outDir = resolve(config.outDir);
@@ -682,7 +955,7 @@ async function runDocsDev(args) {
682
955
  proc.on("error", reject);
683
956
  });
684
957
  }
685
- var docsDevCommand = defineCommand3({
958
+ var docsDevCommand = defineCommand5({
686
959
  meta: {
687
960
  name: "dev",
688
961
  description: "Start a local doc preview server"
@@ -707,15 +980,16 @@ var docsDevCommand = defineCommand3({
707
980
  });
708
981
 
709
982
  // src/commands/init-docs.ts
983
+ import { existsSync } from "fs";
710
984
  import { mkdir, writeFile } from "fs/promises";
711
985
  import { join, resolve as resolve2 } from "path";
712
- import { loadConfig as loadConfig4 } from "@forge-ts/core";
986
+ import { loadConfig as loadConfig5 } from "@forge-ts/core";
713
987
  import {
714
988
  DEFAULT_TARGET as DEFAULT_TARGET2,
715
989
  getAdapter as getAdapter2,
716
990
  getAvailableTargets
717
991
  } from "@forge-ts/gen";
718
- import { defineCommand as defineCommand4 } from "citty";
992
+ import { defineCommand as defineCommand6 } from "citty";
719
993
  async function runInitDocs(args) {
720
994
  const start = Date.now();
721
995
  const rawTarget = args.target ?? DEFAULT_TARGET2;
@@ -741,7 +1015,7 @@ async function runInitDocs(args) {
741
1015
  }
742
1016
  const target = rawTarget;
743
1017
  const adapter = getAdapter2(target);
744
- const config = await loadConfig4(args.cwd);
1018
+ const config = await loadConfig5(args.cwd);
745
1019
  const outDir = args.outDir ? resolve2(args.outDir) : config.outDir;
746
1020
  const alreadyExists = await adapter.detectExisting(outDir);
747
1021
  if (alreadyExists && !args.force) {
@@ -792,6 +1066,28 @@ async function runInitDocs(args) {
792
1066
  await writeFile(filePath, file.content, "utf8");
793
1067
  writtenFiles.push(file.path);
794
1068
  }
1069
+ if (config.tsdoc.writeConfig) {
1070
+ const tsdocPath = join(config.rootDir, "tsdoc.json");
1071
+ if (existsSync(tsdocPath)) {
1072
+ warnings.push({
1073
+ code: "INIT_TSDOC_EXISTS",
1074
+ message: "tsdoc.json already exists \u2014 skipping. Remove it and re-run to regenerate."
1075
+ });
1076
+ } else {
1077
+ const tsdocContent = JSON.stringify(
1078
+ {
1079
+ $schema: "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
1080
+ extends: ["@forge-ts/tsdoc-config/tsdoc.json"]
1081
+ },
1082
+ null,
1083
+ " "
1084
+ );
1085
+ await mkdir(config.rootDir, { recursive: true });
1086
+ await writeFile(tsdocPath, `${tsdocContent}
1087
+ `, "utf8");
1088
+ writtenFiles.push("tsdoc.json");
1089
+ }
1090
+ }
795
1091
  const depCount = Object.keys(manifest.dependencies).length + Object.keys(manifest.devDependencies).length;
796
1092
  const scriptCount = Object.keys(manifest.scripts).length;
797
1093
  const data = {
@@ -849,7 +1145,7 @@ function formatInitDocsHuman(result) {
849
1145
  );
850
1146
  return lines.join("\n");
851
1147
  }
852
- var initDocsCommand = defineCommand4({
1148
+ var initDocsCommand = defineCommand6({
853
1149
  meta: {
854
1150
  name: "init",
855
1151
  description: "Scaffold a documentation site"
@@ -918,7 +1214,7 @@ var initDocsCommand = defineCommand4({
918
1214
  process.exit(resolveExitCode(output));
919
1215
  }
920
1216
  });
921
- var initCommand = defineCommand4({
1217
+ var initCommand = defineCommand6({
922
1218
  meta: {
923
1219
  name: "init",
924
1220
  description: "Scaffold project artefacts"
@@ -928,15 +1224,492 @@ var initCommand = defineCommand4({
928
1224
  }
929
1225
  });
930
1226
 
931
- // src/commands/test.ts
932
- import { loadConfig as loadConfig5 } from "@forge-ts/core";
933
- import { doctest } from "@forge-ts/doctest";
934
- import { defineCommand as defineCommand5 } from "citty";
935
- async function runTest(args) {
936
- const config = await loadConfig5(args.cwd);
937
- const result = await doctest(config);
938
- const mviLevel = args.mvi ?? "standard";
939
- const failCount = result.errors.length;
1227
+ // src/commands/init-hooks.ts
1228
+ import { existsSync as existsSync2, readFileSync } from "fs";
1229
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
1230
+ import { join as join2 } from "path";
1231
+ import { defineCommand as defineCommand7 } from "citty";
1232
+ function detectHookManager(rootDir) {
1233
+ const huskyDir = join2(rootDir, ".husky");
1234
+ if (existsSync2(huskyDir)) {
1235
+ return "husky";
1236
+ }
1237
+ const lefthookYml = join2(rootDir, "lefthook.yml");
1238
+ if (existsSync2(lefthookYml)) {
1239
+ return "lefthook";
1240
+ }
1241
+ const pkgJsonPath = join2(rootDir, "package.json");
1242
+ if (existsSync2(pkgJsonPath)) {
1243
+ try {
1244
+ const raw = readFileSync(pkgJsonPath, "utf8");
1245
+ const pkg2 = JSON.parse(raw);
1246
+ const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
1247
+ if ("husky" in allDeps) return "husky";
1248
+ if ("lefthook" in allDeps) return "lefthook";
1249
+ } catch {
1250
+ }
1251
+ }
1252
+ return "none";
1253
+ }
1254
+ var HUSKY_PRE_COMMIT = `#!/usr/bin/env sh
1255
+ . "$(dirname -- "$0")/_/husky.sh"
1256
+
1257
+ npx forge-ts check
1258
+ `;
1259
+ var LEFTHOOK_BLOCK = `pre-commit:
1260
+ commands:
1261
+ forge-ts-check:
1262
+ run: npx forge-ts check
1263
+ `;
1264
+ async function runInitHooks(args) {
1265
+ const start = Date.now();
1266
+ const rootDir = args.cwd ?? process.cwd();
1267
+ const hookManager = detectHookManager(rootDir);
1268
+ const writtenFiles = [];
1269
+ const skippedFiles = [];
1270
+ const warnings = [];
1271
+ const instructions = [];
1272
+ if (hookManager === "husky" || hookManager === "none") {
1273
+ const huskyDir = join2(rootDir, ".husky");
1274
+ const hookPath = join2(huskyDir, "pre-commit");
1275
+ const relativePath = ".husky/pre-commit";
1276
+ if (existsSync2(hookPath) && !args.force) {
1277
+ const existing = await readFile(hookPath, "utf8");
1278
+ if (existing.includes("forge-ts check")) {
1279
+ skippedFiles.push(relativePath);
1280
+ warnings.push({
1281
+ code: "HOOKS_ALREADY_EXISTS",
1282
+ message: `${relativePath} already contains forge-ts check \u2014 skipping. Use --force to overwrite.`
1283
+ });
1284
+ } else {
1285
+ const appended = `${existing.trimEnd()}
1286
+
1287
+ npx forge-ts check
1288
+ `;
1289
+ await writeFile2(hookPath, appended, { mode: 493 });
1290
+ writtenFiles.push(relativePath);
1291
+ }
1292
+ } else {
1293
+ await mkdir2(huskyDir, { recursive: true });
1294
+ await writeFile2(hookPath, HUSKY_PRE_COMMIT, { mode: 493 });
1295
+ writtenFiles.push(relativePath);
1296
+ }
1297
+ if (hookManager === "none") {
1298
+ instructions.push(
1299
+ "No hook manager detected. Wrote .husky/pre-commit as a starting point.",
1300
+ "Install husky to activate: npx husky-init && npm install (or pnpm dlx husky-init && pnpm install)"
1301
+ );
1302
+ } else {
1303
+ instructions.push("Husky pre-commit hook configured to run forge-ts check.");
1304
+ }
1305
+ } else if (hookManager === "lefthook") {
1306
+ const lefthookPath = join2(rootDir, "lefthook.yml");
1307
+ const relativePath = "lefthook.yml";
1308
+ if (existsSync2(lefthookPath)) {
1309
+ const existing = await readFile(lefthookPath, "utf8");
1310
+ if (existing.includes("forge-ts check") && !args.force) {
1311
+ skippedFiles.push(relativePath);
1312
+ warnings.push({
1313
+ code: "HOOKS_ALREADY_EXISTS",
1314
+ message: `${relativePath} already contains forge-ts check \u2014 skipping. Use --force to overwrite.`
1315
+ });
1316
+ } else if (existing.includes("pre-commit:") && !args.force) {
1317
+ const appended = `${existing.trimEnd()}
1318
+ forge-ts-check:
1319
+ run: npx forge-ts check
1320
+ `;
1321
+ await writeFile2(lefthookPath, appended, "utf8");
1322
+ writtenFiles.push(relativePath);
1323
+ } else {
1324
+ const appended = `${existing.trimEnd()}
1325
+
1326
+ ${LEFTHOOK_BLOCK}`;
1327
+ await writeFile2(lefthookPath, appended, "utf8");
1328
+ writtenFiles.push(relativePath);
1329
+ }
1330
+ } else {
1331
+ await writeFile2(lefthookPath, LEFTHOOK_BLOCK, "utf8");
1332
+ writtenFiles.push(relativePath);
1333
+ }
1334
+ instructions.push("Lefthook pre-commit hook configured to run forge-ts check.");
1335
+ }
1336
+ const data = {
1337
+ success: true,
1338
+ hookManager,
1339
+ summary: {
1340
+ filesWritten: writtenFiles.length,
1341
+ filesSkipped: skippedFiles.length
1342
+ },
1343
+ files: writtenFiles,
1344
+ instructions
1345
+ };
1346
+ return {
1347
+ operation: "init.hooks",
1348
+ success: true,
1349
+ data,
1350
+ warnings: warnings.length > 0 ? warnings : void 0,
1351
+ duration: Date.now() - start
1352
+ };
1353
+ }
1354
+ function formatInitHooksHuman(result) {
1355
+ const lines = [];
1356
+ const managerName = result.hookManager === "none" ? "husky (default)" : result.hookManager;
1357
+ lines.push(`
1358
+ Configuring git hooks (${managerName})...
1359
+ `);
1360
+ for (const file of result.files) {
1361
+ lines.push(` \u2713 ${file}`);
1362
+ }
1363
+ if (result.summary.filesSkipped > 0) {
1364
+ lines.push(` (${result.summary.filesSkipped} file(s) skipped \u2014 already configured)`);
1365
+ }
1366
+ if (result.instructions.length > 0) {
1367
+ lines.push("\n Next steps:");
1368
+ for (const [idx, inst] of result.instructions.entries()) {
1369
+ lines.push(` ${idx + 1}. ${inst}`);
1370
+ }
1371
+ }
1372
+ lines.push(`
1373
+ ${result.summary.filesWritten} file(s) written.`);
1374
+ return lines.join("\n");
1375
+ }
1376
+ var initHooksCommand = defineCommand7({
1377
+ meta: {
1378
+ name: "hooks",
1379
+ description: "Scaffold git hook integration (husky/lefthook)"
1380
+ },
1381
+ args: {
1382
+ cwd: {
1383
+ type: "string",
1384
+ description: "Project root directory"
1385
+ },
1386
+ force: {
1387
+ type: "boolean",
1388
+ description: "Overwrite existing hook files",
1389
+ default: false
1390
+ },
1391
+ json: {
1392
+ type: "boolean",
1393
+ description: "Output as LAFS JSON envelope",
1394
+ default: false
1395
+ },
1396
+ human: {
1397
+ type: "boolean",
1398
+ description: "Output as formatted text",
1399
+ default: false
1400
+ },
1401
+ quiet: {
1402
+ type: "boolean",
1403
+ description: "Suppress non-essential output",
1404
+ default: false
1405
+ },
1406
+ mvi: {
1407
+ type: "string",
1408
+ description: "MVI verbosity level: minimal, standard, full"
1409
+ }
1410
+ },
1411
+ async run({ args }) {
1412
+ const output = await runInitHooks({
1413
+ cwd: args.cwd,
1414
+ force: args.force,
1415
+ mvi: args.mvi
1416
+ });
1417
+ const flags = {
1418
+ json: args.json,
1419
+ human: args.human,
1420
+ quiet: args.quiet,
1421
+ mvi: args.mvi
1422
+ };
1423
+ emitResult(output, flags, (data, cmd) => {
1424
+ if (!cmd.success) {
1425
+ const logger = createLogger();
1426
+ const msg = cmd.errors?.[0]?.message ?? "Hook scaffolding failed";
1427
+ logger.error(msg);
1428
+ return "";
1429
+ }
1430
+ return formatInitHooksHuman(data);
1431
+ });
1432
+ process.exit(resolveExitCode(output));
1433
+ }
1434
+ });
1435
+
1436
+ // src/commands/lock.ts
1437
+ import {
1438
+ appendAuditEvent,
1439
+ createLockManifest,
1440
+ loadConfig as loadConfig6,
1441
+ readLockFile,
1442
+ writeLockFile
1443
+ } from "@forge-ts/core";
1444
+ import { defineCommand as defineCommand8 } from "citty";
1445
+ async function runLock(args) {
1446
+ const config = await loadConfig6(args.cwd);
1447
+ const rootDir = config.rootDir;
1448
+ const existingLock = readLockFile(rootDir);
1449
+ const manifest = createLockManifest(config);
1450
+ writeLockFile(rootDir, manifest);
1451
+ appendAuditEvent(rootDir, {
1452
+ timestamp: manifest.lockedAt,
1453
+ event: "config.lock",
1454
+ user: manifest.lockedBy,
1455
+ details: {
1456
+ rules: Object.keys(manifest.config.rules).length,
1457
+ tsconfig: manifest.config.tsconfig !== void 0,
1458
+ biome: manifest.config.biome !== void 0,
1459
+ overwrote: existingLock !== null
1460
+ }
1461
+ });
1462
+ const lockFile = `${rootDir}/.forge-lock.json`;
1463
+ const data = {
1464
+ success: true,
1465
+ lockFile,
1466
+ lockedAt: manifest.lockedAt,
1467
+ lockedBy: manifest.lockedBy,
1468
+ locked: {
1469
+ rules: Object.keys(manifest.config.rules).length,
1470
+ tsconfig: manifest.config.tsconfig !== void 0,
1471
+ biome: manifest.config.biome !== void 0
1472
+ },
1473
+ overwrote: existingLock !== null
1474
+ };
1475
+ return {
1476
+ operation: "lock",
1477
+ success: true,
1478
+ data,
1479
+ duration: 0
1480
+ };
1481
+ }
1482
+ function formatLockHuman(result) {
1483
+ const lines = [];
1484
+ if (result.overwrote) {
1485
+ lines.push("forge-ts lock: updated existing lock\n");
1486
+ } else {
1487
+ lines.push("forge-ts lock: created .forge-lock.json\n");
1488
+ }
1489
+ lines.push(` Locked ${result.locked.rules} enforce rule(s)`);
1490
+ if (result.locked.tsconfig) {
1491
+ lines.push(" Locked tsconfig guard settings");
1492
+ }
1493
+ if (result.locked.biome) {
1494
+ lines.push(" Locked biome guard settings");
1495
+ }
1496
+ lines.push(`
1497
+ Locked by: ${result.lockedBy}`);
1498
+ lines.push(` Locked at: ${result.lockedAt}`);
1499
+ lines.push(`
1500
+ To modify locked settings, run: forge-ts unlock --reason="..."`);
1501
+ return lines.join("\n");
1502
+ }
1503
+ var lockCommand = defineCommand8({
1504
+ meta: {
1505
+ name: "lock",
1506
+ description: "Lock current config to prevent silent weakening"
1507
+ },
1508
+ args: {
1509
+ cwd: {
1510
+ type: "string",
1511
+ description: "Project root directory"
1512
+ },
1513
+ json: {
1514
+ type: "boolean",
1515
+ description: "Output as LAFS JSON envelope (agent-friendly)",
1516
+ default: false
1517
+ },
1518
+ human: {
1519
+ type: "boolean",
1520
+ description: "Output as formatted text (default for TTY)",
1521
+ default: false
1522
+ },
1523
+ quiet: {
1524
+ type: "boolean",
1525
+ description: "Suppress non-essential output",
1526
+ default: false
1527
+ }
1528
+ },
1529
+ async run({ args }) {
1530
+ const output = await runLock({ cwd: args.cwd });
1531
+ const flags = {
1532
+ json: args.json,
1533
+ human: args.human,
1534
+ quiet: args.quiet
1535
+ };
1536
+ emitResult(output, flags, (data) => formatLockHuman(data));
1537
+ process.exit(resolveExitCode(output));
1538
+ }
1539
+ });
1540
+
1541
+ // src/commands/prepublish.ts
1542
+ import { defineCommand as defineCommand9 } from "citty";
1543
+ async function runPrepublish(args) {
1544
+ const start = Date.now();
1545
+ const allErrors = [];
1546
+ const checkOutput = await runCheck({
1547
+ cwd: args.cwd,
1548
+ strict: args.strict,
1549
+ mvi: args.mvi
1550
+ });
1551
+ const checkDuration = checkOutput.duration ?? 0;
1552
+ if (!checkOutput.success) {
1553
+ const data2 = {
1554
+ success: false,
1555
+ summary: {
1556
+ steps: 1,
1557
+ passed: 0,
1558
+ failed: 1,
1559
+ duration: Date.now() - start
1560
+ },
1561
+ check: {
1562
+ success: false,
1563
+ errors: checkOutput.data.summary.errors,
1564
+ warnings: checkOutput.data.summary.warnings,
1565
+ duration: checkDuration
1566
+ },
1567
+ skippedReason: "Check failed \u2014 build step skipped."
1568
+ };
1569
+ if (checkOutput.errors) {
1570
+ allErrors.push(...checkOutput.errors);
1571
+ }
1572
+ return {
1573
+ operation: "prepublish",
1574
+ success: false,
1575
+ data: data2,
1576
+ errors: allErrors.length > 0 ? allErrors : void 0,
1577
+ duration: Date.now() - start
1578
+ };
1579
+ }
1580
+ const buildOutput = await runBuild({
1581
+ cwd: args.cwd,
1582
+ mvi: args.mvi
1583
+ });
1584
+ const buildDuration = buildOutput.duration ?? 0;
1585
+ const buildSuccess = buildOutput.success;
1586
+ const overallSuccess = buildSuccess;
1587
+ if (buildOutput.errors) {
1588
+ allErrors.push(...buildOutput.errors);
1589
+ }
1590
+ const data = {
1591
+ success: overallSuccess,
1592
+ summary: {
1593
+ steps: 2,
1594
+ passed: (checkOutput.success ? 1 : 0) + (buildSuccess ? 1 : 0),
1595
+ failed: (checkOutput.success ? 0 : 1) + (buildSuccess ? 0 : 1),
1596
+ duration: Date.now() - start
1597
+ },
1598
+ check: {
1599
+ success: checkOutput.success,
1600
+ errors: checkOutput.data.summary.errors,
1601
+ warnings: checkOutput.data.summary.warnings,
1602
+ duration: checkDuration
1603
+ },
1604
+ build: {
1605
+ success: buildSuccess,
1606
+ steps: buildOutput.data.summary.steps,
1607
+ succeeded: buildOutput.data.summary.succeeded,
1608
+ failed: buildOutput.data.summary.failed,
1609
+ duration: buildDuration
1610
+ }
1611
+ };
1612
+ return {
1613
+ operation: "prepublish",
1614
+ success: overallSuccess,
1615
+ data,
1616
+ errors: allErrors.length > 0 ? allErrors : void 0,
1617
+ duration: Date.now() - start
1618
+ };
1619
+ }
1620
+ function formatPrepublishHuman(result) {
1621
+ const lines = [];
1622
+ lines.push(`
1623
+ forge-ts prepublish: ${result.success ? "PASSED" : "FAILED"}
1624
+ `);
1625
+ const checkIcon = result.check.success ? "\u2713" : "\u2717";
1626
+ lines.push(
1627
+ ` ${checkIcon} check: ${result.check.errors} error(s), ${result.check.warnings} warning(s) (${result.check.duration}ms)`
1628
+ );
1629
+ if (result.build) {
1630
+ const buildIcon = result.build.success ? "\u2713" : "\u2717";
1631
+ lines.push(
1632
+ ` ${buildIcon} build: ${result.build.succeeded}/${result.build.steps} steps succeeded (${result.build.duration}ms)`
1633
+ );
1634
+ } else if (result.skippedReason) {
1635
+ lines.push(` - build: skipped (${result.skippedReason})`);
1636
+ }
1637
+ lines.push(
1638
+ `
1639
+ ${result.summary.passed}/${result.summary.steps} steps passed in ${result.summary.duration}ms`
1640
+ );
1641
+ if (!result.success) {
1642
+ lines.push("\n Publish blocked. Fix the above issues and re-run forge-ts prepublish.");
1643
+ }
1644
+ return lines.join("\n");
1645
+ }
1646
+ var prepublishCommand = defineCommand9({
1647
+ meta: {
1648
+ name: "prepublish",
1649
+ description: "Safety gate: check + build before npm publish"
1650
+ },
1651
+ args: {
1652
+ cwd: {
1653
+ type: "string",
1654
+ description: "Project root directory"
1655
+ },
1656
+ strict: {
1657
+ type: "boolean",
1658
+ description: "Treat warnings as errors during check",
1659
+ default: false
1660
+ },
1661
+ json: {
1662
+ type: "boolean",
1663
+ description: "Output as LAFS JSON envelope (agent-friendly)",
1664
+ default: false
1665
+ },
1666
+ human: {
1667
+ type: "boolean",
1668
+ description: "Output as formatted text (default for TTY)",
1669
+ default: false
1670
+ },
1671
+ quiet: {
1672
+ type: "boolean",
1673
+ description: "Suppress non-essential output",
1674
+ default: false
1675
+ },
1676
+ mvi: {
1677
+ type: "string",
1678
+ description: "MVI verbosity level: minimal, standard, full"
1679
+ }
1680
+ },
1681
+ async run({ args }) {
1682
+ const output = await runPrepublish({
1683
+ cwd: args.cwd,
1684
+ strict: args.strict,
1685
+ mvi: args.mvi
1686
+ });
1687
+ const flags = {
1688
+ json: args.json,
1689
+ human: args.human,
1690
+ quiet: args.quiet,
1691
+ mvi: args.mvi
1692
+ };
1693
+ emitResult(output, flags, (data, cmd) => {
1694
+ if (!cmd.success) {
1695
+ const logger = createLogger();
1696
+ logger.error("Prepublish gate failed");
1697
+ }
1698
+ return formatPrepublishHuman(data);
1699
+ });
1700
+ process.exit(resolveExitCode(output));
1701
+ }
1702
+ });
1703
+
1704
+ // src/commands/test.ts
1705
+ import { loadConfig as loadConfig7 } from "@forge-ts/core";
1706
+ import { doctest } from "@forge-ts/doctest";
1707
+ import { defineCommand as defineCommand10 } from "citty";
1708
+ async function runTest(args) {
1709
+ const config = await loadConfig7(args.cwd);
1710
+ const result = await doctest(config);
1711
+ const mviLevel = args.mvi ?? "standard";
1712
+ const failCount = result.errors.length;
940
1713
  const totalSymbols = result.symbols.length;
941
1714
  const passCount = totalSymbols - failCount > 0 ? totalSymbols - failCount : 0;
942
1715
  const summary = {
@@ -974,7 +1747,7 @@ async function runTest(args) {
974
1747
  duration: result.duration
975
1748
  };
976
1749
  }
977
- var testCommand = defineCommand5({
1750
+ var testCommand = defineCommand10({
978
1751
  meta: {
979
1752
  name: "test",
980
1753
  description: "Run @example blocks as doctests"
@@ -1027,10 +1800,155 @@ var testCommand = defineCommand5({
1027
1800
  }
1028
1801
  });
1029
1802
 
1803
+ // src/commands/unlock.ts
1804
+ import {
1805
+ appendAuditEvent as appendAuditEvent2,
1806
+ getCurrentUser,
1807
+ loadConfig as loadConfig8,
1808
+ readLockFile as readLockFile2,
1809
+ removeLockFile
1810
+ } from "@forge-ts/core";
1811
+ import { defineCommand as defineCommand11 } from "citty";
1812
+ async function runUnlock(args) {
1813
+ const config = await loadConfig8(args.cwd);
1814
+ const rootDir = config.rootDir;
1815
+ const existingLock = readLockFile2(rootDir);
1816
+ if (!existingLock) {
1817
+ return {
1818
+ operation: "unlock",
1819
+ success: false,
1820
+ data: {
1821
+ success: false,
1822
+ reason: args.reason,
1823
+ previousLockedBy: null,
1824
+ previousLockedAt: null
1825
+ },
1826
+ errors: [
1827
+ {
1828
+ code: "FORGE_NO_LOCK",
1829
+ message: "No .forge-lock.json found. Nothing to unlock."
1830
+ }
1831
+ ],
1832
+ duration: 0
1833
+ };
1834
+ }
1835
+ const removed = removeLockFile(rootDir);
1836
+ if (!removed) {
1837
+ return {
1838
+ operation: "unlock",
1839
+ success: false,
1840
+ data: {
1841
+ success: false,
1842
+ reason: args.reason,
1843
+ previousLockedBy: existingLock.lockedBy,
1844
+ previousLockedAt: existingLock.lockedAt
1845
+ },
1846
+ errors: [
1847
+ {
1848
+ code: "FORGE_UNLOCK_FAILED",
1849
+ message: "Failed to remove .forge-lock.json. Check file permissions."
1850
+ }
1851
+ ],
1852
+ duration: 0
1853
+ };
1854
+ }
1855
+ appendAuditEvent2(rootDir, {
1856
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1857
+ event: "config.unlock",
1858
+ user: getCurrentUser(),
1859
+ reason: args.reason,
1860
+ details: {
1861
+ previousLockedBy: existingLock.lockedBy,
1862
+ previousLockedAt: existingLock.lockedAt
1863
+ }
1864
+ });
1865
+ return {
1866
+ operation: "unlock",
1867
+ success: true,
1868
+ data: {
1869
+ success: true,
1870
+ reason: args.reason,
1871
+ previousLockedBy: existingLock.lockedBy,
1872
+ previousLockedAt: existingLock.lockedAt
1873
+ },
1874
+ duration: 0
1875
+ };
1876
+ }
1877
+ function formatUnlockHuman(result) {
1878
+ const lines = [];
1879
+ if (!result.success) {
1880
+ lines.push("forge-ts unlock: FAILED\n");
1881
+ lines.push(" No .forge-lock.json found. Nothing to unlock.");
1882
+ return lines.join("\n");
1883
+ }
1884
+ lines.push("forge-ts unlock: removed .forge-lock.json\n");
1885
+ lines.push(` Reason: ${result.reason}`);
1886
+ if (result.previousLockedBy) {
1887
+ lines.push(` Previously locked by: ${result.previousLockedBy}`);
1888
+ }
1889
+ if (result.previousLockedAt) {
1890
+ lines.push(` Previously locked at: ${result.previousLockedAt}`);
1891
+ }
1892
+ lines.push("\n Config settings can now be modified freely.");
1893
+ lines.push(" Run `forge-ts lock` to re-lock after changes.");
1894
+ return lines.join("\n");
1895
+ }
1896
+ var unlockCommand = defineCommand11({
1897
+ meta: {
1898
+ name: "unlock",
1899
+ description: "Remove config lock (requires --reason)"
1900
+ },
1901
+ args: {
1902
+ cwd: {
1903
+ type: "string",
1904
+ description: "Project root directory"
1905
+ },
1906
+ reason: {
1907
+ type: "string",
1908
+ description: "Mandatory reason for unlocking (audit trail)",
1909
+ required: true
1910
+ },
1911
+ json: {
1912
+ type: "boolean",
1913
+ description: "Output as LAFS JSON envelope (agent-friendly)",
1914
+ default: false
1915
+ },
1916
+ human: {
1917
+ type: "boolean",
1918
+ description: "Output as formatted text (default for TTY)",
1919
+ default: false
1920
+ },
1921
+ quiet: {
1922
+ type: "boolean",
1923
+ description: "Suppress non-essential output",
1924
+ default: false
1925
+ }
1926
+ },
1927
+ async run({ args }) {
1928
+ if (!args.reason) {
1929
+ console.error(
1930
+ "[forge-ts] error: --reason is required. Provide a reason for unlocking the config."
1931
+ );
1932
+ process.exit(1);
1933
+ }
1934
+ const output = await runUnlock({
1935
+ cwd: args.cwd,
1936
+ reason: args.reason
1937
+ });
1938
+ const flags = {
1939
+ json: args.json,
1940
+ human: args.human,
1941
+ quiet: args.quiet
1942
+ };
1943
+ emitResult(output, flags, (data) => formatUnlockHuman(data));
1944
+ process.exit(resolveExitCode(output));
1945
+ }
1946
+ });
1947
+
1030
1948
  // src/index.ts
1031
1949
  var require2 = createRequire(import.meta.url);
1032
1950
  var pkg = require2("../package.json");
1033
- var docsCommand = defineCommand6({
1951
+ var docsCommand = defineCommand12({
1034
1952
  meta: {
1035
1953
  name: "docs",
1036
1954
  description: "Documentation site management"
@@ -1040,7 +1958,17 @@ var docsCommand = defineCommand6({
1040
1958
  dev: docsDevCommand
1041
1959
  }
1042
1960
  });
1043
- var main = defineCommand6({
1961
+ var initCommand2 = defineCommand12({
1962
+ meta: {
1963
+ name: "init",
1964
+ description: "Scaffold project artefacts"
1965
+ },
1966
+ subCommands: {
1967
+ docs: initDocsCommand,
1968
+ hooks: initHooksCommand
1969
+ }
1970
+ });
1971
+ var main = defineCommand12({
1044
1972
  meta: {
1045
1973
  name: "forge-ts",
1046
1974
  version: pkg.version,
@@ -1050,19 +1978,37 @@ var main = defineCommand6({
1050
1978
  check: checkCommand,
1051
1979
  test: testCommand,
1052
1980
  build: buildCommand,
1053
- docs: docsCommand
1981
+ docs: docsCommand,
1982
+ init: initCommand2,
1983
+ lock: lockCommand,
1984
+ unlock: unlockCommand,
1985
+ bypass: bypassCommand,
1986
+ audit: auditCommand,
1987
+ prepublish: prepublishCommand
1054
1988
  }
1055
1989
  });
1056
1990
  runMain(main);
1057
1991
  export {
1992
+ auditCommand,
1058
1993
  buildCommand,
1994
+ bypassCommand,
1059
1995
  checkCommand,
1060
1996
  createLogger,
1061
1997
  docsDevCommand,
1062
1998
  emitResult,
1063
1999
  initDocsCommand,
2000
+ initHooksCommand,
2001
+ lockCommand,
2002
+ prepublishCommand,
1064
2003
  resolveExitCode,
2004
+ runBypassCreate,
2005
+ runBypassStatus,
1065
2006
  runDocsDev,
1066
- testCommand
2007
+ runInitHooks,
2008
+ runLock,
2009
+ runPrepublish,
2010
+ runUnlock,
2011
+ testCommand,
2012
+ unlockCommand
1067
2013
  };
1068
2014
  //# sourceMappingURL=index.js.map