@graphorin/cli 0.5.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 (109) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/dist/bin/graphorin.d.ts +1 -0
  5. package/dist/bin/graphorin.js +606 -0
  6. package/dist/bin/graphorin.js.map +1 -0
  7. package/dist/commands/audit.d.ts +59 -0
  8. package/dist/commands/audit.d.ts.map +1 -0
  9. package/dist/commands/audit.js +156 -0
  10. package/dist/commands/audit.js.map +1 -0
  11. package/dist/commands/auth.d.ts +48 -0
  12. package/dist/commands/auth.d.ts.map +1 -0
  13. package/dist/commands/auth.js +148 -0
  14. package/dist/commands/auth.js.map +1 -0
  15. package/dist/commands/consolidator.d.ts +49 -0
  16. package/dist/commands/consolidator.d.ts.map +1 -0
  17. package/dist/commands/consolidator.js +97 -0
  18. package/dist/commands/consolidator.js.map +1 -0
  19. package/dist/commands/doctor.d.ts +64 -0
  20. package/dist/commands/doctor.d.ts.map +1 -0
  21. package/dist/commands/doctor.js +150 -0
  22. package/dist/commands/doctor.js.map +1 -0
  23. package/dist/commands/guard.d.ts +35 -0
  24. package/dist/commands/guard.d.ts.map +1 -0
  25. package/dist/commands/guard.js +86 -0
  26. package/dist/commands/guard.js.map +1 -0
  27. package/dist/commands/index.d.ts +21 -0
  28. package/dist/commands/index.js +22 -0
  29. package/dist/commands/init.d.ts +46 -0
  30. package/dist/commands/init.d.ts.map +1 -0
  31. package/dist/commands/init.js +131 -0
  32. package/dist/commands/init.js.map +1 -0
  33. package/dist/commands/memory.d.ts +243 -0
  34. package/dist/commands/memory.d.ts.map +1 -0
  35. package/dist/commands/memory.js +471 -0
  36. package/dist/commands/memory.js.map +1 -0
  37. package/dist/commands/migrate-config.d.ts +20 -0
  38. package/dist/commands/migrate-config.d.ts.map +1 -0
  39. package/dist/commands/migrate-config.js +48 -0
  40. package/dist/commands/migrate-config.js.map +1 -0
  41. package/dist/commands/migrate-export.d.ts +26 -0
  42. package/dist/commands/migrate-export.d.ts.map +1 -0
  43. package/dist/commands/migrate-export.js +67 -0
  44. package/dist/commands/migrate-export.js.map +1 -0
  45. package/dist/commands/migrate.d.ts +31 -0
  46. package/dist/commands/migrate.d.ts.map +1 -0
  47. package/dist/commands/migrate.js +59 -0
  48. package/dist/commands/migrate.js.map +1 -0
  49. package/dist/commands/pricing.d.ts +66 -0
  50. package/dist/commands/pricing.d.ts.map +1 -0
  51. package/dist/commands/pricing.js +128 -0
  52. package/dist/commands/pricing.js.map +1 -0
  53. package/dist/commands/secrets.d.ts +96 -0
  54. package/dist/commands/secrets.d.ts.map +1 -0
  55. package/dist/commands/secrets.js +182 -0
  56. package/dist/commands/secrets.js.map +1 -0
  57. package/dist/commands/skills.d.ts +58 -0
  58. package/dist/commands/skills.d.ts.map +1 -0
  59. package/dist/commands/skills.js +181 -0
  60. package/dist/commands/skills.js.map +1 -0
  61. package/dist/commands/start.d.ts +51 -0
  62. package/dist/commands/start.d.ts.map +1 -0
  63. package/dist/commands/start.js +122 -0
  64. package/dist/commands/start.js.map +1 -0
  65. package/dist/commands/storage.d.ts +110 -0
  66. package/dist/commands/storage.d.ts.map +1 -0
  67. package/dist/commands/storage.js +274 -0
  68. package/dist/commands/storage.js.map +1 -0
  69. package/dist/commands/telemetry.d.ts +21 -0
  70. package/dist/commands/telemetry.d.ts.map +1 -0
  71. package/dist/commands/telemetry.js +75 -0
  72. package/dist/commands/telemetry.js.map +1 -0
  73. package/dist/commands/token.d.ts +109 -0
  74. package/dist/commands/token.d.ts.map +1 -0
  75. package/dist/commands/token.js +246 -0
  76. package/dist/commands/token.js.map +1 -0
  77. package/dist/commands/tools-lint.d.ts +103 -0
  78. package/dist/commands/tools-lint.d.ts.map +1 -0
  79. package/dist/commands/tools-lint.js +240 -0
  80. package/dist/commands/tools-lint.js.map +1 -0
  81. package/dist/commands/traces.d.ts +33 -0
  82. package/dist/commands/traces.d.ts.map +1 -0
  83. package/dist/commands/traces.js +103 -0
  84. package/dist/commands/traces.js.map +1 -0
  85. package/dist/commands/triggers.d.ts +49 -0
  86. package/dist/commands/triggers.d.ts.map +1 -0
  87. package/dist/commands/triggers.js +129 -0
  88. package/dist/commands/triggers.js.map +1 -0
  89. package/dist/index.d.ts +46 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +47 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/internal/exit.js +17 -0
  94. package/dist/internal/exit.js.map +1 -0
  95. package/dist/internal/load-config.js +84 -0
  96. package/dist/internal/load-config.js.map +1 -0
  97. package/dist/internal/offline.d.ts +58 -0
  98. package/dist/internal/offline.d.ts.map +1 -0
  99. package/dist/internal/offline.js +72 -0
  100. package/dist/internal/offline.js.map +1 -0
  101. package/dist/internal/output.d.ts +56 -0
  102. package/dist/internal/output.d.ts.map +1 -0
  103. package/dist/internal/output.js +86 -0
  104. package/dist/internal/output.js.map +1 -0
  105. package/dist/internal/prompts.js +49 -0
  106. package/dist/internal/prompts.js.map +1 -0
  107. package/dist/internal/store-context.js +69 -0
  108. package/dist/internal/store-context.js.map +1 -0
  109. package/package.json +79 -0
@@ -0,0 +1,240 @@
1
+ import { brand, defaultJsonSink, defaultPrintSink, statusMarker } from "../internal/output.js";
2
+ import { EXIT_CODES } from "../internal/exit.js";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { isAbsolute, relative, resolve, sep } from "node:path";
5
+ import process from "node:process";
6
+ import { discoverToolCallsInSource, gradeTool, runToolRules } from "@graphorin/eslint-plugin";
7
+
8
+ //#region src/commands/tools-lint.ts
9
+ /**
10
+ * `graphorin tools lint` — RB-49 / suggested DEC-165.
11
+ *
12
+ * Discovers every `tool({...})` registration in the operator's project
13
+ * via static AST analysis (no runtime probe; no `tsc` invocation), runs
14
+ * the three RB-49 rules from `@graphorin/eslint-plugin`, computes the
15
+ * per-tool grader score (40 + 30 + 30 = 100 points), aggregates a
16
+ * structured report, and exits `1` when any tool falls below the
17
+ * `--threshold` (default `60`).
18
+ *
19
+ * ## Surface
20
+ *
21
+ * graphorin tools lint [--config <path>] [--threshold <n>]
22
+ * [--format <text|json>] [--source <pattern>]
23
+ *
24
+ * ## Grader rubric (RB-49 calibration — 40 + 30 + 30 = 100 points)
25
+ *
26
+ * - **description axis (0..40):** 0 if missing / placeholder / shorter
27
+ * than 20 chars; 16 if length >= 20; 24 if length >= 30; 32 if
28
+ * length >= 50; 40 if length >= 80.
29
+ * - **examples axis (0..30):** 0 if no examples or > 5; 12 base for
30
+ * the first example, +6 per additional, cap at 30. Subtract 6 per
31
+ * PII finding (cap at 0).
32
+ * - **parameter naming axis (0..30):** 30 base, deducted per finding.
33
+ * -30/N for ambiguous names; -10/N for numeric suffixes.
34
+ *
35
+ * ## Exit codes
36
+ *
37
+ * - `0` — every tool meets or exceeds the threshold.
38
+ * - `1` — at least one tool falls below the threshold.
39
+ * - `2` — invocation could not start (config missing, walker failed).
40
+ *
41
+ * The CLI re-uses the rule modules from `@graphorin/eslint-plugin` so
42
+ * the rule logic has a single source of truth — the per-tool grader,
43
+ * threshold gate, and report formatting are the only logic that lives
44
+ * in the CLI.
45
+ *
46
+ * @packageDocumentation
47
+ */
48
+ /** Default file glob honoured when no `--source` / `--config` is supplied. */
49
+ const DEFAULT_GLOB = "src/**/*.{ts,tsx}";
50
+ /**
51
+ * Run the discovery + grader pipeline. Returns the structured report
52
+ * the CLI emits to stdout.
53
+ *
54
+ * @stable
55
+ */
56
+ async function runToolsLint(options = {}) {
57
+ const cwd = options.cwd ?? process.cwd();
58
+ const threshold = options.threshold ?? 60;
59
+ const format = options.format ?? "text";
60
+ const sources = await collectSources(cwd, options);
61
+ const tools = [];
62
+ for (const { file, source } of sources) tools.push(...discoverToolCallsInSource(file, source));
63
+ const scored = [];
64
+ let failed = 0;
65
+ let passed = 0;
66
+ let totalFindings = 0;
67
+ const violations = [];
68
+ for (const tool of tools) {
69
+ const findings = runToolRules(tool);
70
+ const score = gradeTool(tool, findings);
71
+ scored.push(score);
72
+ totalFindings += findings.length;
73
+ if (score.score < threshold) {
74
+ failed += 1;
75
+ violations.push(Object.freeze({
76
+ toolName: score.toolName,
77
+ score: score.score,
78
+ threshold
79
+ }));
80
+ } else passed += 1;
81
+ }
82
+ const counterSink = options.counterSink ?? noopCounterSink;
83
+ for (const v of violations) counterSink(v);
84
+ const report = Object.freeze({
85
+ summary: Object.freeze({
86
+ totalTools: scored.length,
87
+ totalFindings,
88
+ threshold,
89
+ passed,
90
+ failed
91
+ }),
92
+ tools: Object.freeze(scored.map(toReportTool))
93
+ });
94
+ if (format === "json") (options.jsonPrint ?? defaultJsonSink)(report);
95
+ else emitTextReport(report, options);
96
+ if (failed > 0) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
97
+ return report;
98
+ }
99
+ function noopCounterSink(_event) {}
100
+ function toReportTool(score) {
101
+ return Object.freeze({
102
+ name: score.toolName,
103
+ source: `${score.file}:${score.line}`,
104
+ score: score.score,
105
+ axes: score.axes,
106
+ findings: Object.freeze(score.findings.map(toReportFinding))
107
+ });
108
+ }
109
+ function toReportFinding(finding) {
110
+ return Object.freeze({
111
+ rule: finding.rule,
112
+ kind: finding.kind,
113
+ severity: finding.severity,
114
+ message: finding.message,
115
+ location: Object.freeze({
116
+ file: finding.file,
117
+ line: finding.line
118
+ }),
119
+ ...finding.hint !== void 0 ? { hint: finding.hint } : {},
120
+ ...finding.matchedPattern !== void 0 ? { matchedPattern: finding.matchedPattern } : {}
121
+ });
122
+ }
123
+ function emitTextReport(report, options) {
124
+ const print = options.print ?? defaultPrintSink;
125
+ if (report.tools.length === 0) {
126
+ print(brand("no tool({...}) registrations were discovered."));
127
+ return;
128
+ }
129
+ for (const tool of report.tools) {
130
+ const ok = tool.score >= report.summary.threshold;
131
+ const mark = ok ? statusMarker("ok") : statusMarker("fail");
132
+ const flag = ok ? "" : " (BELOW THRESHOLD)";
133
+ print(brand(`${mark} ${tool.name} (score=${tool.score}/100)${flag}`));
134
+ print(` source: ${tool.source}`);
135
+ print(` axes: description=${tool.axes.description}, examples=${tool.axes.examples}, parameterNaming=${tool.axes.parameterNaming}`);
136
+ if (tool.findings.length === 0) print(` findings: none`);
137
+ else {
138
+ print(` findings:`);
139
+ for (const f of tool.findings) print(` [${f.severity}] ${f.rule}: ${f.message} (${f.location.file}:${f.location.line})`);
140
+ }
141
+ }
142
+ print(brand(`${report.summary.failed === 0 ? statusMarker("ok") : statusMarker("fail")} summary: ${report.summary.totalTools} tool(s), ${report.summary.totalFindings} finding(s), threshold=${report.summary.threshold}, passed=${report.summary.passed}, failed=${report.summary.failed}`));
143
+ }
144
+ async function collectSources(cwd, options) {
145
+ if (options.inlineSources !== void 0) return options.inlineSources;
146
+ const files = await walkGlob(cwd, options.source ?? await loadIncludeGlob(cwd, options.config) ?? DEFAULT_GLOB);
147
+ const out = [];
148
+ for (const file of files) try {
149
+ const source = await readFile(file, "utf8");
150
+ out.push({
151
+ file,
152
+ source
153
+ });
154
+ } catch {}
155
+ return out;
156
+ }
157
+ async function loadIncludeGlob(cwd, configPath) {
158
+ if (configPath === void 0) return void 0;
159
+ const abs = isAbsolute(configPath) ? configPath : resolve(cwd, configPath);
160
+ let raw;
161
+ try {
162
+ raw = await readFile(abs, "utf8");
163
+ } catch {
164
+ return;
165
+ }
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(stripJsonComments(raw));
169
+ } catch {
170
+ return;
171
+ }
172
+ return parsed.include?.[0];
173
+ }
174
+ function stripJsonComments(raw) {
175
+ return raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
176
+ }
177
+ async function walkGlob(cwd, pattern) {
178
+ const root = resolveGlobRoot(cwd, pattern);
179
+ const matcher = compileGlob(pattern);
180
+ const out = [];
181
+ await walkDir(root, async (file) => {
182
+ if (!matcher(relative(cwd, file))) return;
183
+ out.push(file);
184
+ });
185
+ return out.sort();
186
+ }
187
+ function resolveGlobRoot(cwd, pattern) {
188
+ const idx = pattern.indexOf("*");
189
+ if (idx < 0) return resolve(cwd, pattern);
190
+ return resolve(cwd, pattern.slice(0, idx));
191
+ }
192
+ function compileGlob(pattern) {
193
+ if (pattern.length > 4096) throw new Error(`compileGlob: pattern is too long (${pattern.length} > 4096 chars).`);
194
+ const BRACE_OPEN = "";
195
+ const BRACE_CLOSE = "";
196
+ const BRACE_SEP = "";
197
+ const GLOBSTAR = "";
198
+ const STAR = "";
199
+ const QUESTION = "";
200
+ let tokenized = pattern.replace(/\{([^}]+)\}/g, (_m, group) => `${BRACE_OPEN}${group.split(",").join(BRACE_SEP)}${BRACE_CLOSE}`);
201
+ tokenized = tokenized.replace(/\*\*/g, GLOBSTAR).replace(/\*/g, STAR).replace(/\?/g, QUESTION);
202
+ const translated = tokenized.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(new RegExp(BRACE_OPEN, "g"), "(?:").replace(new RegExp(BRACE_CLOSE, "g"), ")").replace(new RegExp(BRACE_SEP, "g"), "|").replace(new RegExp(GLOBSTAR, "g"), ".*").replace(new RegExp(STAR, "g"), "[^/]*").replace(new RegExp(QUESTION, "g"), "[^/]");
203
+ const re = /* @__PURE__ */ new RegExp(`^${translated}$`);
204
+ return (rel) => {
205
+ const normalized = rel.split(sep).join("/");
206
+ return re.test(normalized);
207
+ };
208
+ }
209
+ async function walkDir(root, visit) {
210
+ let s;
211
+ try {
212
+ s = await stat(root);
213
+ } catch {
214
+ return;
215
+ }
216
+ if (!s.isDirectory()) {
217
+ if (s.isFile()) await visit(root);
218
+ return;
219
+ }
220
+ const queue = [root];
221
+ while (queue.length > 0) {
222
+ const dir = queue.shift();
223
+ let entries;
224
+ try {
225
+ entries = await (await import("node:fs/promises")).readdir(dir, { withFileTypes: true });
226
+ } catch {
227
+ continue;
228
+ }
229
+ for (const entry of entries) {
230
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
231
+ const next = `${dir}${sep}${entry.name}`;
232
+ if (entry.isDirectory()) queue.push(next);
233
+ else if (entry.isFile()) await visit(next);
234
+ }
235
+ }
236
+ }
237
+
238
+ //#endregion
239
+ export { runToolsLint };
240
+ //# sourceMappingURL=tools-lint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools-lint.js","names":["tools: DiscoveredTool[]","scored: ToolGraderScore[]","violations: ToolsLintThresholdViolation[]","report: ToolsLintReport","out: Array<{ file: string; source: string }>","raw: string","parsed: { readonly include?: ReadonlyArray<string> }","out: string[]","s: import('node:fs').Stats","queue: string[]","entries: import('node:fs').Dirent[]"],"sources":["../../src/commands/tools-lint.ts"],"sourcesContent":["/**\n * `graphorin tools lint` — RB-49 / suggested DEC-165.\n *\n * Discovers every `tool({...})` registration in the operator's project\n * via static AST analysis (no runtime probe; no `tsc` invocation), runs\n * the three RB-49 rules from `@graphorin/eslint-plugin`, computes the\n * per-tool grader score (40 + 30 + 30 = 100 points), aggregates a\n * structured report, and exits `1` when any tool falls below the\n * `--threshold` (default `60`).\n *\n * ## Surface\n *\n * graphorin tools lint [--config <path>] [--threshold <n>]\n * [--format <text|json>] [--source <pattern>]\n *\n * ## Grader rubric (RB-49 calibration — 40 + 30 + 30 = 100 points)\n *\n * - **description axis (0..40):** 0 if missing / placeholder / shorter\n * than 20 chars; 16 if length >= 20; 24 if length >= 30; 32 if\n * length >= 50; 40 if length >= 80.\n * - **examples axis (0..30):** 0 if no examples or > 5; 12 base for\n * the first example, +6 per additional, cap at 30. Subtract 6 per\n * PII finding (cap at 0).\n * - **parameter naming axis (0..30):** 30 base, deducted per finding.\n * -30/N for ambiguous names; -10/N for numeric suffixes.\n *\n * ## Exit codes\n *\n * - `0` — every tool meets or exceeds the threshold.\n * - `1` — at least one tool falls below the threshold.\n * - `2` — invocation could not start (config missing, walker failed).\n *\n * The CLI re-uses the rule modules from `@graphorin/eslint-plugin` so\n * the rule logic has a single source of truth — the per-tool grader,\n * threshold gate, and report formatting are the only logic that lives\n * in the CLI.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve, sep } from 'node:path';\nimport process from 'node:process';\n\nimport {\n type DiscoveredTool,\n discoverToolCallsInSource,\n gradeTool,\n type LintFinding,\n type LintFindingKind,\n runToolRules,\n type ToolGraderScore,\n} from '@graphorin/eslint-plugin';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultJsonSink,\n defaultPrintSink,\n statusMarker,\n} from '../internal/output.js';\n\n/**\n * Counter event emitted per below-threshold tool per invocation. The\n * CLI exposes a configurable sink so observability pipelines (Phase\n * 04) can wire the counter into Prometheus / OTLP without touching\n * the CLI runtime. Default: no-op.\n *\n * Mirrors the `tool.lint.threshold.violations.total{toolName,score,\n * threshold}` contract documented in RB-49 § Counter emission.\n *\n * @stable\n */\nexport interface ToolsLintThresholdViolation {\n readonly toolName: string;\n readonly score: number;\n readonly threshold: number;\n}\n\n/** @stable */\nexport type ToolsLintCounterSink = (event: ToolsLintThresholdViolation) => void;\n\n/** @stable */\nexport interface ToolsLintOptions extends CommonOutputOptions {\n /** Optional path to a `tsconfig.json` whose `include` overrides the file glob. */\n readonly config?: string;\n /** Minimum acceptable per-tool score. Default `60`. */\n readonly threshold?: number;\n /** Output format. Default `'text'`. */\n readonly format?: 'text' | 'json';\n /** Optional override of the file glob pattern. */\n readonly source?: string;\n /** Override `cwd`. Default `process.cwd()`. */\n readonly cwd?: string;\n /**\n * Test seam — supply a list of `(file, source)` pairs directly so\n * the test does not need to fish around the filesystem.\n */\n readonly inlineSources?: ReadonlyArray<{ readonly file: string; readonly source: string }>;\n /**\n * Optional sink for the `tool.lint.threshold.violations.total`\n * counter (RB-49). The CLI calls this once per below-threshold tool\n * per invocation. Default: no-op.\n */\n readonly counterSink?: ToolsLintCounterSink;\n}\n\n/**\n * Documented JSON shape emitted on `--format json`. The shape is\n * stable + forward-compatible with the `@eslint/mcp` industry\n * direction (per-finding `rule` / `severity` / `message` / `location`\n * mirror the ESLint LSP convention).\n *\n * @stable\n */\nexport interface ToolsLintReport {\n readonly summary: {\n readonly totalTools: number;\n readonly totalFindings: number;\n readonly threshold: number;\n readonly passed: number;\n readonly failed: number;\n };\n readonly tools: ReadonlyArray<ToolsLintReportTool>;\n}\n\n/** @stable */\nexport interface ToolsLintReportTool {\n readonly name: string;\n readonly source: string;\n readonly score: number;\n readonly axes: {\n readonly description: number;\n readonly examples: number;\n readonly parameterNaming: number;\n };\n readonly findings: ReadonlyArray<ToolsLintReportFinding>;\n}\n\n/** @stable */\nexport interface ToolsLintReportFinding {\n readonly rule: LintFinding['rule'];\n readonly kind: LintFindingKind;\n readonly severity: LintFinding['severity'];\n readonly message: string;\n readonly location: { readonly file: string; readonly line: number };\n readonly hint?: string;\n readonly matchedPattern?: string;\n}\n\n/** Default file glob honoured when no `--source` / `--config` is supplied. */\nconst DEFAULT_GLOB = 'src/**/*.{ts,tsx}';\n\n/**\n * Run the discovery + grader pipeline. Returns the structured report\n * the CLI emits to stdout.\n *\n * @stable\n */\nexport async function runToolsLint(options: ToolsLintOptions = {}): Promise<ToolsLintReport> {\n const cwd = options.cwd ?? process.cwd();\n const threshold = options.threshold ?? 60;\n const format = options.format ?? 'text';\n\n const sources = await collectSources(cwd, options);\n const tools: DiscoveredTool[] = [];\n for (const { file, source } of sources) {\n tools.push(...discoverToolCallsInSource(file, source));\n }\n\n const scored: ToolGraderScore[] = [];\n let failed = 0;\n let passed = 0;\n let totalFindings = 0;\n const violations: ToolsLintThresholdViolation[] = [];\n for (const tool of tools) {\n const findings = runToolRules(tool);\n const score = gradeTool(tool, findings);\n scored.push(score);\n totalFindings += findings.length;\n if (score.score < threshold) {\n failed += 1;\n violations.push(\n Object.freeze({\n toolName: score.toolName,\n score: score.score,\n threshold,\n }),\n );\n } else {\n passed += 1;\n }\n }\n\n // Emit the documented counter exactly once per below-threshold tool\n // per invocation. The default sink is a no-op so non-observability\n // hosts pay nothing; the standalone server's tracer wires its own\n // counter pipeline through `--counter-sink` in v0.2.\n const counterSink = options.counterSink ?? noopCounterSink;\n for (const v of violations) counterSink(v);\n\n const report: ToolsLintReport = Object.freeze({\n summary: Object.freeze({\n totalTools: scored.length,\n totalFindings,\n threshold,\n passed,\n failed,\n }),\n tools: Object.freeze(scored.map(toReportTool)),\n });\n\n if (format === 'json') {\n const sink = options.jsonPrint ?? defaultJsonSink;\n sink(report);\n } else {\n emitTextReport(report, options);\n }\n\n if (failed > 0) {\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n }\n\n return report;\n}\n\nfunction noopCounterSink(_event: ToolsLintThresholdViolation): void {\n /* no-op */\n}\n\nfunction toReportTool(score: ToolGraderScore): ToolsLintReportTool {\n return Object.freeze({\n name: score.toolName,\n source: `${score.file}:${score.line}`,\n score: score.score,\n axes: score.axes,\n findings: Object.freeze(score.findings.map(toReportFinding)),\n });\n}\n\nfunction toReportFinding(finding: LintFinding): ToolsLintReportFinding {\n return Object.freeze({\n rule: finding.rule,\n kind: finding.kind,\n severity: finding.severity,\n message: finding.message,\n location: Object.freeze({ file: finding.file, line: finding.line }),\n ...(finding.hint !== undefined ? { hint: finding.hint } : {}),\n ...(finding.matchedPattern !== undefined ? { matchedPattern: finding.matchedPattern } : {}),\n });\n}\n\nfunction emitTextReport(report: ToolsLintReport, options: ToolsLintOptions): void {\n const print = options.print ?? defaultPrintSink;\n if (report.tools.length === 0) {\n print(brand('no tool({...}) registrations were discovered.'));\n return;\n }\n for (const tool of report.tools) {\n const ok = tool.score >= report.summary.threshold;\n const mark = ok ? statusMarker('ok') : statusMarker('fail');\n const flag = ok ? '' : ' (BELOW THRESHOLD)';\n print(brand(`${mark} ${tool.name} (score=${tool.score}/100)${flag}`));\n print(` source: ${tool.source}`);\n print(\n ` axes: description=${tool.axes.description}, examples=${tool.axes.examples}, parameterNaming=${tool.axes.parameterNaming}`,\n );\n if (tool.findings.length === 0) {\n print(` findings: none`);\n } else {\n print(` findings:`);\n for (const f of tool.findings) {\n print(\n ` [${f.severity}] ${f.rule}: ${f.message} (${f.location.file}:${f.location.line})`,\n );\n }\n }\n }\n const tag = report.summary.failed === 0 ? statusMarker('ok') : statusMarker('fail');\n print(\n brand(\n `${tag} summary: ${report.summary.totalTools} tool(s), ${report.summary.totalFindings} finding(s), threshold=${report.summary.threshold}, passed=${report.summary.passed}, failed=${report.summary.failed}`,\n ),\n );\n}\n\nasync function collectSources(\n cwd: string,\n options: ToolsLintOptions,\n): Promise<ReadonlyArray<{ readonly file: string; readonly source: string }>> {\n if (options.inlineSources !== undefined) return options.inlineSources;\n\n const glob = options.source ?? (await loadIncludeGlob(cwd, options.config)) ?? DEFAULT_GLOB;\n const files = await walkGlob(cwd, glob);\n const out: Array<{ file: string; source: string }> = [];\n for (const file of files) {\n try {\n const source = await readFile(file, 'utf8');\n out.push({ file, source });\n } catch {\n // Best-effort — skip unreadable files.\n }\n }\n return out;\n}\n\nasync function loadIncludeGlob(\n cwd: string,\n configPath: string | undefined,\n): Promise<string | undefined> {\n if (configPath === undefined) return undefined;\n const abs = isAbsolute(configPath) ? configPath : resolve(cwd, configPath);\n let raw: string;\n try {\n raw = await readFile(abs, 'utf8');\n } catch {\n return undefined;\n }\n let parsed: { readonly include?: ReadonlyArray<string> };\n try {\n parsed = JSON.parse(stripJsonComments(raw)) as { readonly include?: ReadonlyArray<string> };\n } catch {\n return undefined;\n }\n return parsed.include?.[0];\n}\n\nfunction stripJsonComments(raw: string): string {\n // Lightweight block-comment + line-comment stripper. tsconfig.json\n // commonly carries `//` comments which JSON.parse rejects.\n return raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/(^|[^:])\\/\\/.*$/gm, '$1');\n}\n\nasync function walkGlob(cwd: string, pattern: string): Promise<string[]> {\n const root = resolveGlobRoot(cwd, pattern);\n const matcher = compileGlob(pattern);\n const out: string[] = [];\n await walkDir(root, async (file) => {\n const rel = relative(cwd, file);\n if (!matcher(rel)) return;\n out.push(file);\n });\n return out.sort();\n}\n\nfunction resolveGlobRoot(cwd: string, pattern: string): string {\n const idx = pattern.indexOf('*');\n if (idx < 0) return resolve(cwd, pattern);\n const prefix = pattern.slice(0, idx);\n return resolve(cwd, prefix);\n}\n\nfunction compileGlob(pattern: string): (rel: string) => boolean {\n // The translation is intentionally done in three phases so the\n // brace-expansion output (which includes literal `(?:...)`) does not\n // collide with the `?` glob translation:\n //\n // 1. Tokenize the four glob constructs into ASCII placeholders\n // that are guaranteed not to appear in any host project's\n // filename.\n // 2. Escape every regex meta-character on the remaining literal\n // text.\n // 3. Substitute the placeholders for the equivalent regex\n // fragments (with `(?:...)` for brace expansion).\n if (pattern.length > 4096) {\n throw new Error(`compileGlob: pattern is too long (${pattern.length} > 4096 chars).`);\n }\n const BRACE_OPEN = '\\u0001';\n const BRACE_CLOSE = '\\u0002';\n const BRACE_SEP = '\\u0003';\n const GLOBSTAR = '\\u0004';\n const STAR = '\\u0005';\n const QUESTION = '\\u0006';\n\n // Phase 1: tokenize.\n let tokenized = pattern.replace(\n /\\{([^}]+)\\}/g,\n (_m, group: string) => `${BRACE_OPEN}${group.split(',').join(BRACE_SEP)}${BRACE_CLOSE}`,\n );\n tokenized = tokenized.replace(/\\*\\*/g, GLOBSTAR).replace(/\\*/g, STAR).replace(/\\?/g, QUESTION);\n\n // Phase 2: escape every remaining regex meta-char on the literal\n // text. The placeholders are non-printable and never collide.\n const escaped = tokenized.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n\n // Phase 3: substitute the placeholders for the regex fragments.\n const translated = escaped\n .replace(new RegExp(BRACE_OPEN, 'g'), '(?:')\n .replace(new RegExp(BRACE_CLOSE, 'g'), ')')\n .replace(new RegExp(BRACE_SEP, 'g'), '|')\n .replace(new RegExp(GLOBSTAR, 'g'), '.*')\n .replace(new RegExp(STAR, 'g'), '[^/]*')\n .replace(new RegExp(QUESTION, 'g'), '[^/]');\n\n const re = new RegExp(`^${translated}$`);\n return (rel: string) => {\n const normalized = rel.split(sep).join('/');\n return re.test(normalized);\n };\n}\n\nasync function walkDir(root: string, visit: (file: string) => Promise<void>): Promise<void> {\n let s: import('node:fs').Stats;\n try {\n s = await stat(root);\n } catch {\n return;\n }\n if (!s.isDirectory()) {\n if (s.isFile()) await visit(root);\n return;\n }\n const queue: string[] = [root];\n while (queue.length > 0) {\n const dir = queue.shift() as string;\n let entries: import('node:fs').Dirent[];\n try {\n entries = await (await import('node:fs/promises')).readdir(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') continue;\n const next = `${dir}${sep}${entry.name}`;\n if (entry.isDirectory()) queue.push(next);\n else if (entry.isFile()) await visit(next);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { CommonOutputOptions } from "../internal/output.js";
2
+
3
+ //#region src/commands/traces.d.ts
4
+
5
+ /** @stable */
6
+ interface TracesCommonOptions extends CommonOutputOptions {
7
+ readonly config?: string;
8
+ }
9
+ /** @stable */
10
+ interface TracesStatusResult {
11
+ readonly tableExists: boolean;
12
+ readonly rows: number;
13
+ readonly oldestStartedAt?: string;
14
+ readonly newestStartedAt?: string;
15
+ }
16
+ /** @stable */
17
+ declare function runTracesStatus(options?: TracesCommonOptions): Promise<TracesStatusResult>;
18
+ /** @stable */
19
+ interface TracesPruneOptions extends TracesCommonOptions {
20
+ /** ISO date / epoch ms cutoff. Required so the helper never silently
21
+ * empties the table. */
22
+ readonly before: string;
23
+ }
24
+ /** @stable */
25
+ interface TracesPruneResult {
26
+ readonly removed: number;
27
+ readonly cutoff: string;
28
+ }
29
+ /** @stable */
30
+ declare function runTracesPrune(options: TracesPruneOptions): Promise<TracesPruneResult>;
31
+ //#endregion
32
+ export { TracesCommonOptions, TracesPruneOptions, TracesPruneResult, TracesStatusResult, runTracesPrune, runTracesStatus };
33
+ //# sourceMappingURL=traces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traces.d.ts","names":[],"sources":["../../src/commands/traces.ts"],"sourcesContent":[],"mappings":";;;;;UA0BiB,mBAAA,SAA4B;;;;UAK5B,kBAAA;;;;;;;iBAQK,eAAA,WACX,sBACR,QAAQ;;UAyCM,kBAAA,SAA2B;;;;;;UAO3B,iBAAA;;;;;iBAMK,cAAA,UAAwB,qBAAqB,QAAQ"}
@@ -0,0 +1,103 @@
1
+ import { brand, defaultPrintSink, emitReport, statusMarker } from "../internal/output.js";
2
+ import { openStoreContext } from "../internal/store-context.js";
3
+
4
+ //#region src/commands/traces.ts
5
+ /**
6
+ * `graphorin traces` — operate on the local trace cache.
7
+ *
8
+ * Surface (per Phase 15 § Traces):
9
+ *
10
+ * - `graphorin traces status` — count rows + report TTL config.
11
+ * - `graphorin traces prune [--before <date>]` — manual TTL
12
+ * enforcement.
13
+ *
14
+ * The trace cache lives in the SQLite store under the `traces` table
15
+ * (registered in Phase 04). This command is read + delete only — the
16
+ * trace producer is the running `@graphorin/observability` exporter.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ /** @stable */
21
+ async function runTracesStatus(options = {}) {
22
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
23
+ try {
24
+ const conn = ctx.store.connection;
25
+ const exists = tableExists(conn, "traces");
26
+ let out;
27
+ if (exists) {
28
+ const oldest = pickIso(conn, "SELECT MIN(started_at) AS started_at FROM traces");
29
+ const newest = pickIso(conn, "SELECT MAX(started_at) AS started_at FROM traces");
30
+ out = Object.freeze({
31
+ tableExists: true,
32
+ rows: numericCount(conn, "SELECT COUNT(*) AS n FROM traces"),
33
+ ...oldest !== void 0 ? { oldestStartedAt: oldest } : {},
34
+ ...newest !== void 0 ? { newestStartedAt: newest } : {}
35
+ });
36
+ } else out = Object.freeze({
37
+ tableExists: false,
38
+ rows: 0
39
+ });
40
+ emitReport(options, out, () => {
41
+ const print = options.print ?? defaultPrintSink;
42
+ if (!out.tableExists) {
43
+ print(brand("traces table not found — the observability exporter has not initialized it yet."));
44
+ return;
45
+ }
46
+ print(brand(`traces: ${out.rows} row(s) (oldest=${out.oldestStartedAt ?? "-"}, newest=${out.newestStartedAt ?? "-"})`));
47
+ });
48
+ return out;
49
+ } finally {
50
+ await ctx.close();
51
+ }
52
+ }
53
+ /** @stable */
54
+ async function runTracesPrune(options) {
55
+ const cutoffMs = parseCutoff(options.before);
56
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
57
+ try {
58
+ if (!tableExists(ctx.store.connection, "traces")) {
59
+ const out$1 = Object.freeze({
60
+ removed: 0,
61
+ cutoff: new Date(cutoffMs).toISOString()
62
+ });
63
+ emitReport(options, out$1, () => {
64
+ (options.print ?? defaultPrintSink)(brand(`traces table not found; nothing to prune.`));
65
+ });
66
+ return out$1;
67
+ }
68
+ const result = ctx.store.connection.run("DELETE FROM traces WHERE started_at < ?", [cutoffMs]);
69
+ const out = Object.freeze({
70
+ removed: result.changes,
71
+ cutoff: new Date(cutoffMs).toISOString()
72
+ });
73
+ emitReport(options, out, () => {
74
+ (options.print ?? defaultPrintSink)(brand(`${out.removed > 0 ? statusMarker("ok") : statusMarker("info")} pruned ${out.removed} trace row(s) (cutoff=${out.cutoff}).`));
75
+ });
76
+ return out;
77
+ } finally {
78
+ await ctx.close();
79
+ }
80
+ }
81
+ function tableExists(conn, name) {
82
+ return conn.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [name]) !== void 0;
83
+ }
84
+ function numericCount(conn, sql) {
85
+ const row = conn.get(sql);
86
+ return typeof row?.n === "number" ? row.n : 0;
87
+ }
88
+ function pickIso(conn, sql) {
89
+ const row = conn.get(sql);
90
+ if (row === void 0 || row.started_at === null) return void 0;
91
+ return new Date(row.started_at).toISOString();
92
+ }
93
+ function parseCutoff(input) {
94
+ const numeric = Number(input);
95
+ if (Number.isFinite(numeric) && numeric > 0) return numeric;
96
+ const ms = Date.parse(input);
97
+ if (!Number.isFinite(ms)) throw new Error(`[graphorin/cli] --before '${input}' is not a valid ISO date or epoch-ms value.`);
98
+ return ms;
99
+ }
100
+
101
+ //#endregion
102
+ export { runTracesPrune, runTracesStatus };
103
+ //# sourceMappingURL=traces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traces.js","names":["out: TracesStatusResult","out: TracesPruneResult","out"],"sources":["../../src/commands/traces.ts"],"sourcesContent":["/**\n * `graphorin traces` — operate on the local trace cache.\n *\n * Surface (per Phase 15 § Traces):\n *\n * - `graphorin traces status` — count rows + report TTL config.\n * - `graphorin traces prune [--before <date>]` — manual TTL\n * enforcement.\n *\n * The trace cache lives in the SQLite store under the `traces` table\n * (registered in Phase 04). This command is read + delete only — the\n * trace producer is the running `@graphorin/observability` exporter.\n *\n * @packageDocumentation\n */\n\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\nimport { openStoreContext } from '../internal/store-context.js';\n\n/** @stable */\nexport interface TracesCommonOptions extends CommonOutputOptions {\n readonly config?: string;\n}\n\n/** @stable */\nexport interface TracesStatusResult {\n readonly tableExists: boolean;\n readonly rows: number;\n readonly oldestStartedAt?: string;\n readonly newestStartedAt?: string;\n}\n\n/** @stable */\nexport async function runTracesStatus(\n options: TracesCommonOptions = {},\n): Promise<TracesStatusResult> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const conn = ctx.store.connection;\n const exists = tableExists(conn, 'traces');\n let out: TracesStatusResult;\n if (exists) {\n const oldest = pickIso(conn, 'SELECT MIN(started_at) AS started_at FROM traces');\n const newest = pickIso(conn, 'SELECT MAX(started_at) AS started_at FROM traces');\n out = Object.freeze({\n tableExists: true,\n rows: numericCount(conn, 'SELECT COUNT(*) AS n FROM traces'),\n ...(oldest !== undefined ? { oldestStartedAt: oldest } : {}),\n ...(newest !== undefined ? { newestStartedAt: newest } : {}),\n });\n } else {\n out = Object.freeze({ tableExists: false, rows: 0 });\n }\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n if (!out.tableExists) {\n print(\n brand('traces table not found — the observability exporter has not initialized it yet.'),\n );\n return;\n }\n print(\n brand(\n `traces: ${out.rows} row(s) (oldest=${out.oldestStartedAt ?? '-'}, newest=${out.newestStartedAt ?? '-'})`,\n ),\n );\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TracesPruneOptions extends TracesCommonOptions {\n /** ISO date / epoch ms cutoff. Required so the helper never silently\n * empties the table. */\n readonly before: string;\n}\n\n/** @stable */\nexport interface TracesPruneResult {\n readonly removed: number;\n readonly cutoff: string;\n}\n\n/** @stable */\nexport async function runTracesPrune(options: TracesPruneOptions): Promise<TracesPruneResult> {\n const cutoffMs = parseCutoff(options.before);\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n if (!tableExists(ctx.store.connection, 'traces')) {\n const out: TracesPruneResult = Object.freeze({\n removed: 0,\n cutoff: new Date(cutoffMs).toISOString(),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`traces table not found; nothing to prune.`));\n });\n return out;\n }\n const result = ctx.store.connection.run('DELETE FROM traces WHERE started_at < ?', [cutoffMs]);\n const out: TracesPruneResult = Object.freeze({\n removed: result.changes,\n cutoff: new Date(cutoffMs).toISOString(),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n const mark = out.removed > 0 ? statusMarker('ok') : statusMarker('info');\n print(brand(`${mark} pruned ${out.removed} trace row(s) (cutoff=${out.cutoff}).`));\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\nfunction tableExists(\n conn: { get: <T = unknown>(s: string, p?: ReadonlyArray<unknown>) => T | undefined },\n name: string,\n): boolean {\n const row = conn.get<{ name: string }>(\n \"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?\",\n [name],\n );\n return row !== undefined;\n}\n\nfunction numericCount(\n conn: { get: <T = unknown>(s: string) => T | undefined },\n sql: string,\n): number {\n const row = conn.get<{ n: number }>(sql);\n return typeof row?.n === 'number' ? row.n : 0;\n}\n\nfunction pickIso(\n conn: { get: <T = unknown>(s: string) => T | undefined },\n sql: string,\n): string | undefined {\n const row = conn.get<{ started_at: number | null }>(sql);\n if (row === undefined || row.started_at === null) return undefined;\n return new Date(row.started_at).toISOString();\n}\n\nfunction parseCutoff(input: string): number {\n const numeric = Number(input);\n if (Number.isFinite(numeric) && numeric > 0) return numeric;\n const ms = Date.parse(input);\n if (!Number.isFinite(ms)) {\n throw new Error(\n `[graphorin/cli] --before '${input}' is not a valid ISO date or epoch-ms value.`,\n );\n }\n return ms;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuCA,eAAsB,gBACpB,UAA+B,EAAE,EACJ;CAC7B,MAAM,MAAM,MAAM,iBAAiB,EACjC,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE,EACnE,CAAC;AACF,KAAI;EACF,MAAM,OAAO,IAAI,MAAM;EACvB,MAAM,SAAS,YAAY,MAAM,SAAS;EAC1C,IAAIA;AACJ,MAAI,QAAQ;GACV,MAAM,SAAS,QAAQ,MAAM,mDAAmD;GAChF,MAAM,SAAS,QAAQ,MAAM,mDAAmD;AAChF,SAAM,OAAO,OAAO;IAClB,aAAa;IACb,MAAM,aAAa,MAAM,mCAAmC;IAC5D,GAAI,WAAW,SAAY,EAAE,iBAAiB,QAAQ,GAAG,EAAE;IAC3D,GAAI,WAAW,SAAY,EAAE,iBAAiB,QAAQ,GAAG,EAAE;IAC5D,CAAC;QAEF,OAAM,OAAO,OAAO;GAAE,aAAa;GAAO,MAAM;GAAG,CAAC;AAEtD,aAAW,SAAS,WAAW;GAC7B,MAAM,QAAQ,QAAQ,SAAS;AAC/B,OAAI,CAAC,IAAI,aAAa;AACpB,UACE,MAAM,kFAAkF,CACzF;AACD;;AAEF,SACE,MACE,WAAW,IAAI,KAAK,kBAAkB,IAAI,mBAAmB,IAAI,WAAW,IAAI,mBAAmB,IAAI,GACxG,CACF;IACD;AACF,SAAO;WACC;AACR,QAAM,IAAI,OAAO;;;;AAkBrB,eAAsB,eAAe,SAAyD;CAC5F,MAAM,WAAW,YAAY,QAAQ,OAAO;CAC5C,MAAM,MAAM,MAAM,iBAAiB,EACjC,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE,EACnE,CAAC;AACF,KAAI;AACF,MAAI,CAAC,YAAY,IAAI,MAAM,YAAY,SAAS,EAAE;GAChD,MAAMC,QAAyB,OAAO,OAAO;IAC3C,SAAS;IACT,QAAQ,IAAI,KAAK,SAAS,CAAC,aAAa;IACzC,CAAC;AACF,cAAW,SAASC,aAAW;AAE7B,KADc,QAAQ,SAAS,kBACzB,MAAM,4CAA4C,CAAC;KACzD;AACF,UAAOA;;EAET,MAAM,SAAS,IAAI,MAAM,WAAW,IAAI,2CAA2C,CAAC,SAAS,CAAC;EAC9F,MAAMD,MAAyB,OAAO,OAAO;GAC3C,SAAS,OAAO;GAChB,QAAQ,IAAI,KAAK,SAAS,CAAC,aAAa;GACzC,CAAC;AACF,aAAW,SAAS,WAAW;AAG7B,IAFc,QAAQ,SAAS,kBAEzB,MAAM,GADC,IAAI,UAAU,IAAI,aAAa,KAAK,GAAG,aAAa,OAAO,CACpD,UAAU,IAAI,QAAQ,wBAAwB,IAAI,OAAO,IAAI,CAAC;IAClF;AACF,SAAO;WACC;AACR,QAAM,IAAI,OAAO;;;AAIrB,SAAS,YACP,MACA,MACS;AAKT,QAJY,KAAK,IACf,oEACA,CAAC,KAAK,CACP,KACc;;AAGjB,SAAS,aACP,MACA,KACQ;CACR,MAAM,MAAM,KAAK,IAAmB,IAAI;AACxC,QAAO,OAAO,KAAK,MAAM,WAAW,IAAI,IAAI;;AAG9C,SAAS,QACP,MACA,KACoB;CACpB,MAAM,MAAM,KAAK,IAAmC,IAAI;AACxD,KAAI,QAAQ,UAAa,IAAI,eAAe,KAAM,QAAO;AACzD,QAAO,IAAI,KAAK,IAAI,WAAW,CAAC,aAAa;;AAG/C,SAAS,YAAY,OAAuB;CAC1C,MAAM,UAAU,OAAO,MAAM;AAC7B,KAAI,OAAO,SAAS,QAAQ,IAAI,UAAU,EAAG,QAAO;CACpD,MAAM,KAAK,KAAK,MAAM,MAAM;AAC5B,KAAI,CAAC,OAAO,SAAS,GAAG,CACtB,OAAM,IAAI,MACR,6BAA6B,MAAM,8CACpC;AAEH,QAAO"}
@@ -0,0 +1,49 @@
1
+ import { CommonOutputOptions } from "../internal/output.js";
2
+ import { TriggerState } from "@graphorin/core/contracts";
3
+
4
+ //#region src/commands/triggers.d.ts
5
+
6
+ /** @stable */
7
+ interface TriggersCommonOptions extends CommonOutputOptions {
8
+ readonly config?: string;
9
+ }
10
+ /** @stable */
11
+ declare function runTriggersList(options?: TriggersCommonOptions): Promise<ReadonlyArray<TriggerState>>;
12
+ /** @stable */
13
+ interface TriggersStatusOptions extends TriggersCommonOptions {
14
+ readonly id: string;
15
+ }
16
+ /** @stable */
17
+ declare function runTriggersStatus(options: TriggersStatusOptions): Promise<TriggerState | null>;
18
+ /** @stable */
19
+ interface TriggersFireOptions extends TriggersCommonOptions {
20
+ readonly id: string;
21
+ }
22
+ /** @stable */
23
+ declare function runTriggersFire(options: TriggersFireOptions): Promise<{
24
+ readonly fired: false;
25
+ readonly unsupported: true;
26
+ }>;
27
+ /** @stable */
28
+ interface TriggersDisableOptions extends TriggersCommonOptions {
29
+ readonly id: string;
30
+ }
31
+ /** @stable */
32
+ declare function runTriggersDisable(options: TriggersDisableOptions): Promise<TriggerState | null>;
33
+ /** @stable */
34
+ interface TriggersPruneOptions extends TriggersCommonOptions {
35
+ /**
36
+ * ISO date / epoch ms — drop disabled triggers whose `lastFiredAt`
37
+ * (or `createdAt`, when never fired) is older than this cutoff.
38
+ */
39
+ readonly before?: string;
40
+ }
41
+ /** @stable */
42
+ interface TriggersPruneResult {
43
+ readonly removed: ReadonlyArray<string>;
44
+ }
45
+ /** @stable */
46
+ declare function runTriggersPrune(options?: TriggersPruneOptions): Promise<TriggersPruneResult>;
47
+ //#endregion
48
+ export { TriggersCommonOptions, TriggersDisableOptions, TriggersFireOptions, TriggersPruneOptions, TriggersPruneResult, TriggersStatusOptions, runTriggersDisable, runTriggersFire, runTriggersList, runTriggersPrune, runTriggersStatus };
49
+ //# sourceMappingURL=triggers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triggers.d.ts","names":[],"sources":["../../src/commands/triggers.ts"],"sourcesContent":[],"mappings":";;;;;;AAqJU,UAlHO,qBAAA,SAA8B,mBAkHrC,CAAA;EA0BO,SAAA,MAAA,CAAA,EAAA,MAAqB;AAStC;AAKA;AACW,iBAtJW,eAAA,CAsJX,OAAA,CAAA,EArJA,qBAqJA,CAAA,EApJR,OAoJQ,CApJA,aAoJA,CApJc,YAoJd,CAAA,CAAA;;AACR,UA1Hc,qBAAA,SAA8B,qBA0H5C,CAAA;EAAO,SAAA,EAAA,EAAA,MAAA;;;iBArHY,iBAAA,UACX,wBACR,QAAQ;;UA6BM,mBAAA,SAA4B;;;;iBAKvB,eAAA,UACX,sBACR;;;;;UA8Bc,sBAAA,SAA+B;;;;iBAK1B,kBAAA,UACX,yBACR,QAAQ;;UA0BM,oBAAA,SAA6B;;;;;;;;UAS7B,mBAAA;oBACG;;;iBAIE,gBAAA,WACX,uBACR,QAAQ"}
@@ -0,0 +1,129 @@
1
+ import { brand, defaultPrintSink, emitReport, statusMarker } from "../internal/output.js";
2
+ import { EXIT_CODES } from "../internal/exit.js";
3
+ import { openStoreContext } from "../internal/store-context.js";
4
+
5
+ //#region src/commands/triggers.ts
6
+ /** @stable */
7
+ async function runTriggersList(options = {}) {
8
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
9
+ try {
10
+ const list = await ctx.store.triggers.list();
11
+ emitReport(options, list, () => {
12
+ const print = options.print ?? defaultPrintSink;
13
+ if (list.length === 0) {
14
+ print(brand("no triggers persisted."));
15
+ return;
16
+ }
17
+ print(brand(`${list.length} trigger(s):`));
18
+ for (const t of list) print(` ${t.disabled ? statusMarker("warn") : statusMarker("ok")} ${t.id} (kind=${t.kind}, spec=${t.spec}, next=${t.nextFireAt ?? "-"}, missed=${t.missedFires})`);
19
+ });
20
+ return list;
21
+ } finally {
22
+ await ctx.close();
23
+ }
24
+ }
25
+ /** @stable */
26
+ async function runTriggersStatus(options) {
27
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
28
+ try {
29
+ const state = await ctx.store.triggers.get(options.id);
30
+ emitReport(options, state, () => {
31
+ const print = options.print ?? defaultPrintSink;
32
+ if (state === null) {
33
+ print(brand(`trigger '${options.id}' not found.`));
34
+ process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
35
+ return;
36
+ }
37
+ print(brand(`trigger ${state.id}`));
38
+ print(` kind: ${state.kind}`);
39
+ print(` spec: ${state.spec}`);
40
+ print(` disabled: ${state.disabled}`);
41
+ print(` catchupPolicy: ${state.catchupPolicy}`);
42
+ print(` nextFireAt: ${state.nextFireAt ?? "-"}`);
43
+ print(` lastFiredAt: ${state.lastFiredAt ?? "-"}`);
44
+ print(` missedFires: ${state.missedFires}`);
45
+ });
46
+ return state;
47
+ } finally {
48
+ await ctx.close();
49
+ }
50
+ }
51
+ /** @stable */
52
+ async function runTriggersFire(options) {
53
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
54
+ try {
55
+ if (await ctx.store.triggers.get(options.id) === null) throw new Error(`[graphorin/cli] trigger '${options.id}' not found.`);
56
+ const result = {
57
+ fired: false,
58
+ unsupported: true
59
+ };
60
+ emitReport(options, result, () => {
61
+ (options.print ?? defaultPrintSink)(brand(`direct CLI fire is not wired yet — use the running server: POST /v1/triggers/${options.id}/fire (scope triggers:fire).`));
62
+ });
63
+ process.exitCode = EXIT_CODES.UNSUPPORTED;
64
+ return result;
65
+ } finally {
66
+ await ctx.close();
67
+ }
68
+ }
69
+ /** @stable */
70
+ async function runTriggersDisable(options) {
71
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
72
+ try {
73
+ const existing = await ctx.store.triggers.get(options.id);
74
+ if (existing === null) throw new Error(`[graphorin/cli] trigger '${options.id}' not found.`);
75
+ const next = {
76
+ ...existing,
77
+ disabled: true,
78
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
79
+ };
80
+ await ctx.store.triggers.upsert(next);
81
+ emitReport(options, next, () => {
82
+ (options.print ?? defaultPrintSink)(brand(`trigger '${options.id}' disabled.`));
83
+ });
84
+ return next;
85
+ } finally {
86
+ await ctx.close();
87
+ }
88
+ }
89
+ /** @stable */
90
+ async function runTriggersPrune(options = {}) {
91
+ const cutoff = options.before === void 0 ? 0 : parseCutoff(options.before);
92
+ const ctx = await openStoreContext({ ...options.config !== void 0 ? { config: options.config } : {} });
93
+ try {
94
+ const all = await ctx.store.triggers.list();
95
+ const removed = [];
96
+ for (const t of all) {
97
+ if (!t.disabled) continue;
98
+ const lastTouched = t.lastFiredAt ?? t.createdAt;
99
+ const lastTouchedMs = Date.parse(lastTouched);
100
+ if (Number.isFinite(lastTouchedMs) && lastTouchedMs >= cutoff) continue;
101
+ await ctx.store.triggers.remove(t.id);
102
+ removed.push(t.id);
103
+ }
104
+ const out = Object.freeze({ removed: Object.freeze(removed) });
105
+ emitReport(options, out, () => {
106
+ const print = options.print ?? defaultPrintSink;
107
+ if (out.removed.length === 0) {
108
+ print(brand("no orphan triggers found."));
109
+ return;
110
+ }
111
+ print(brand(`pruned ${out.removed.length} orphan trigger(s):`));
112
+ for (const id of out.removed) print(` - ${id}`);
113
+ });
114
+ return out;
115
+ } finally {
116
+ await ctx.close();
117
+ }
118
+ }
119
+ function parseCutoff(input) {
120
+ const numeric = Number(input);
121
+ if (Number.isFinite(numeric) && numeric > 0) return numeric;
122
+ const ms = Date.parse(input);
123
+ if (!Number.isFinite(ms)) throw new Error(`[graphorin/cli] --before '${input}' is not a valid ISO date or epoch-ms value.`);
124
+ return ms;
125
+ }
126
+
127
+ //#endregion
128
+ export { runTriggersDisable, runTriggersFire, runTriggersList, runTriggersPrune, runTriggersStatus };
129
+ //# sourceMappingURL=triggers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triggers.js","names":["next: TriggerState","removed: string[]","out: TriggersPruneResult"],"sources":["../../src/commands/triggers.ts"],"sourcesContent":["/**\n * `graphorin triggers` — operate on the durable trigger registry.\n *\n * Surface (per Phase 15 § Triggers):\n *\n * - `graphorin triggers list` — every persisted trigger.\n * - `graphorin triggers status <id>` — single-trigger detail.\n * - `graphorin triggers fire <id>` — operator-fired side-effect (admin\n * only). The CLI persists a fire-now signal into `trigger_admin`\n * that the running scheduler picks up; library-mode triggers\n * can also be fired by re-running the host process.\n * - `graphorin triggers disable <id>` — flip the `disabled` column.\n * - `graphorin triggers prune` — drop orphan rows whose callbackRef\n * no longer exists in the running registry. Without a running\n * server the CLI cannot tell which triggers are orphans, so the\n * helper deletes only triggers whose `disabled` column is true and\n * whose `lastFiredAt` is older than the cutoff supplied via\n * `--before <ISO>`.\n *\n * @packageDocumentation\n */\n\nimport type { TriggerState } from '@graphorin/core/contracts';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\nimport { openStoreContext } from '../internal/store-context.js';\n\n/** @stable */\nexport interface TriggersCommonOptions extends CommonOutputOptions {\n readonly config?: string;\n}\n\n/** @stable */\nexport async function runTriggersList(\n options: TriggersCommonOptions = {},\n): Promise<ReadonlyArray<TriggerState>> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const list = await ctx.store.triggers.list();\n emitReport(options, list, () => {\n const print = options.print ?? defaultPrintSink;\n if (list.length === 0) {\n print(brand('no triggers persisted.'));\n return;\n }\n print(brand(`${list.length} trigger(s):`));\n for (const t of list) {\n const mark = t.disabled ? statusMarker('warn') : statusMarker('ok');\n print(\n ` ${mark} ${t.id} (kind=${t.kind}, spec=${t.spec}, next=${t.nextFireAt ?? '-'}, missed=${t.missedFires})`,\n );\n }\n });\n return list;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TriggersStatusOptions extends TriggersCommonOptions {\n readonly id: string;\n}\n\n/** @stable */\nexport async function runTriggersStatus(\n options: TriggersStatusOptions,\n): Promise<TriggerState | null> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const state = await ctx.store.triggers.get(options.id);\n emitReport(options, state, () => {\n const print = options.print ?? defaultPrintSink;\n if (state === null) {\n print(brand(`trigger '${options.id}' not found.`));\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return;\n }\n print(brand(`trigger ${state.id}`));\n print(` kind: ${state.kind}`);\n print(` spec: ${state.spec}`);\n print(` disabled: ${state.disabled}`);\n print(` catchupPolicy: ${state.catchupPolicy}`);\n print(` nextFireAt: ${state.nextFireAt ?? '-'}`);\n print(` lastFiredAt: ${state.lastFiredAt ?? '-'}`);\n print(` missedFires: ${state.missedFires}`);\n });\n return state;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TriggersFireOptions extends TriggersCommonOptions {\n readonly id: string;\n}\n\n/** @stable */\nexport async function runTriggersFire(\n options: TriggersFireOptions,\n): Promise<{ readonly fired: false; readonly unsupported: true }> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const state = await ctx.store.triggers.get(options.id);\n if (state === null) {\n throw new Error(`[graphorin/cli] trigger '${options.id}' not found.`);\n }\n // IP-4: the old implementation queued a row into a `trigger_admin`\n // table NOTHING polled and reported success — the fire never\n // happened. Until a daemon-side poll exists, the honest answer is\n // UNSUPPORTED with the working alternative.\n const result = { fired: false, unsupported: true } as const;\n emitReport(options, result, () => {\n const print = options.print ?? defaultPrintSink;\n print(\n brand(\n `direct CLI fire is not wired yet — use the running server: POST /v1/triggers/${options.id}/fire (scope triggers:fire).`,\n ),\n );\n });\n process.exitCode = EXIT_CODES.UNSUPPORTED;\n return result;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TriggersDisableOptions extends TriggersCommonOptions {\n readonly id: string;\n}\n\n/** @stable */\nexport async function runTriggersDisable(\n options: TriggersDisableOptions,\n): Promise<TriggerState | null> {\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const existing = await ctx.store.triggers.get(options.id);\n if (existing === null) {\n throw new Error(`[graphorin/cli] trigger '${options.id}' not found.`);\n }\n const next: TriggerState = {\n ...existing,\n disabled: true,\n updatedAt: new Date().toISOString(),\n };\n await ctx.store.triggers.upsert(next);\n emitReport(options, next, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`trigger '${options.id}' disabled.`));\n });\n return next;\n } finally {\n await ctx.close();\n }\n}\n\n/** @stable */\nexport interface TriggersPruneOptions extends TriggersCommonOptions {\n /**\n * ISO date / epoch ms — drop disabled triggers whose `lastFiredAt`\n * (or `createdAt`, when never fired) is older than this cutoff.\n */\n readonly before?: string;\n}\n\n/** @stable */\nexport interface TriggersPruneResult {\n readonly removed: ReadonlyArray<string>;\n}\n\n/** @stable */\nexport async function runTriggersPrune(\n options: TriggersPruneOptions = {},\n): Promise<TriggersPruneResult> {\n const cutoff = options.before === undefined ? 0 : parseCutoff(options.before);\n const ctx = await openStoreContext({\n ...(options.config !== undefined ? { config: options.config } : {}),\n });\n try {\n const all = await ctx.store.triggers.list();\n const removed: string[] = [];\n for (const t of all) {\n if (!t.disabled) continue;\n const lastTouched = t.lastFiredAt ?? t.createdAt;\n const lastTouchedMs = Date.parse(lastTouched);\n if (Number.isFinite(lastTouchedMs) && lastTouchedMs >= cutoff) continue;\n await ctx.store.triggers.remove(t.id);\n removed.push(t.id);\n }\n const out: TriggersPruneResult = Object.freeze({ removed: Object.freeze(removed) });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n if (out.removed.length === 0) {\n print(brand('no orphan triggers found.'));\n return;\n }\n print(brand(`pruned ${out.removed.length} orphan trigger(s):`));\n for (const id of out.removed) print(` - ${id}`);\n });\n return out;\n } finally {\n await ctx.close();\n }\n}\n\nfunction parseCutoff(input: string): number {\n const numeric = Number(input);\n if (Number.isFinite(numeric) && numeric > 0) return numeric;\n const ms = Date.parse(input);\n if (!Number.isFinite(ms)) {\n throw new Error(\n `[graphorin/cli] --before '${input}' is not a valid ISO date or epoch-ms value.`,\n );\n }\n return ms;\n}\n"],"mappings":"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"}