@spekn/cli 1.0.0 → 1.0.2

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 (154) hide show
  1. package/README.md +58 -0
  2. package/dist/main.js +40540 -32176
  3. package/dist/prompts/governance-analysis.prompt.md +109 -0
  4. package/dist/resources/prompts/repo-analysis.prompt.md +28 -136
  5. package/dist/resources/prompts/repo-sync-analysis.prompt.md +31 -68
  6. package/dist/tui/chunk-4WEASLXY.mjs +3444 -0
  7. package/dist/tui/chunk-755CADEG.mjs +3401 -0
  8. package/dist/tui/chunk-BUJQVTY5.mjs +3409 -0
  9. package/dist/tui/chunk-BZKKMGFB.mjs +1959 -0
  10. package/dist/tui/chunk-DJYOBCNM.mjs +3159 -0
  11. package/dist/tui/chunk-GTFTFDY4.mjs +3417 -0
  12. package/dist/tui/chunk-IMEBD2KA.mjs +3444 -0
  13. package/dist/tui/chunk-IX6DR5SW.mjs +3433 -0
  14. package/dist/tui/chunk-JKFOY4IF.mjs +2003 -0
  15. package/dist/tui/chunk-OXXZ3O5L.mjs +3378 -0
  16. package/dist/tui/chunk-SHJNIAAJ.mjs +1697 -0
  17. package/dist/tui/chunk-V4SNDRUS.mjs +1666 -0
  18. package/dist/tui/chunk-VXVHNZST.mjs +1666 -0
  19. package/dist/tui/chunk-WCTSFKTA.mjs +3459 -0
  20. package/dist/tui/chunk-X2XP5ACW.mjs +3443 -0
  21. package/dist/tui/chunk-YUYJ7VBG.mjs +2029 -0
  22. package/dist/tui/chunk-ZM3EI5IA.mjs +3384 -0
  23. package/dist/tui/chunk-ZYOX64HP.mjs +1653 -0
  24. package/dist/tui/index.mjs +6999 -6938
  25. package/dist/tui/prompts/spec-creation-system.prompt.md +47 -0
  26. package/dist/tui/prompts/spec-refinement-system.prompt.md +72 -0
  27. package/dist/tui/use-session-store-63YUGUFA.mjs +8 -0
  28. package/dist/tui/use-session-store-ACO2SMJC.mjs +8 -0
  29. package/dist/tui/use-session-store-BVFDAWOB.mjs +8 -0
  30. package/dist/tui/use-session-store-DJIZ3FQZ.mjs +9 -0
  31. package/dist/tui/use-session-store-EAIQA4UG.mjs +9 -0
  32. package/dist/tui/use-session-store-EFBAXC3G.mjs +8 -0
  33. package/dist/tui/use-session-store-FJOR4KTG.mjs +8 -0
  34. package/dist/tui/use-session-store-IJE5KVOC.mjs +8 -0
  35. package/dist/tui/use-session-store-KGAFXCKI.mjs +8 -0
  36. package/dist/tui/use-session-store-KS4DPNDY.mjs +8 -0
  37. package/dist/tui/use-session-store-MMHJENNL.mjs +8 -0
  38. package/dist/tui/use-session-store-OZ6HC4I2.mjs +9 -0
  39. package/dist/tui/use-session-store-PTMWISNJ.mjs +8 -0
  40. package/dist/tui/use-session-store-VCDECQMW.mjs +8 -0
  41. package/dist/tui/use-session-store-VOK5ML5J.mjs +9 -0
  42. package/package.json +33 -13
  43. package/dist/__tests__/export-cli.test.d.ts +0 -1
  44. package/dist/__tests__/export-cli.test.js +0 -70
  45. package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
  46. package/dist/__tests__/tui-args-policy.test.js +0 -50
  47. package/dist/acp-S2MHZOAD.mjs +0 -23
  48. package/dist/acp-UCCI44JY.mjs +0 -25
  49. package/dist/auth/credentials-store.d.ts +0 -2
  50. package/dist/auth/credentials-store.js +0 -5
  51. package/dist/auth/device-flow.d.ts +0 -36
  52. package/dist/auth/device-flow.js +0 -189
  53. package/dist/auth/jwt.d.ts +0 -1
  54. package/dist/auth/jwt.js +0 -6
  55. package/dist/auth/session.d.ts +0 -67
  56. package/dist/auth/session.js +0 -86
  57. package/dist/auth-login.d.ts +0 -34
  58. package/dist/auth-login.js +0 -202
  59. package/dist/auth-logout.d.ts +0 -25
  60. package/dist/auth-logout.js +0 -115
  61. package/dist/auth-status.d.ts +0 -24
  62. package/dist/auth-status.js +0 -109
  63. package/dist/backlog-generate.d.ts +0 -11
  64. package/dist/backlog-generate.js +0 -308
  65. package/dist/backlog-health.d.ts +0 -11
  66. package/dist/backlog-health.js +0 -287
  67. package/dist/bridge-login.d.ts +0 -40
  68. package/dist/bridge-login.js +0 -277
  69. package/dist/chunk-3PAYRI4G.mjs +0 -2428
  70. package/dist/chunk-M4CS3A25.mjs +0 -2426
  71. package/dist/commands/auth/login.d.ts +0 -30
  72. package/dist/commands/auth/login.js +0 -164
  73. package/dist/commands/auth/logout.d.ts +0 -25
  74. package/dist/commands/auth/logout.js +0 -115
  75. package/dist/commands/auth/status.d.ts +0 -24
  76. package/dist/commands/auth/status.js +0 -109
  77. package/dist/commands/backlog/generate.d.ts +0 -11
  78. package/dist/commands/backlog/generate.js +0 -308
  79. package/dist/commands/backlog/health.d.ts +0 -11
  80. package/dist/commands/backlog/health.js +0 -287
  81. package/dist/commands/bridge/login.d.ts +0 -36
  82. package/dist/commands/bridge/login.js +0 -258
  83. package/dist/commands/export.d.ts +0 -35
  84. package/dist/commands/export.js +0 -485
  85. package/dist/commands/marketplace-export.d.ts +0 -21
  86. package/dist/commands/marketplace-export.js +0 -214
  87. package/dist/commands/project-clean.d.ts +0 -1
  88. package/dist/commands/project-clean.js +0 -126
  89. package/dist/commands/repo/common.d.ts +0 -105
  90. package/dist/commands/repo/common.js +0 -775
  91. package/dist/commands/repo/detach.d.ts +0 -2
  92. package/dist/commands/repo/detach.js +0 -120
  93. package/dist/commands/repo/register.d.ts +0 -21
  94. package/dist/commands/repo/register.js +0 -175
  95. package/dist/commands/repo/sync.d.ts +0 -22
  96. package/dist/commands/repo/sync.js +0 -873
  97. package/dist/commands/skills-import-local.d.ts +0 -16
  98. package/dist/commands/skills-import-local.js +0 -352
  99. package/dist/commands/spec/drift-check.d.ts +0 -3
  100. package/dist/commands/spec/drift-check.js +0 -186
  101. package/dist/commands/spec/frontmatter.d.ts +0 -11
  102. package/dist/commands/spec/frontmatter.js +0 -219
  103. package/dist/commands/spec/lint.d.ts +0 -11
  104. package/dist/commands/spec/lint.js +0 -499
  105. package/dist/commands/spec/parse.d.ts +0 -11
  106. package/dist/commands/spec/parse.js +0 -162
  107. package/dist/export.d.ts +0 -35
  108. package/dist/export.js +0 -485
  109. package/dist/main.d.ts +0 -1
  110. package/dist/marketplace-export.d.ts +0 -21
  111. package/dist/marketplace-export.js +0 -214
  112. package/dist/project-clean.d.ts +0 -1
  113. package/dist/project-clean.js +0 -126
  114. package/dist/project-context.d.ts +0 -99
  115. package/dist/project-context.js +0 -376
  116. package/dist/repo-common.d.ts +0 -101
  117. package/dist/repo-common.js +0 -671
  118. package/dist/repo-detach.d.ts +0 -2
  119. package/dist/repo-detach.js +0 -102
  120. package/dist/repo-ingest.d.ts +0 -29
  121. package/dist/repo-ingest.js +0 -305
  122. package/dist/repo-register.d.ts +0 -21
  123. package/dist/repo-register.js +0 -175
  124. package/dist/repo-sync.d.ts +0 -16
  125. package/dist/repo-sync.js +0 -152
  126. package/dist/resources/prompt-loader.d.ts +0 -1
  127. package/dist/resources/prompt-loader.js +0 -62
  128. package/dist/skills-import-local.d.ts +0 -16
  129. package/dist/skills-import-local.js +0 -352
  130. package/dist/spec-drift-check.d.ts +0 -3
  131. package/dist/spec-drift-check.js +0 -186
  132. package/dist/spec-frontmatter.d.ts +0 -11
  133. package/dist/spec-frontmatter.js +0 -219
  134. package/dist/spec-lint.d.ts +0 -11
  135. package/dist/spec-lint.js +0 -499
  136. package/dist/spec-parse.d.ts +0 -11
  137. package/dist/spec-parse.js +0 -162
  138. package/dist/stubs/dotenv.d.ts +0 -5
  139. package/dist/stubs/dotenv.js +0 -6
  140. package/dist/stubs/typeorm.d.ts +0 -22
  141. package/dist/stubs/typeorm.js +0 -28
  142. package/dist/tui-bundle.d.ts +0 -1
  143. package/dist/tui-bundle.js +0 -5
  144. package/dist/tui-entry.mjs +0 -1407
  145. package/dist/utils/cli-runtime.d.ts +0 -5
  146. package/dist/utils/cli-runtime.js +0 -22
  147. package/dist/utils/help-error.d.ts +0 -7
  148. package/dist/utils/help-error.js +0 -14
  149. package/dist/utils/interaction.d.ts +0 -19
  150. package/dist/utils/interaction.js +0 -93
  151. package/dist/utils/structured-log.d.ts +0 -7
  152. package/dist/utils/structured-log.js +0 -112
  153. package/dist/utils/trpc-url.d.ts +0 -4
  154. package/dist/utils/trpc-url.js +0 -15
@@ -1,16 +0,0 @@
1
- /**
2
- * skills import-local CLI command
3
- *
4
- * Imports SKILL.md files from a local directory into a Spekn project.
5
- *
6
- * Usage: spekn skills import-local <path> --project-id <uuid> [--namespace <ns>] [--api-url <url>]
7
- */
8
- import { CredentialsStore } from "../auth/credentials-store";
9
- interface Deps {
10
- stdout: (content: string) => void;
11
- stderr: (content: string) => void;
12
- credentialsStore: CredentialsStore;
13
- }
14
- export declare function runSkillsImportLocalCli(args: string[], deps?: Deps): Promise<number>;
15
- export declare function main(): Promise<void>;
16
- export {};
@@ -1,352 +0,0 @@
1
- "use strict";
2
- /**
3
- * skills import-local CLI command
4
- *
5
- * Imports SKILL.md files from a local directory into a Spekn project.
6
- *
7
- * Usage: spekn skills import-local <path> --project-id <uuid> [--namespace <ns>] [--api-url <url>]
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.runSkillsImportLocalCli = runSkillsImportLocalCli;
11
- exports.main = main;
12
- const node_fs_1 = require("node:fs");
13
- const node_path_1 = require("node:path");
14
- const client_1 = require("@trpc/client");
15
- const shared_1 = require("@spekn/shared");
16
- const credentials_store_1 = require("../auth/credentials-store");
17
- const project_context_1 = require("../project-context");
18
- const structured_log_1 = require("../utils/structured-log");
19
- const trpc_url_1 = require("../utils/trpc-url");
20
- const help_error_1 = require("../utils/help-error");
21
- const defaultDeps = {
22
- stdout: (content) => process.stdout.write(content),
23
- stderr: (content) => process.stderr.write(content),
24
- credentialsStore: new credentials_store_1.CredentialsStore(),
25
- };
26
- function printHelp(stderr) {
27
- stderr(`
28
- skills import-local - Import SKILL.md files from a local directory
29
-
30
- USAGE:
31
- spekn skills import-local <path> --project-id <uuid> [options]
32
-
33
- ARGUMENTS:
34
- <path> Directory containing SKILL.md files (searched recursively)
35
-
36
- OPTIONS:
37
- --project-id <uuid> Project ID to import skills into (optional if .spekn/context is present)
38
- --namespace <ns> Namespace for imported skills (default: directory name)
39
- --names <a,b,c> Comma-separated skill names to import (default: all)
40
- --api-url <url> API base URL (default: SPEKN_API_URL or https://app.spekn.com)
41
- --help Show this help message
42
-
43
- ENVIRONMENT:
44
- SPEKN_API_URL API base URL
45
- SPEKN_AUTH_TOKEN Bearer token for authentication
46
- SPEKN_ORGANIZATION_ID Organization ID header
47
-
48
- EXAMPLES:
49
- spekn skills import-local ./my-skills --project-id 11111111-1111-4111-8111-111111111111
50
- spekn skills import-local ./plugins/skills --project-id <uuid> --namespace my-org
51
- `);
52
- }
53
- function parseArgs(args) {
54
- let path = "";
55
- let projectId = "";
56
- let namespace = "";
57
- let names = [];
58
- let apiUrl = process.env.SPEKN_API_URL ?? "https://app.spekn.com";
59
- for (let index = 0; index < args.length; index++) {
60
- const arg = args[index];
61
- if (arg === "--help" || arg === "-h") {
62
- throw new help_error_1.HelpRequestedError();
63
- }
64
- if (arg === "--project-id" && args[index + 1]) {
65
- projectId = args[++index];
66
- continue;
67
- }
68
- if (arg?.startsWith("--project-id=")) {
69
- projectId = arg.slice("--project-id=".length);
70
- continue;
71
- }
72
- if (arg === "--namespace" && args[index + 1]) {
73
- namespace = args[++index];
74
- continue;
75
- }
76
- if (arg?.startsWith("--namespace=")) {
77
- namespace = arg.slice("--namespace=".length);
78
- continue;
79
- }
80
- if (arg === "--names" && args[index + 1]) {
81
- names = args[++index].split(",").map((n) => n.trim()).filter(Boolean);
82
- continue;
83
- }
84
- if (arg?.startsWith("--names=")) {
85
- names = arg.slice("--names=".length).split(",").map((n) => n.trim()).filter(Boolean);
86
- continue;
87
- }
88
- if (arg === "--api-url" && args[index + 1]) {
89
- apiUrl = args[++index];
90
- continue;
91
- }
92
- if (arg?.startsWith("--api-url=")) {
93
- apiUrl = arg.slice("--api-url=".length);
94
- continue;
95
- }
96
- // Positional argument: path
97
- if (!arg?.startsWith("--") && !path) {
98
- path = arg;
99
- }
100
- }
101
- if (!path) {
102
- throw new Error("Missing required argument: <path>");
103
- }
104
- const resolvedPath = (0, node_path_1.resolve)(path);
105
- if (!namespace) {
106
- namespace = (0, node_path_1.basename)(resolvedPath);
107
- }
108
- return { path: resolvedPath, projectId, namespace, names, apiUrl };
109
- }
110
- /**
111
- * Recursively find all SKILL.md files under a directory.
112
- */
113
- function findSkillFiles(dir) {
114
- const results = [];
115
- const entries = (0, node_fs_1.readdirSync)(dir);
116
- for (const entry of entries) {
117
- const fullPath = (0, node_path_1.join)(dir, entry);
118
- const stat = (0, node_fs_1.statSync)(fullPath);
119
- if (stat.isDirectory()) {
120
- results.push(...findSkillFiles(fullPath));
121
- }
122
- else if (entry === "SKILL.md") {
123
- results.push(fullPath);
124
- }
125
- }
126
- return results;
127
- }
128
- /**
129
- * Recursively collect all files in a skill directory and return a map
130
- * of relative paths to file contents.
131
- */
132
- function collectSkillFiles(skillDir) {
133
- const files = {};
134
- function walk(dir) {
135
- const entries = (0, node_fs_1.readdirSync)(dir);
136
- for (const entry of entries) {
137
- const fullPath = (0, node_path_1.join)(dir, entry);
138
- const stat = (0, node_fs_1.statSync)(fullPath);
139
- if (stat.isDirectory()) {
140
- walk(fullPath);
141
- }
142
- else {
143
- const relPath = (0, node_path_1.relative)(skillDir, fullPath);
144
- files[relPath] = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
145
- }
146
- }
147
- }
148
- walk(skillDir);
149
- return files;
150
- }
151
- /**
152
- * Parse a SKILL.md file: extract YAML frontmatter and body content.
153
- */
154
- function parseSkillFile(filePath) {
155
- const content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
156
- const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
157
- if (!fmMatch?.[1]) {
158
- return { error: `${filePath}: no frontmatter found` };
159
- }
160
- // Simple YAML parsing without js-yaml dependency.
161
- // SKILL.md frontmatter is flat key-value, so we parse it manually.
162
- const yamlText = fmMatch[1];
163
- const frontmatter = {};
164
- let currentKey = "";
165
- let inNestedMap = false;
166
- const nestedMap = {};
167
- for (const line of yamlText.split("\n")) {
168
- // Nested map value (indented)
169
- if (inNestedMap && /^\s{2,}\S/.test(line)) {
170
- const kvMatch = /^\s+(\S+):\s*(.*)$/.exec(line);
171
- if (kvMatch) {
172
- nestedMap[kvMatch[1]] = kvMatch[2].replace(/^["']|["']$/g, "");
173
- }
174
- continue;
175
- }
176
- // Flush nested map if we were in one
177
- if (inNestedMap) {
178
- frontmatter[currentKey] = { ...nestedMap };
179
- for (const k of Object.keys(nestedMap))
180
- delete nestedMap[k];
181
- inNestedMap = false;
182
- }
183
- // Top-level key: value
184
- const topMatch = /^(\S+):\s*(.*)$/.exec(line);
185
- if (topMatch) {
186
- const key = topMatch[1];
187
- const value = topMatch[2].replace(/^["']|["']$/g, "");
188
- if (value === "" || value === undefined) {
189
- // Could be start of a nested map
190
- currentKey = key;
191
- inNestedMap = true;
192
- }
193
- else {
194
- frontmatter[key] = value;
195
- }
196
- }
197
- }
198
- // Flush last nested map
199
- if (inNestedMap) {
200
- frontmatter[currentKey] = { ...nestedMap };
201
- }
202
- // Extract readme (content after frontmatter)
203
- const readme = content.slice(fmMatch[0].length).trim();
204
- return { frontmatter, readme };
205
- }
206
- async function runSkillsImportLocalCli(args, deps = defaultDeps) {
207
- try {
208
- const options = parseArgs(args);
209
- (0, structured_log_1.appendCliStructuredLog)({
210
- source: "cli.skills.import-local",
211
- level: "info",
212
- message: "Starting skills import-local",
213
- details: { path: options.path, namespace: options.namespace, apiUrl: options.apiUrl },
214
- });
215
- // Find SKILL.md files
216
- const skillFiles = findSkillFiles(options.path);
217
- if (skillFiles.length === 0) {
218
- deps.stderr(`No SKILL.md files found under ${options.path}\n`);
219
- return 1;
220
- }
221
- deps.stdout(`Found ${skillFiles.length} SKILL.md file(s) in ${options.path}\n`);
222
- // Parse and validate each file
223
- const skills = [];
224
- const parseErrors = [];
225
- for (const filePath of skillFiles) {
226
- const result = parseSkillFile(filePath);
227
- if ("error" in result) {
228
- parseErrors.push(result.error);
229
- continue;
230
- }
231
- const validation = shared_1.SkillFrontmatterSchema.safeParse(result.frontmatter);
232
- if (!validation.success) {
233
- const issues = validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
234
- parseErrors.push(`${filePath}: ${issues.join("; ")}`);
235
- continue;
236
- }
237
- const fm = validation.data;
238
- const meta = fm.metadata;
239
- const skillDir = (0, node_path_1.dirname)(filePath);
240
- const files = collectSkillFiles(skillDir);
241
- skills.push({
242
- name: fm.name,
243
- description: fm.description,
244
- version: meta?.version || "0.0.0",
245
- namespace: options.namespace,
246
- readme: result.readme || undefined,
247
- license: fm.license,
248
- compatibility: fm.compatibility,
249
- author: meta?.author,
250
- tags: [],
251
- metadata: meta ? { ...meta } : undefined,
252
- files,
253
- });
254
- }
255
- if (parseErrors.length > 0) {
256
- deps.stderr(`\nParse errors:\n`);
257
- for (const err of parseErrors) {
258
- deps.stderr(` - ${err}\n`);
259
- }
260
- }
261
- // Filter by --names if provided
262
- if (options.names.length > 0) {
263
- const nameSet = new Set(options.names);
264
- const before = skills.length;
265
- const filtered = skills.filter((s) => nameSet.has(s.name));
266
- skills.length = 0;
267
- skills.push(...filtered);
268
- if (skills.length < before) {
269
- deps.stdout(`Filtered to ${skills.length} of ${before} skill(s) by --names\n`);
270
- }
271
- }
272
- if (skills.length === 0) {
273
- deps.stderr(`No valid skills to import.\n`);
274
- return 1;
275
- }
276
- deps.stdout(`\nImporting ${skills.length} skill(s)...\n`);
277
- // Auth
278
- const storedToken = await deps.credentialsStore.getValidToken();
279
- const authToken = storedToken ?? process.env.SPEKN_AUTH_TOKEN;
280
- const storedCreds = deps.credentialsStore.load();
281
- const context = (0, project_context_1.resolveProjectContext)({
282
- explicitProjectId: options.projectId,
283
- repoPath: process.cwd(),
284
- credentialsOrganizationId: storedCreds?.organizationId,
285
- envOrganizationId: process.env.SPEKN_ORGANIZATION_ID,
286
- });
287
- // tRPC client
288
- const client = (0, client_1.createTRPCProxyClient)({
289
- links: [
290
- (0, client_1.httpBatchLink)({
291
- url: (0, trpc_url_1.normalizeTrpcUrl)(options.apiUrl),
292
- headers: {
293
- "x-organization-id": context.organizationId,
294
- authorization: authToken ? `Bearer ${authToken}` : "",
295
- },
296
- }),
297
- ],
298
- });
299
- const result = await client.skills.importLocal.mutate({
300
- projectId: context.projectId,
301
- skills,
302
- });
303
- // Report
304
- if (result.imported.length > 0) {
305
- deps.stdout(`\nImported: ${result.imported.join(", ")}\n`);
306
- }
307
- if (result.updated.length > 0) {
308
- deps.stdout(`Updated: ${result.updated.join(", ")}\n`);
309
- }
310
- if (result.errors.length > 0) {
311
- deps.stderr(`\nServer errors:\n`);
312
- for (const err of result.errors) {
313
- deps.stderr(` - ${err}\n`);
314
- }
315
- }
316
- const total = result.imported.length + result.updated.length;
317
- deps.stdout(`\nDone. ${total} skill(s) processed.\n`);
318
- (0, structured_log_1.appendCliStructuredLog)({
319
- source: "cli.skills.import-local",
320
- level: result.errors.length > 0 ? "warn" : "info",
321
- message: "Skills import-local completed",
322
- details: {
323
- totalProcessed: total,
324
- imported: result.imported.length,
325
- updated: result.updated.length,
326
- errors: result.errors.length,
327
- },
328
- });
329
- return result.errors.length > 0 ? 1 : 0;
330
- }
331
- catch (error) {
332
- if (error instanceof help_error_1.HelpRequestedError) {
333
- printHelp(deps.stderr);
334
- return 0;
335
- }
336
- const message = error instanceof Error ? error.message : String(error);
337
- deps.stderr(`Error: ${message}\n`);
338
- (0, structured_log_1.appendCliStructuredLog)({
339
- source: "cli.skills.import-local",
340
- level: "error",
341
- message,
342
- });
343
- return 1;
344
- }
345
- }
346
- async function main() {
347
- const exitCode = await runSkillsImportLocalCli(process.argv.slice(2));
348
- process.exit(exitCode);
349
- }
350
- if (require.main === module) {
351
- void main();
352
- }
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- declare function main(argv?: string[]): Promise<number>;
3
- export { main };
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.main = main;
5
- const shared_1 = require("@spekn/shared");
6
- const cli_runtime_1 = require("../../utils/cli-runtime");
7
- async function columnExists(tableName, columnName) {
8
- const rows = (await shared_1.AppDataSource.query(`
9
- SELECT 1
10
- FROM information_schema.columns
11
- WHERE table_schema = current_schema()
12
- AND table_name = $1
13
- AND column_name = $2
14
- LIMIT 1
15
- `, [tableName, columnName]));
16
- return rows.length > 0;
17
- }
18
- function parseArgs(args) {
19
- const options = {};
20
- for (const arg of args) {
21
- if (arg.startsWith("--project-id=")) {
22
- options.projectId = arg.split("=")[1];
23
- }
24
- else if (arg.startsWith("--spec-id=")) {
25
- options.specId = arg.split("=")[1];
26
- }
27
- else if (arg === "--json" || arg === "-j") {
28
- options.json = true;
29
- }
30
- else if (arg === "--verbose" || arg === "-v") {
31
- options.verbose = true;
32
- }
33
- }
34
- return options;
35
- }
36
- function printHelp() {
37
- console.log(`
38
- spec-drift-check - Fail when task/spec bindings drift
39
-
40
- USAGE:
41
- npm run spec-drift-check [OPTIONS]
42
-
43
- OPTIONS:
44
- --project-id=<id> Filter by project ID
45
- --spec-id=<id> Filter by specification ID
46
- -j, --json Output JSON
47
- -v, --verbose Print individual drift rows
48
- -h, --help Show this help message
49
- `);
50
- }
51
- async function main(argv) {
52
- const args = argv ?? process.argv.slice(2);
53
- if ((0, cli_runtime_1.hasHelpFlag)(args)) {
54
- printHelp();
55
- return 0;
56
- }
57
- const options = parseArgs(args);
58
- const hasDbConfig = Boolean(process.env.DATABASE_URL ||
59
- process.env.DB_HOST ||
60
- process.env.DB_NAME ||
61
- process.env.POSTGRES_HOST);
62
- if (!hasDbConfig) {
63
- console.log("spec-drift-check: skipped (no database configuration found in environment).");
64
- return 0;
65
- }
66
- await shared_1.AppDataSource.initialize();
67
- try {
68
- const hasTaskSpecVersion = await columnExists("task", "specVersion");
69
- const hasTaskSpecRef = await columnExists("task", "specRef");
70
- const hasSpecVersion = await columnExists("specification", "version");
71
- const hasSpecRef = await columnExists("specification", "specRef");
72
- const filters = [];
73
- const params = [];
74
- if (options.projectId) {
75
- params.push(options.projectId);
76
- filters.push(`t."projectId" = $${params.length}`);
77
- }
78
- if (options.specId) {
79
- params.push(options.specId);
80
- filters.push(`t."specificationId" = $${params.length}`);
81
- }
82
- const whereClause = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
83
- const taskSpecVersionExpr = hasTaskSpecVersion
84
- ? `t."specVersion"`
85
- : `NULL::text`;
86
- const taskSpecRefExpr = hasTaskSpecRef ? `t."specRef"` : `NULL::text`;
87
- const currentSpecVersionExpr = hasSpecVersion
88
- ? `s."version"`
89
- : `NULL::text`;
90
- const currentSpecRefExpr = hasSpecRef ? `s."specRef"` : `NULL::text`;
91
- const rows = (await shared_1.AppDataSource.query(`
92
- SELECT
93
- t."id" AS "taskId",
94
- t."title" AS "taskTitle",
95
- t."specificationId" AS "specificationId",
96
- s."id" AS "specId",
97
- s."title" AS "specTitle",
98
- ${taskSpecVersionExpr} AS "taskSpecVersion",
99
- ${taskSpecRefExpr} AS "taskSpecRef",
100
- ${currentSpecVersionExpr} AS "currentSpecVersion",
101
- ${currentSpecRefExpr} AS "currentSpecRef"
102
- FROM "task" t
103
- LEFT JOIN "specification" s ON s."id" = t."specificationId"
104
- ${whereClause}
105
- `, params));
106
- const driftItems = rows
107
- .map((row) => {
108
- const driftReasons = [];
109
- if (!row.specId) {
110
- driftReasons.push("spec_missing");
111
- }
112
- if (!hasTaskSpecVersion || !row.taskSpecVersion) {
113
- driftReasons.push("task_spec_version_missing");
114
- }
115
- if (!hasTaskSpecRef || !row.taskSpecRef) {
116
- driftReasons.push("task_spec_ref_missing");
117
- }
118
- if (!hasSpecRef) {
119
- driftReasons.push("spec_ref_missing");
120
- }
121
- if (row.taskSpecVersion &&
122
- row.currentSpecVersion &&
123
- row.taskSpecVersion !== row.currentSpecVersion) {
124
- driftReasons.push("spec_version_drift");
125
- }
126
- if (row.taskSpecRef &&
127
- row.currentSpecRef &&
128
- row.taskSpecRef !== row.currentSpecRef) {
129
- driftReasons.push("spec_ref_drift");
130
- }
131
- return {
132
- taskId: row.taskId,
133
- taskTitle: row.taskTitle,
134
- specificationId: row.specificationId,
135
- specTitle: row.specTitle || "<missing specification>",
136
- taskSpecVersion: row.taskSpecVersion,
137
- currentSpecVersion: row.currentSpecVersion,
138
- taskSpecRef: row.taskSpecRef,
139
- currentSpecRef: row.currentSpecRef,
140
- driftReasons,
141
- };
142
- })
143
- .filter((item) => item.driftReasons.length > 0);
144
- const summary = {
145
- checkedTasks: rows.length,
146
- driftedTasks: driftItems.length,
147
- versionDrift: driftItems.filter((d) => d.driftReasons.includes("spec_version_drift")).length,
148
- refDrift: driftItems.filter((d) => d.driftReasons.includes("spec_ref_drift")).length,
149
- missingBindings: driftItems.filter((d) => d.driftReasons.includes("task_spec_ref_missing") ||
150
- d.driftReasons.includes("task_spec_version_missing")).length,
151
- missingSpecs: driftItems.filter((d) => d.driftReasons.includes("spec_missing")).length,
152
- };
153
- if (options.json) {
154
- console.log(JSON.stringify({ summary, items: driftItems }, null, 2));
155
- }
156
- else {
157
- console.log("Spec Drift Check");
158
- console.log(`Checked tasks: ${summary.checkedTasks}`);
159
- console.log(`Drifted tasks: ${summary.driftedTasks}`);
160
- console.log(`Version drift: ${summary.versionDrift}`);
161
- console.log(`Ref drift: ${summary.refDrift}`);
162
- console.log(`Missing bindings: ${summary.missingBindings}`);
163
- console.log(`Missing specs: ${summary.missingSpecs}`);
164
- if (options.verbose && driftItems.length > 0) {
165
- console.log("\nDrift items:");
166
- for (const item of driftItems) {
167
- console.log(`- ${item.taskTitle} (${item.taskId})`);
168
- console.log(` Spec: ${item.specTitle} (${item.specificationId})`);
169
- console.log(` Version: ${item.taskSpecVersion ?? "<missing>"} -> ${item.currentSpecVersion ?? "<missing>"}`);
170
- console.log(` Ref: ${item.taskSpecRef ?? "<missing>"} -> ${item.currentSpecRef ?? "<missing>"}`);
171
- console.log(` Reasons: ${item.driftReasons.join(", ")}`);
172
- }
173
- }
174
- }
175
- if (driftItems.length > 0) {
176
- return 2;
177
- }
178
- return 0;
179
- }
180
- finally {
181
- await shared_1.AppDataSource.destroy();
182
- }
183
- }
184
- if (require.main === module) {
185
- void (0, cli_runtime_1.runCliMain)(main, { errorPrefix: "spec-drift-check failed" });
186
- }
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * spec-frontmatter CLI Tool
4
- *
5
- * Parse and validate YAML frontmatter from a specification file.
6
- *
7
- * Usage: spekn spec frontmatter <spec-file-path> [OPTIONS]
8
- * Example: spekn spec frontmatter specs/SPECIFICATION.md
9
- */
10
- declare function main(argv?: string[]): number;
11
- export { main };