@spekn/cli 1.0.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 (159) hide show
  1. package/dist/__tests__/export-cli.test.d.ts +1 -0
  2. package/dist/__tests__/export-cli.test.js +70 -0
  3. package/dist/__tests__/tui-args-policy.test.d.ts +1 -0
  4. package/dist/__tests__/tui-args-policy.test.js +50 -0
  5. package/dist/acp-S2MHZOAD.mjs +23 -0
  6. package/dist/acp-UCCI44JY.mjs +25 -0
  7. package/dist/auth/credentials-store.d.ts +2 -0
  8. package/dist/auth/credentials-store.js +5 -0
  9. package/dist/auth/device-flow.d.ts +36 -0
  10. package/dist/auth/device-flow.js +189 -0
  11. package/dist/auth/jwt.d.ts +1 -0
  12. package/dist/auth/jwt.js +6 -0
  13. package/dist/auth/session.d.ts +67 -0
  14. package/dist/auth/session.js +86 -0
  15. package/dist/auth-login.d.ts +34 -0
  16. package/dist/auth-login.js +202 -0
  17. package/dist/auth-logout.d.ts +25 -0
  18. package/dist/auth-logout.js +115 -0
  19. package/dist/auth-status.d.ts +24 -0
  20. package/dist/auth-status.js +109 -0
  21. package/dist/backlog-generate.d.ts +11 -0
  22. package/dist/backlog-generate.js +308 -0
  23. package/dist/backlog-health.d.ts +11 -0
  24. package/dist/backlog-health.js +287 -0
  25. package/dist/bridge-login.d.ts +40 -0
  26. package/dist/bridge-login.js +277 -0
  27. package/dist/chunk-3PAYRI4G.mjs +2428 -0
  28. package/dist/chunk-M4CS3A25.mjs +2426 -0
  29. package/dist/commands/auth/login.d.ts +30 -0
  30. package/dist/commands/auth/login.js +164 -0
  31. package/dist/commands/auth/logout.d.ts +25 -0
  32. package/dist/commands/auth/logout.js +115 -0
  33. package/dist/commands/auth/status.d.ts +24 -0
  34. package/dist/commands/auth/status.js +109 -0
  35. package/dist/commands/backlog/generate.d.ts +11 -0
  36. package/dist/commands/backlog/generate.js +308 -0
  37. package/dist/commands/backlog/health.d.ts +11 -0
  38. package/dist/commands/backlog/health.js +287 -0
  39. package/dist/commands/bridge/login.d.ts +36 -0
  40. package/dist/commands/bridge/login.js +258 -0
  41. package/dist/commands/export.d.ts +35 -0
  42. package/dist/commands/export.js +485 -0
  43. package/dist/commands/marketplace-export.d.ts +21 -0
  44. package/dist/commands/marketplace-export.js +214 -0
  45. package/dist/commands/project-clean.d.ts +1 -0
  46. package/dist/commands/project-clean.js +126 -0
  47. package/dist/commands/repo/common.d.ts +105 -0
  48. package/dist/commands/repo/common.js +775 -0
  49. package/dist/commands/repo/detach.d.ts +2 -0
  50. package/dist/commands/repo/detach.js +120 -0
  51. package/dist/commands/repo/register.d.ts +21 -0
  52. package/dist/commands/repo/register.js +175 -0
  53. package/dist/commands/repo/sync.d.ts +22 -0
  54. package/dist/commands/repo/sync.js +873 -0
  55. package/dist/commands/skills-import-local.d.ts +16 -0
  56. package/dist/commands/skills-import-local.js +352 -0
  57. package/dist/commands/spec/drift-check.d.ts +3 -0
  58. package/dist/commands/spec/drift-check.js +186 -0
  59. package/dist/commands/spec/frontmatter.d.ts +11 -0
  60. package/dist/commands/spec/frontmatter.js +219 -0
  61. package/dist/commands/spec/lint.d.ts +11 -0
  62. package/dist/commands/spec/lint.js +499 -0
  63. package/dist/commands/spec/parse.d.ts +11 -0
  64. package/dist/commands/spec/parse.js +162 -0
  65. package/dist/export.d.ts +35 -0
  66. package/dist/export.js +485 -0
  67. package/dist/index.d.ts +11 -0
  68. package/dist/index.js +21 -0
  69. package/dist/main.d.ts +1 -0
  70. package/dist/main.js +115280 -0
  71. package/dist/marketplace-export.d.ts +21 -0
  72. package/dist/marketplace-export.js +214 -0
  73. package/dist/project-clean.d.ts +1 -0
  74. package/dist/project-clean.js +126 -0
  75. package/dist/project-context.d.ts +99 -0
  76. package/dist/project-context.js +376 -0
  77. package/dist/repo-common.d.ts +101 -0
  78. package/dist/repo-common.js +671 -0
  79. package/dist/repo-detach.d.ts +2 -0
  80. package/dist/repo-detach.js +102 -0
  81. package/dist/repo-ingest.d.ts +29 -0
  82. package/dist/repo-ingest.js +305 -0
  83. package/dist/repo-register.d.ts +21 -0
  84. package/dist/repo-register.js +175 -0
  85. package/dist/repo-sync.d.ts +16 -0
  86. package/dist/repo-sync.js +152 -0
  87. package/dist/resources/prompt-loader.d.ts +1 -0
  88. package/dist/resources/prompt-loader.js +62 -0
  89. package/dist/resources/prompts/README.md +21 -0
  90. package/dist/resources/prompts/prompts/repo-analysis.prompt.md +126 -0
  91. package/dist/resources/prompts/repo-analysis.prompt.md +151 -0
  92. package/dist/resources/prompts/repo-sync-analysis.prompt.md +85 -0
  93. package/dist/skills-import-local.d.ts +16 -0
  94. package/dist/skills-import-local.js +352 -0
  95. package/dist/spec-drift-check.d.ts +3 -0
  96. package/dist/spec-drift-check.js +186 -0
  97. package/dist/spec-frontmatter.d.ts +11 -0
  98. package/dist/spec-frontmatter.js +219 -0
  99. package/dist/spec-lint.d.ts +11 -0
  100. package/dist/spec-lint.js +499 -0
  101. package/dist/spec-parse.d.ts +11 -0
  102. package/dist/spec-parse.js +162 -0
  103. package/dist/stubs/dotenv.d.ts +5 -0
  104. package/dist/stubs/dotenv.js +6 -0
  105. package/dist/stubs/typeorm.d.ts +22 -0
  106. package/dist/stubs/typeorm.js +28 -0
  107. package/dist/tui/app.d.ts +7 -0
  108. package/dist/tui/app.js +122 -0
  109. package/dist/tui/args.d.ts +8 -0
  110. package/dist/tui/args.js +57 -0
  111. package/dist/tui/capabilities/policy.d.ts +7 -0
  112. package/dist/tui/capabilities/policy.js +64 -0
  113. package/dist/tui/components/frame.d.ts +8 -0
  114. package/dist/tui/components/frame.js +8 -0
  115. package/dist/tui/components/status-bar.d.ts +8 -0
  116. package/dist/tui/components/status-bar.js +8 -0
  117. package/dist/tui/index.d.ts +2 -0
  118. package/dist/tui/index.js +23 -0
  119. package/dist/tui/index.mjs +7563 -0
  120. package/dist/tui/keymap/use-global-keymap.d.ts +19 -0
  121. package/dist/tui/keymap/use-global-keymap.js +82 -0
  122. package/dist/tui/navigation/nav-items.d.ts +3 -0
  123. package/dist/tui/navigation/nav-items.js +18 -0
  124. package/dist/tui/screens/bridge.d.ts +8 -0
  125. package/dist/tui/screens/bridge.js +19 -0
  126. package/dist/tui/screens/decisions.d.ts +5 -0
  127. package/dist/tui/screens/decisions.js +28 -0
  128. package/dist/tui/screens/export.d.ts +5 -0
  129. package/dist/tui/screens/export.js +16 -0
  130. package/dist/tui/screens/home.d.ts +5 -0
  131. package/dist/tui/screens/home.js +33 -0
  132. package/dist/tui/screens/locked.d.ts +5 -0
  133. package/dist/tui/screens/locked.js +9 -0
  134. package/dist/tui/screens/specs.d.ts +5 -0
  135. package/dist/tui/screens/specs.js +31 -0
  136. package/dist/tui/services/client.d.ts +1 -0
  137. package/dist/tui/services/client.js +18 -0
  138. package/dist/tui/services/context-service.d.ts +19 -0
  139. package/dist/tui/services/context-service.js +246 -0
  140. package/dist/tui/shared-enums.d.ts +16 -0
  141. package/dist/tui/shared-enums.js +19 -0
  142. package/dist/tui/state/use-app-state.d.ts +35 -0
  143. package/dist/tui/state/use-app-state.js +177 -0
  144. package/dist/tui/types.d.ts +77 -0
  145. package/dist/tui/types.js +2 -0
  146. package/dist/tui-bundle.d.ts +1 -0
  147. package/dist/tui-bundle.js +5 -0
  148. package/dist/tui-entry.mjs +1407 -0
  149. package/dist/utils/cli-runtime.d.ts +5 -0
  150. package/dist/utils/cli-runtime.js +22 -0
  151. package/dist/utils/help-error.d.ts +7 -0
  152. package/dist/utils/help-error.js +14 -0
  153. package/dist/utils/interaction.d.ts +19 -0
  154. package/dist/utils/interaction.js +93 -0
  155. package/dist/utils/structured-log.d.ts +7 -0
  156. package/dist/utils/structured-log.js +112 -0
  157. package/dist/utils/trpc-url.d.ts +4 -0
  158. package/dist/utils/trpc-url.js +15 -0
  159. package/package.json +59 -0
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * backlog-generate CLI Tool
5
+ *
6
+ * Auto-generate tasks from specification anchors.
7
+ *
8
+ * Usage: npm run backlog-generate <spec-file-path> [OPTIONS]
9
+ * Example: npm run backlog-generate specs/WORKFLOW.md --project-id=<uuid>
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.main = main;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const agents_1 = require("@spekn/agents");
49
+ const shared_1 = require("@spekn/shared");
50
+ const shared_2 = require("@spekn/shared");
51
+ const shared_3 = require("@spekn/shared");
52
+ const cli_runtime_1 = require("../../utils/cli-runtime");
53
+ function parseArgs(args) {
54
+ const options = { dryRun: false, verbose: false };
55
+ let filePath;
56
+ for (let i = 0; i < args.length; i++) {
57
+ const arg = args[i];
58
+ if (arg.startsWith("--project-id=")) {
59
+ options.projectId = arg.split("=")[1];
60
+ }
61
+ else if (arg.startsWith("--spec-id=")) {
62
+ options.specId = arg.split("=")[1];
63
+ }
64
+ else if (arg.startsWith("--priority=")) {
65
+ const priority = arg.split("=")[1];
66
+ if (Object.values(shared_2.TaskPriority).includes(priority)) {
67
+ options.priority = priority;
68
+ }
69
+ }
70
+ else if (arg === "--dry-run" || arg === "-d") {
71
+ options.dryRun = true;
72
+ }
73
+ else if (arg === "--verbose" || arg === "-v") {
74
+ options.verbose = true;
75
+ }
76
+ else if (!arg.startsWith("-")) {
77
+ filePath = arg;
78
+ }
79
+ }
80
+ if (!filePath) {
81
+ throw new Error("missing required argument: <spec-file-path>");
82
+ }
83
+ return { filePath, options };
84
+ }
85
+ function printHelp() {
86
+ console.log(`
87
+ backlog-generate - Auto-generate tasks from specification anchors
88
+
89
+ USAGE:
90
+ npm run backlog-generate <spec-file-path> [OPTIONS]
91
+
92
+ ARGUMENTS:
93
+ <spec-file-path> Path to the specification markdown file
94
+
95
+ OPTIONS:
96
+ --project-id=<id> Project ID to associate tasks with (required for DB insert)
97
+ --spec-id=<id> Specification ID to associate tasks with (required for DB insert)
98
+ --priority=<level> Default task priority (low|medium|high|critical, default: medium)
99
+ -d, --dry-run Preview tasks without creating them
100
+ -v, --verbose Show detailed output
101
+ -h, --help Show this help message
102
+
103
+ EXAMPLES:
104
+ npm run backlog-generate specs/WORKFLOW.md --dry-run
105
+ npm run backlog-generate specs/WORKFLOW.md --project-id=abc123 --spec-id=def456
106
+ npm run backlog-generate specs/SPECIFICATION.md --priority=high --verbose
107
+ `);
108
+ }
109
+ function extractAcceptanceCriteria(content) {
110
+ const criteria = [];
111
+ // Look for "Acceptance Criteria" section
112
+ const lines = content.split("\n");
113
+ let inCriteriaSection = false;
114
+ for (const line of lines) {
115
+ if (line.match(/^\*\*Acceptance Criteria\*\*:|^##\s*Acceptance Criteria|^###\s*Acceptance Criteria/i)) {
116
+ inCriteriaSection = true;
117
+ continue;
118
+ }
119
+ // Stop at next section heading or bold marker
120
+ if (inCriteriaSection &&
121
+ (line.match(/^##[^#]|^\*\*[^*]+\*\*:/) || line.trim() === "---")) {
122
+ break;
123
+ }
124
+ // Extract list items
125
+ if (inCriteriaSection && line.match(/^[-*]\s+/)) {
126
+ const criterion = line.replace(/^[-*]\s+/, "").trim();
127
+ if (criterion) {
128
+ criteria.push(criterion);
129
+ }
130
+ }
131
+ }
132
+ // If no explicit criteria found, look for bullet points
133
+ if (criteria.length === 0) {
134
+ for (const line of lines) {
135
+ if (line.match(/^[-*]\s+/)) {
136
+ const item = line.replace(/^[-*]\s+/, "").trim();
137
+ if (item && !item.startsWith("**")) {
138
+ criteria.push(item);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return criteria;
144
+ }
145
+ function extractDependencies(content) {
146
+ const dependencies = [];
147
+ // Look for "Dependencies" section
148
+ const dependencyMatch = content.match(/\*\*Dependencies\*\*:([^\n]+)/i);
149
+ if (dependencyMatch) {
150
+ const dependencyText = dependencyMatch[1];
151
+ const dependencyAnchors = dependencyText.match(/#[a-z0-9._-]+/gi);
152
+ if (dependencyAnchors) {
153
+ dependencies.push(...dependencyAnchors.map((d) => d.replace(/^#/, "")));
154
+ }
155
+ }
156
+ return dependencies;
157
+ }
158
+ async function main(argv) {
159
+ const args = argv ?? process.argv.slice(2);
160
+ if ((0, cli_runtime_1.hasHelpFlag)(args)) {
161
+ printHelp();
162
+ return 0;
163
+ }
164
+ let filePath;
165
+ let options;
166
+ try {
167
+ ({ filePath, options } = parseArgs(args));
168
+ }
169
+ catch {
170
+ console.error("Error: Missing required argument <spec-file-path>");
171
+ printHelp();
172
+ return 1;
173
+ }
174
+ // Resolve file path
175
+ const resolvedPath = path.resolve(process.cwd(), filePath);
176
+ // Check if file exists
177
+ if (!fs.existsSync(resolvedPath)) {
178
+ console.error(`Error: File not found: ${resolvedPath}`);
179
+ return 1;
180
+ }
181
+ // Read file content
182
+ let content;
183
+ try {
184
+ content = fs.readFileSync(resolvedPath, "utf-8");
185
+ }
186
+ catch (error) {
187
+ console.error(`Error reading file: ${(0, cli_runtime_1.errorMessage)(error)}`);
188
+ return 1;
189
+ }
190
+ // Extract anchors
191
+ const anchors = (0, agents_1.extractAnchors)(content);
192
+ if (anchors.length === 0) {
193
+ console.log("No anchors found in the specification.");
194
+ return 0;
195
+ }
196
+ console.log(`\nFound ${anchors.length} anchor(s) in ${path.basename(filePath)}`);
197
+ // Generate tasks from anchors
198
+ const tasksToCreate = [];
199
+ for (const anchor of anchors) {
200
+ const anchorContent = (0, agents_1.extractAnchorContent)(anchor.anchor, content);
201
+ if (!anchorContent)
202
+ continue;
203
+ const acceptanceCriteria = extractAcceptanceCriteria(anchorContent);
204
+ const dependencies = extractDependencies(anchorContent);
205
+ // Extract first paragraph as description
206
+ const lines = anchorContent.split("\n").filter((l) => l.trim());
207
+ const descriptionLines = lines
208
+ .slice(1)
209
+ .filter((l) => !l.match(/^#{1,6}\s/) &&
210
+ !l.match(/^[-*]\s/) &&
211
+ !l.match(/^\*\*[^*]+\*\*:/) &&
212
+ l.trim() !== "---");
213
+ const description = descriptionLines.slice(0, 3).join("\n").trim() ||
214
+ `Implementation of ${anchor.anchor}`;
215
+ tasksToCreate.push({
216
+ title: anchor.headingText.replace(/^#+\s*#[a-z0-9._-]+\s*/i, "").trim() ||
217
+ anchor.anchor,
218
+ description,
219
+ anchor: anchor.anchor,
220
+ acceptanceCriteria: acceptanceCriteria.length > 0
221
+ ? acceptanceCriteria
222
+ : ["Implementation complete", "Tests passing"],
223
+ dependencies,
224
+ priority: options.priority || shared_2.TaskPriority.MEDIUM,
225
+ });
226
+ }
227
+ // Display preview
228
+ console.log(`\nGenerated ${tasksToCreate.length} task(s):\n`);
229
+ tasksToCreate.forEach((task, index) => {
230
+ console.log(`${index + 1}. ${task.title}`);
231
+ console.log(` Anchor: ${task.anchor}`);
232
+ console.log(` Priority: ${task.priority}`);
233
+ if (task.dependencies.length > 0) {
234
+ console.log(` Dependencies: ${task.dependencies.join(", ")}`);
235
+ }
236
+ if (options.verbose) {
237
+ console.log(` Description: ${task.description}`);
238
+ console.log(` Acceptance Criteria:`);
239
+ task.acceptanceCriteria.forEach((criterion) => {
240
+ console.log(` - ${criterion}`);
241
+ });
242
+ }
243
+ console.log();
244
+ });
245
+ // Create tasks in database if not dry-run
246
+ if (!options.dryRun) {
247
+ if (!options.projectId || !options.specId) {
248
+ console.error("\nError: --project-id and --spec-id are required for task creation");
249
+ console.log("Use --dry-run to preview without creating tasks");
250
+ return 1;
251
+ }
252
+ console.log("Connecting to database...");
253
+ await shared_1.AppDataSource.initialize();
254
+ try {
255
+ // Verify project and spec exist
256
+ const specification = await shared_1.AppDataSource.getRepository(shared_3.Specification).findOne({
257
+ where: { id: options.specId },
258
+ });
259
+ if (!specification) {
260
+ console.error(`Error: Specification with ID ${options.specId} not found`);
261
+ return 1;
262
+ }
263
+ if (specification.projectId !== options.projectId) {
264
+ console.error(`Error: Specification belongs to project ${specification.projectId}, not ${options.projectId}`);
265
+ return 1;
266
+ }
267
+ // Create tasks
268
+ const taskRepository = shared_1.AppDataSource.getRepository(shared_2.Task);
269
+ const createdTasks = [];
270
+ for (const taskData of tasksToCreate) {
271
+ const task = taskRepository.create({
272
+ title: taskData.title,
273
+ description: taskData.description,
274
+ status: shared_2.TaskStatus.OPEN,
275
+ priority: taskData.priority,
276
+ acceptanceCriteria: taskData.acceptanceCriteria,
277
+ dependencies: taskData.dependencies,
278
+ specAnchor: taskData.anchor,
279
+ specificationId: options.specId,
280
+ projectId: options.projectId,
281
+ });
282
+ const saved = await taskRepository.save(task);
283
+ createdTasks.push(saved);
284
+ }
285
+ console.log(`\nSuccessfully created ${createdTasks.length} task(s)!`);
286
+ if (options.verbose) {
287
+ console.log("\nCreated Task IDs:");
288
+ createdTasks.forEach((task, index) => {
289
+ console.log(` ${index + 1}. ${task.id} - ${task.title}`);
290
+ });
291
+ }
292
+ }
293
+ catch (error) {
294
+ console.error(`\nError creating tasks: ${(0, cli_runtime_1.errorMessage)(error)}`);
295
+ return 1;
296
+ }
297
+ finally {
298
+ await shared_1.AppDataSource.destroy();
299
+ }
300
+ }
301
+ else {
302
+ console.log("Dry-run mode: Tasks not created. Use --project-id and --spec-id to create tasks.");
303
+ }
304
+ return 0;
305
+ }
306
+ if (require.main === module) {
307
+ void (0, cli_runtime_1.runCliMain)(main, { errorPrefix: "backlog-generate failed" });
308
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * backlog-health CLI Tool
4
+ *
5
+ * Report backlog health metrics.
6
+ *
7
+ * Usage: npm run backlog-health [OPTIONS]
8
+ * Example: npm run backlog-health --project-id=<uuid>
9
+ */
10
+ declare function main(argv?: string[]): Promise<number>;
11
+ export { main };
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * backlog-health CLI Tool
5
+ *
6
+ * Report backlog health metrics.
7
+ *
8
+ * Usage: npm run backlog-health [OPTIONS]
9
+ * Example: npm run backlog-health --project-id=<uuid>
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.main = main;
13
+ const shared_1 = require("@spekn/shared");
14
+ const shared_2 = require("@spekn/shared");
15
+ const cli_runtime_1 = require("../../utils/cli-runtime");
16
+ function parseArgs(args) {
17
+ const options = {};
18
+ for (let i = 0; i < args.length; i++) {
19
+ const arg = args[i];
20
+ if (arg.startsWith("--project-id=")) {
21
+ options.projectId = arg.split("=")[1];
22
+ }
23
+ else if (arg.startsWith("--spec-id=")) {
24
+ options.specId = arg.split("=")[1];
25
+ }
26
+ else if (arg === "--verbose" || arg === "-v") {
27
+ options.verbose = true;
28
+ }
29
+ else if (arg === "--json" || arg === "-j") {
30
+ options.json = true;
31
+ }
32
+ }
33
+ return options;
34
+ }
35
+ function printHelp() {
36
+ console.log(`
37
+ backlog-health - Report backlog health metrics
38
+
39
+ USAGE:
40
+ npm run backlog-health [OPTIONS]
41
+
42
+ OPTIONS:
43
+ --project-id=<id> Filter by project ID
44
+ --spec-id=<id> Filter by specification ID
45
+ -v, --verbose Show detailed breakdown
46
+ -j, --json Output in JSON format
47
+ -h, --help Show this help message
48
+
49
+ EXAMPLES:
50
+ npm run backlog-health
51
+ npm run backlog-health --project-id=abc123
52
+ npm run backlog-health --verbose --json
53
+ `);
54
+ }
55
+ async function calculateMetrics(options) {
56
+ const taskRepository = shared_1.AppDataSource.getRepository(shared_2.Task);
57
+ // Build query
58
+ const queryBuilder = taskRepository.createQueryBuilder("task");
59
+ if (options.projectId) {
60
+ queryBuilder.andWhere("task.projectId = :projectId", {
61
+ projectId: options.projectId,
62
+ });
63
+ }
64
+ if (options.specId) {
65
+ queryBuilder.andWhere("task.specificationId = :specId", {
66
+ specId: options.specId,
67
+ });
68
+ }
69
+ const tasks = await queryBuilder.getMany();
70
+ // Fetch specifications with relations for drift detection
71
+ const tasksWithSpec = await taskRepository
72
+ .createQueryBuilder("task")
73
+ .leftJoinAndSelect("task.specification", "specification")
74
+ .where(options.projectId ? "task.projectId = :projectId" : "1=1", {
75
+ projectId: options.projectId,
76
+ })
77
+ .andWhere(options.specId ? "task.specificationId = :specId" : "1=1", {
78
+ specId: options.specId,
79
+ })
80
+ .getMany();
81
+ // Calculate spec version drift
82
+ const outdatedTasks = tasksWithSpec.filter((task) => {
83
+ if (!task.specification || !task.specVersion)
84
+ return false;
85
+ return task.specVersion !== task.specification.version;
86
+ });
87
+ const specVersionDrift = {
88
+ count: outdatedTasks.length,
89
+ outdatedTasks: outdatedTasks.map((task) => ({
90
+ id: task.id,
91
+ title: task.title,
92
+ taskSpecVersion: task.specVersion,
93
+ currentSpecVersion: task.specification?.version || "unknown",
94
+ specTitle: task.specification?.title || "unknown",
95
+ })),
96
+ };
97
+ // Calculate metrics
98
+ const total = tasks.length;
99
+ const byStatus = {
100
+ [shared_2.TaskStatus.OPEN]: 0,
101
+ [shared_2.TaskStatus.IN_PROGRESS]: 0,
102
+ [shared_2.TaskStatus.BLOCKED]: 0,
103
+ [shared_2.TaskStatus.COMPLETED]: 0,
104
+ [shared_2.TaskStatus.CANCELLED]: 0,
105
+ };
106
+ const completionTimes = [];
107
+ let tasksWithoutAnchors = 0;
108
+ let tasksWithDependencies = 0;
109
+ tasks.forEach((task) => {
110
+ byStatus[task.status]++;
111
+ if (!task.specAnchor) {
112
+ tasksWithoutAnchors++;
113
+ }
114
+ if (task.dependencies && task.dependencies.length > 0) {
115
+ tasksWithDependencies++;
116
+ }
117
+ // Calculate completion time for completed tasks
118
+ if (task.status === shared_2.TaskStatus.COMPLETED && task.completedAt) {
119
+ const completionTime = task.completedAt.getTime() - task.createdAt.getTime();
120
+ completionTimes.push(completionTime);
121
+ }
122
+ });
123
+ const byStatusPercentage = {
124
+ [shared_2.TaskStatus.OPEN]: total > 0 ? (byStatus[shared_2.TaskStatus.OPEN] / total) * 100 : 0,
125
+ [shared_2.TaskStatus.IN_PROGRESS]: total > 0 ? (byStatus[shared_2.TaskStatus.IN_PROGRESS] / total) * 100 : 0,
126
+ [shared_2.TaskStatus.BLOCKED]: total > 0 ? (byStatus[shared_2.TaskStatus.BLOCKED] / total) * 100 : 0,
127
+ [shared_2.TaskStatus.COMPLETED]: total > 0 ? (byStatus[shared_2.TaskStatus.COMPLETED] / total) * 100 : 0,
128
+ [shared_2.TaskStatus.CANCELLED]: total > 0 ? (byStatus[shared_2.TaskStatus.CANCELLED] / total) * 100 : 0,
129
+ };
130
+ const blockedCount = byStatus[shared_2.TaskStatus.BLOCKED];
131
+ const blockedPercentage = byStatusPercentage[shared_2.TaskStatus.BLOCKED];
132
+ const completedCount = byStatus[shared_2.TaskStatus.COMPLETED];
133
+ const completedPercentage = byStatusPercentage[shared_2.TaskStatus.COMPLETED];
134
+ // Calculate average completion time
135
+ let averageCompletionTimeHours;
136
+ if (completionTimes.length > 0) {
137
+ const avgMillis = completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length;
138
+ averageCompletionTimeHours = avgMillis / (1000 * 60 * 60);
139
+ }
140
+ // Find oldest open task
141
+ let oldestOpenTask;
142
+ const openTasks = tasks
143
+ .filter((t) => t.status === shared_2.TaskStatus.OPEN)
144
+ .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
145
+ if (openTasks.length > 0) {
146
+ const oldest = openTasks[0];
147
+ const ageInDays = (Date.now() - oldest.createdAt.getTime()) / (1000 * 60 * 60 * 24);
148
+ oldestOpenTask = {
149
+ id: oldest.id,
150
+ title: oldest.title,
151
+ createdAt: oldest.createdAt,
152
+ ageInDays,
153
+ };
154
+ }
155
+ return {
156
+ total,
157
+ byStatus,
158
+ byStatusPercentage,
159
+ blockedCount,
160
+ blockedPercentage,
161
+ completedCount,
162
+ completedPercentage,
163
+ averageCompletionTimeHours,
164
+ oldestOpenTask,
165
+ tasksWithoutAnchors,
166
+ tasksWithDependencies,
167
+ specVersionDrift,
168
+ };
169
+ }
170
+ function formatPercentage(value) {
171
+ return value.toFixed(1) + "%";
172
+ }
173
+ function formatHours(hours) {
174
+ if (hours < 1) {
175
+ return (hours * 60).toFixed(0) + " minutes";
176
+ }
177
+ else if (hours < 24) {
178
+ return hours.toFixed(1) + " hours";
179
+ }
180
+ else {
181
+ return (hours / 24).toFixed(1) + " days";
182
+ }
183
+ }
184
+ async function main(argv) {
185
+ const args = argv ?? process.argv.slice(2);
186
+ if ((0, cli_runtime_1.hasHelpFlag)(args)) {
187
+ printHelp();
188
+ return 0;
189
+ }
190
+ const options = parseArgs(args);
191
+ console.log("Connecting to database...");
192
+ await shared_1.AppDataSource.initialize();
193
+ try {
194
+ const metrics = await calculateMetrics(options);
195
+ if (options.json) {
196
+ console.log(JSON.stringify(metrics, null, 2));
197
+ }
198
+ else {
199
+ console.log("\n=== Backlog Health Report ===\n");
200
+ // Overall stats
201
+ console.log("Overall Statistics:");
202
+ console.log(` Total tasks: ${metrics.total}`);
203
+ console.log(` Completed: ${metrics.completedCount} (${formatPercentage(metrics.completedPercentage)})`);
204
+ console.log(` Blocked: ${metrics.blockedCount} (${formatPercentage(metrics.blockedPercentage)})`);
205
+ if (metrics.averageCompletionTimeHours !== undefined) {
206
+ console.log(` Average completion time: ${formatHours(metrics.averageCompletionTimeHours)}`);
207
+ }
208
+ // Status breakdown
209
+ console.log("\nStatus Breakdown:");
210
+ Object.entries(metrics.byStatus).forEach(([status, count]) => {
211
+ const percentage = metrics.byStatusPercentage[status];
212
+ console.log(` ${status.padEnd(15)}: ${count.toString().padStart(4)} (${formatPercentage(percentage)})`);
213
+ });
214
+ // Quality metrics
215
+ console.log("\nQuality Metrics:");
216
+ console.log(` Tasks without spec anchors: ${metrics.tasksWithoutAnchors}`);
217
+ console.log(` Tasks with dependencies: ${metrics.tasksWithDependencies}`);
218
+ // Spec version drift
219
+ if (metrics.specVersionDrift.count > 0) {
220
+ console.log("\nSpec Version Drift:");
221
+ console.log(` Tasks with outdated spec versions: ${metrics.specVersionDrift.count}`);
222
+ if (options.verbose &&
223
+ metrics.specVersionDrift.outdatedTasks.length > 0) {
224
+ console.log(" Outdated tasks:");
225
+ metrics.specVersionDrift.outdatedTasks.slice(0, 5).forEach((task) => {
226
+ console.log(` - ${task.title}`);
227
+ console.log(` Version: ${task.taskSpecVersion} → ${task.currentSpecVersion} (${task.specTitle})`);
228
+ });
229
+ if (metrics.specVersionDrift.count > 5) {
230
+ console.log(` ... and ${metrics.specVersionDrift.count - 5} more`);
231
+ }
232
+ }
233
+ }
234
+ // Oldest open task
235
+ if (metrics.oldestOpenTask) {
236
+ console.log("\nOldest Open Task:");
237
+ console.log(` Title: ${metrics.oldestOpenTask.title}`);
238
+ console.log(` ID: ${metrics.oldestOpenTask.id}`);
239
+ console.log(` Age: ${metrics.oldestOpenTask.ageInDays.toFixed(1)} days`);
240
+ console.log(` Created: ${metrics.oldestOpenTask.createdAt.toISOString()}`);
241
+ }
242
+ // Health assessment
243
+ console.log("\nHealth Assessment:");
244
+ const healthIssues = [];
245
+ if (metrics.blockedPercentage > 20) {
246
+ healthIssues.push(`High blocked task percentage (${formatPercentage(metrics.blockedPercentage)})`);
247
+ }
248
+ if (metrics.completedPercentage < 30 && metrics.total > 10) {
249
+ healthIssues.push(`Low completion rate (${formatPercentage(metrics.completedPercentage)})`);
250
+ }
251
+ if (metrics.tasksWithoutAnchors > metrics.total * 0.1) {
252
+ healthIssues.push(`Many tasks without spec anchors (${metrics.tasksWithoutAnchors})`);
253
+ }
254
+ // Check for spec version drift
255
+ if (metrics.specVersionDrift.count > 0) {
256
+ const driftPercentage = (metrics.specVersionDrift.count / metrics.total) * 100;
257
+ if (driftPercentage > 20) {
258
+ healthIssues.push(`High spec version drift (${formatPercentage(driftPercentage)} of tasks reference outdated specs)`);
259
+ }
260
+ }
261
+ if (metrics.oldestOpenTask && metrics.oldestOpenTask.ageInDays > 30) {
262
+ healthIssues.push(`Oldest open task is ${metrics.oldestOpenTask.ageInDays.toFixed(1)} days old`);
263
+ }
264
+ if (healthIssues.length === 0) {
265
+ console.log(" ✓ Backlog is healthy!");
266
+ }
267
+ else {
268
+ console.log(" ⚠ Issues detected:");
269
+ healthIssues.forEach((issue) => {
270
+ console.log(` - ${issue}`);
271
+ });
272
+ }
273
+ console.log();
274
+ }
275
+ return 0;
276
+ }
277
+ catch (error) {
278
+ console.error(`Error calculating metrics: ${(0, cli_runtime_1.errorMessage)(error)}`);
279
+ return 1;
280
+ }
281
+ finally {
282
+ await shared_1.AppDataSource.destroy();
283
+ }
284
+ }
285
+ if (require.main === module) {
286
+ void (0, cli_runtime_1.runCliMain)(main, { errorPrefix: "backlog-health failed" });
287
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bridge login CLI command
4
+ *
5
+ * Authenticates the CLI user via the Keycloak Device Authorization Grant,
6
+ * then registers the bridge device with the Spekn SaaS API. This combines
7
+ * `auth login` (credentials) with bridge device registration in a single step.
8
+ *
9
+ * Usage: spekn bridge login [--name <device-name>] [--api-url <url>] [--force]
10
+ */
11
+ import type { DeviceFlowDeps, DeviceFlowResult } from "../../auth/device-flow.js";
12
+ import { CredentialsStore } from "../../auth/credentials-store.js";
13
+ import { BridgeConfigStore } from "@spekn/bridge";
14
+ interface CLIOptions {
15
+ deviceName: string;
16
+ apiUrl: string;
17
+ force: boolean;
18
+ keycloakUrl: string;
19
+ realm: string;
20
+ }
21
+ interface Deps {
22
+ stdout: {
23
+ write(s: string): void;
24
+ };
25
+ stderr: {
26
+ write(s: string): void;
27
+ };
28
+ performDeviceFlow: (keycloakUrl: string, realm: string, clientId: string, deps?: Partial<DeviceFlowDeps>) => Promise<DeviceFlowResult>;
29
+ credentialsStore: CredentialsStore;
30
+ bridgeConfigStore: BridgeConfigStore;
31
+ }
32
+ declare function parseArgs(args: string[]): CLIOptions;
33
+ export declare function runBridgeLoginCli(args: string[], deps?: Partial<Deps>): Promise<number>;
34
+ declare function main(): Promise<void>;
35
+ export { main, parseArgs };
36
+ export { decodeJwtPayload } from "../../auth/jwt.js";