@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.
- package/README.md +58 -0
- package/dist/main.js +40540 -32176
- package/dist/prompts/governance-analysis.prompt.md +109 -0
- package/dist/resources/prompts/repo-analysis.prompt.md +28 -136
- package/dist/resources/prompts/repo-sync-analysis.prompt.md +31 -68
- package/dist/tui/chunk-4WEASLXY.mjs +3444 -0
- package/dist/tui/chunk-755CADEG.mjs +3401 -0
- package/dist/tui/chunk-BUJQVTY5.mjs +3409 -0
- package/dist/tui/chunk-BZKKMGFB.mjs +1959 -0
- package/dist/tui/chunk-DJYOBCNM.mjs +3159 -0
- package/dist/tui/chunk-GTFTFDY4.mjs +3417 -0
- package/dist/tui/chunk-IMEBD2KA.mjs +3444 -0
- package/dist/tui/chunk-IX6DR5SW.mjs +3433 -0
- package/dist/tui/chunk-JKFOY4IF.mjs +2003 -0
- package/dist/tui/chunk-OXXZ3O5L.mjs +3378 -0
- package/dist/tui/chunk-SHJNIAAJ.mjs +1697 -0
- package/dist/tui/chunk-V4SNDRUS.mjs +1666 -0
- package/dist/tui/chunk-VXVHNZST.mjs +1666 -0
- package/dist/tui/chunk-WCTSFKTA.mjs +3459 -0
- package/dist/tui/chunk-X2XP5ACW.mjs +3443 -0
- package/dist/tui/chunk-YUYJ7VBG.mjs +2029 -0
- package/dist/tui/chunk-ZM3EI5IA.mjs +3384 -0
- package/dist/tui/chunk-ZYOX64HP.mjs +1653 -0
- package/dist/tui/index.mjs +6999 -6938
- package/dist/tui/prompts/spec-creation-system.prompt.md +47 -0
- package/dist/tui/prompts/spec-refinement-system.prompt.md +72 -0
- package/dist/tui/use-session-store-63YUGUFA.mjs +8 -0
- package/dist/tui/use-session-store-ACO2SMJC.mjs +8 -0
- package/dist/tui/use-session-store-BVFDAWOB.mjs +8 -0
- package/dist/tui/use-session-store-DJIZ3FQZ.mjs +9 -0
- package/dist/tui/use-session-store-EAIQA4UG.mjs +9 -0
- package/dist/tui/use-session-store-EFBAXC3G.mjs +8 -0
- package/dist/tui/use-session-store-FJOR4KTG.mjs +8 -0
- package/dist/tui/use-session-store-IJE5KVOC.mjs +8 -0
- package/dist/tui/use-session-store-KGAFXCKI.mjs +8 -0
- package/dist/tui/use-session-store-KS4DPNDY.mjs +8 -0
- package/dist/tui/use-session-store-MMHJENNL.mjs +8 -0
- package/dist/tui/use-session-store-OZ6HC4I2.mjs +9 -0
- package/dist/tui/use-session-store-PTMWISNJ.mjs +8 -0
- package/dist/tui/use-session-store-VCDECQMW.mjs +8 -0
- package/dist/tui/use-session-store-VOK5ML5J.mjs +9 -0
- package/package.json +33 -13
- package/dist/__tests__/export-cli.test.d.ts +0 -1
- package/dist/__tests__/export-cli.test.js +0 -70
- package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
- package/dist/__tests__/tui-args-policy.test.js +0 -50
- package/dist/acp-S2MHZOAD.mjs +0 -23
- package/dist/acp-UCCI44JY.mjs +0 -25
- package/dist/auth/credentials-store.d.ts +0 -2
- package/dist/auth/credentials-store.js +0 -5
- package/dist/auth/device-flow.d.ts +0 -36
- package/dist/auth/device-flow.js +0 -189
- package/dist/auth/jwt.d.ts +0 -1
- package/dist/auth/jwt.js +0 -6
- package/dist/auth/session.d.ts +0 -67
- package/dist/auth/session.js +0 -86
- package/dist/auth-login.d.ts +0 -34
- package/dist/auth-login.js +0 -202
- package/dist/auth-logout.d.ts +0 -25
- package/dist/auth-logout.js +0 -115
- package/dist/auth-status.d.ts +0 -24
- package/dist/auth-status.js +0 -109
- package/dist/backlog-generate.d.ts +0 -11
- package/dist/backlog-generate.js +0 -308
- package/dist/backlog-health.d.ts +0 -11
- package/dist/backlog-health.js +0 -287
- package/dist/bridge-login.d.ts +0 -40
- package/dist/bridge-login.js +0 -277
- package/dist/chunk-3PAYRI4G.mjs +0 -2428
- package/dist/chunk-M4CS3A25.mjs +0 -2426
- package/dist/commands/auth/login.d.ts +0 -30
- package/dist/commands/auth/login.js +0 -164
- package/dist/commands/auth/logout.d.ts +0 -25
- package/dist/commands/auth/logout.js +0 -115
- package/dist/commands/auth/status.d.ts +0 -24
- package/dist/commands/auth/status.js +0 -109
- package/dist/commands/backlog/generate.d.ts +0 -11
- package/dist/commands/backlog/generate.js +0 -308
- package/dist/commands/backlog/health.d.ts +0 -11
- package/dist/commands/backlog/health.js +0 -287
- package/dist/commands/bridge/login.d.ts +0 -36
- package/dist/commands/bridge/login.js +0 -258
- package/dist/commands/export.d.ts +0 -35
- package/dist/commands/export.js +0 -485
- package/dist/commands/marketplace-export.d.ts +0 -21
- package/dist/commands/marketplace-export.js +0 -214
- package/dist/commands/project-clean.d.ts +0 -1
- package/dist/commands/project-clean.js +0 -126
- package/dist/commands/repo/common.d.ts +0 -105
- package/dist/commands/repo/common.js +0 -775
- package/dist/commands/repo/detach.d.ts +0 -2
- package/dist/commands/repo/detach.js +0 -120
- package/dist/commands/repo/register.d.ts +0 -21
- package/dist/commands/repo/register.js +0 -175
- package/dist/commands/repo/sync.d.ts +0 -22
- package/dist/commands/repo/sync.js +0 -873
- package/dist/commands/skills-import-local.d.ts +0 -16
- package/dist/commands/skills-import-local.js +0 -352
- package/dist/commands/spec/drift-check.d.ts +0 -3
- package/dist/commands/spec/drift-check.js +0 -186
- package/dist/commands/spec/frontmatter.d.ts +0 -11
- package/dist/commands/spec/frontmatter.js +0 -219
- package/dist/commands/spec/lint.d.ts +0 -11
- package/dist/commands/spec/lint.js +0 -499
- package/dist/commands/spec/parse.d.ts +0 -11
- package/dist/commands/spec/parse.js +0 -162
- package/dist/export.d.ts +0 -35
- package/dist/export.js +0 -485
- package/dist/main.d.ts +0 -1
- package/dist/marketplace-export.d.ts +0 -21
- package/dist/marketplace-export.js +0 -214
- package/dist/project-clean.d.ts +0 -1
- package/dist/project-clean.js +0 -126
- package/dist/project-context.d.ts +0 -99
- package/dist/project-context.js +0 -376
- package/dist/repo-common.d.ts +0 -101
- package/dist/repo-common.js +0 -671
- package/dist/repo-detach.d.ts +0 -2
- package/dist/repo-detach.js +0 -102
- package/dist/repo-ingest.d.ts +0 -29
- package/dist/repo-ingest.js +0 -305
- package/dist/repo-register.d.ts +0 -21
- package/dist/repo-register.js +0 -175
- package/dist/repo-sync.d.ts +0 -16
- package/dist/repo-sync.js +0 -152
- package/dist/resources/prompt-loader.d.ts +0 -1
- package/dist/resources/prompt-loader.js +0 -62
- package/dist/skills-import-local.d.ts +0 -16
- package/dist/skills-import-local.js +0 -352
- package/dist/spec-drift-check.d.ts +0 -3
- package/dist/spec-drift-check.js +0 -186
- package/dist/spec-frontmatter.d.ts +0 -11
- package/dist/spec-frontmatter.js +0 -219
- package/dist/spec-lint.d.ts +0 -11
- package/dist/spec-lint.js +0 -499
- package/dist/spec-parse.d.ts +0 -11
- package/dist/spec-parse.js +0 -162
- package/dist/stubs/dotenv.d.ts +0 -5
- package/dist/stubs/dotenv.js +0 -6
- package/dist/stubs/typeorm.d.ts +0 -22
- package/dist/stubs/typeorm.js +0 -28
- package/dist/tui-bundle.d.ts +0 -1
- package/dist/tui-bundle.js +0 -5
- package/dist/tui-entry.mjs +0 -1407
- package/dist/utils/cli-runtime.d.ts +0 -5
- package/dist/utils/cli-runtime.js +0 -22
- package/dist/utils/help-error.d.ts +0 -7
- package/dist/utils/help-error.js +0 -14
- package/dist/utils/interaction.d.ts +0 -19
- package/dist/utils/interaction.js +0 -93
- package/dist/utils/structured-log.d.ts +0 -7
- package/dist/utils/structured-log.js +0 -112
- package/dist/utils/trpc-url.d.ts +0 -4
- package/dist/utils/trpc-url.js +0 -15
package/dist/backlog-health.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/bridge-login.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
/**
|
|
34
|
-
* Decode the payload of a JWT access token without verification.
|
|
35
|
-
* Returns the raw claims object, or null if decoding fails.
|
|
36
|
-
*/
|
|
37
|
-
declare function decodeJwtPayload(token: string): Record<string, unknown> | null;
|
|
38
|
-
export declare function runBridgeLoginCli(args: string[], deps?: Partial<Deps>): Promise<number>;
|
|
39
|
-
declare function main(): Promise<void>;
|
|
40
|
-
export { main, parseArgs, decodeJwtPayload };
|
package/dist/bridge-login.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
/**
|
|
4
|
-
* bridge login CLI command
|
|
5
|
-
*
|
|
6
|
-
* Authenticates the CLI user via the Keycloak Device Authorization Grant,
|
|
7
|
-
* then registers the bridge device with the Spekn SaaS API. This combines
|
|
8
|
-
* `auth login` (credentials) with bridge device registration in a single step.
|
|
9
|
-
*
|
|
10
|
-
* Usage: spekn bridge login [--name <device-name>] [--api-url <url>] [--force]
|
|
11
|
-
*/
|
|
12
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
-
}
|
|
18
|
-
Object.defineProperty(o, k2, desc);
|
|
19
|
-
}) : (function(o, m, k, k2) {
|
|
20
|
-
if (k2 === undefined) k2 = k;
|
|
21
|
-
o[k2] = m[k];
|
|
22
|
-
}));
|
|
23
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
-
}) : function(o, v) {
|
|
26
|
-
o["default"] = v;
|
|
27
|
-
});
|
|
28
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
-
var ownKeys = function(o) {
|
|
30
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
-
var ar = [];
|
|
32
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
-
return ar;
|
|
34
|
-
};
|
|
35
|
-
return ownKeys(o);
|
|
36
|
-
};
|
|
37
|
-
return function (mod) {
|
|
38
|
-
if (mod && mod.__esModule) return mod;
|
|
39
|
-
var result = {};
|
|
40
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
-
__setModuleDefault(result, mod);
|
|
42
|
-
return result;
|
|
43
|
-
};
|
|
44
|
-
})();
|
|
45
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
-
exports.runBridgeLoginCli = runBridgeLoginCli;
|
|
47
|
-
exports.main = main;
|
|
48
|
-
exports.parseArgs = parseArgs;
|
|
49
|
-
exports.decodeJwtPayload = decodeJwtPayload;
|
|
50
|
-
const os = __importStar(require("node:os"));
|
|
51
|
-
const client_1 = require("@trpc/client");
|
|
52
|
-
const device_flow_js_1 = require("./auth/device-flow.js");
|
|
53
|
-
const credentials_store_js_1 = require("./auth/credentials-store.js");
|
|
54
|
-
const bridge_1 = require("@spekn/bridge");
|
|
55
|
-
const trpc_url_1 = require("./utils/trpc-url");
|
|
56
|
-
const help_error_1 = require("./utils/help-error");
|
|
57
|
-
const structured_log_1 = require("./utils/structured-log");
|
|
58
|
-
const defaultDeps = {
|
|
59
|
-
stdout: process.stdout,
|
|
60
|
-
stderr: process.stderr,
|
|
61
|
-
performDeviceFlow: device_flow_js_1.performDeviceFlow,
|
|
62
|
-
credentialsStore: new credentials_store_js_1.CredentialsStore(),
|
|
63
|
-
bridgeConfigStore: new bridge_1.BridgeConfigStore(),
|
|
64
|
-
};
|
|
65
|
-
function resolveDeps(deps) {
|
|
66
|
-
return {
|
|
67
|
-
stdout: deps?.stdout ?? defaultDeps.stdout,
|
|
68
|
-
stderr: deps?.stderr ?? defaultDeps.stderr,
|
|
69
|
-
performDeviceFlow: deps?.performDeviceFlow ?? defaultDeps.performDeviceFlow,
|
|
70
|
-
credentialsStore: deps?.credentialsStore ?? defaultDeps.credentialsStore,
|
|
71
|
-
bridgeConfigStore: deps?.bridgeConfigStore ?? defaultDeps.bridgeConfigStore,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function printHelp(stderr) {
|
|
75
|
-
stderr.write(`
|
|
76
|
-
bridge login - Authenticate and register the bridge with Spekn SaaS
|
|
77
|
-
|
|
78
|
-
USAGE:
|
|
79
|
-
spekn bridge login [options]
|
|
80
|
-
|
|
81
|
-
OPTIONS:
|
|
82
|
-
--name <name> Device name (default: system hostname)
|
|
83
|
-
--api-url <url> API base URL (default: SPEKN_API_URL or https://app.spekn.com)
|
|
84
|
-
--keycloak-url <url> Keycloak base URL (default: KEYCLOAK_URL or https://auth.spekn.com)
|
|
85
|
-
--realm <realm> Keycloak realm name (default: KEYCLOAK_REALM or spekn)
|
|
86
|
-
--force Re-register even if bridge is already paired
|
|
87
|
-
--help Show this help message
|
|
88
|
-
|
|
89
|
-
ENVIRONMENT:
|
|
90
|
-
SPEKN_API_URL API base URL
|
|
91
|
-
KEYCLOAK_URL Keycloak base URL
|
|
92
|
-
KEYCLOAK_REALM Keycloak realm name
|
|
93
|
-
|
|
94
|
-
EXAMPLES:
|
|
95
|
-
spekn bridge login
|
|
96
|
-
spekn bridge login --name my-dev-machine
|
|
97
|
-
spekn bridge login --api-url https://api.spekn.dev --force
|
|
98
|
-
`);
|
|
99
|
-
}
|
|
100
|
-
function parseArgs(args) {
|
|
101
|
-
let deviceName = os.hostname();
|
|
102
|
-
let apiUrl = process.env["SPEKN_API_URL"] ?? "https://app.spekn.com";
|
|
103
|
-
let keycloakUrl = process.env["KEYCLOAK_URL"] ?? "https://auth.spekn.com";
|
|
104
|
-
let realm = process.env["KEYCLOAK_REALM"] ?? "spekn";
|
|
105
|
-
let force = false;
|
|
106
|
-
for (let index = 0; index < args.length; index++) {
|
|
107
|
-
const arg = args[index];
|
|
108
|
-
if (arg === "--help" || arg === "-h") {
|
|
109
|
-
throw new help_error_1.HelpRequestedError();
|
|
110
|
-
}
|
|
111
|
-
if (arg === "--name" && args[index + 1]) {
|
|
112
|
-
deviceName = args[++index];
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (arg?.startsWith("--name=")) {
|
|
116
|
-
deviceName = arg.slice("--name=".length);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (arg === "--api-url" && args[index + 1]) {
|
|
120
|
-
apiUrl = args[++index];
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (arg?.startsWith("--api-url=")) {
|
|
124
|
-
apiUrl = arg.slice("--api-url=".length);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (arg === "--keycloak-url" && args[index + 1]) {
|
|
128
|
-
keycloakUrl = args[++index];
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (arg?.startsWith("--keycloak-url=")) {
|
|
132
|
-
keycloakUrl = arg.slice("--keycloak-url=".length);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (arg === "--realm" && args[index + 1]) {
|
|
136
|
-
realm = args[++index];
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (arg?.startsWith("--realm=")) {
|
|
140
|
-
realm = arg.slice("--realm=".length);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (arg === "--force") {
|
|
144
|
-
force = true;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return { deviceName, apiUrl, keycloakUrl, realm, force };
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Decode the payload of a JWT access token without verification.
|
|
152
|
-
* Returns the raw claims object, or null if decoding fails.
|
|
153
|
-
*/
|
|
154
|
-
function decodeJwtPayload(token) {
|
|
155
|
-
try {
|
|
156
|
-
const parts = token.split(".");
|
|
157
|
-
if (parts.length !== 3) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
const base64 = parts[1]
|
|
161
|
-
.replace(/-/g, "+")
|
|
162
|
-
.replace(/_/g, "/");
|
|
163
|
-
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
|
|
164
|
-
const json = Buffer.from(padded, "base64").toString("utf-8");
|
|
165
|
-
return JSON.parse(json);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async function runBridgeLoginCli(args, deps) {
|
|
172
|
-
const resolved = resolveDeps(deps);
|
|
173
|
-
try {
|
|
174
|
-
const options = parseArgs(args);
|
|
175
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
176
|
-
source: "cli.bridge.login",
|
|
177
|
-
level: "info",
|
|
178
|
-
message: "Starting bridge login",
|
|
179
|
-
details: { deviceName: options.deviceName, apiUrl: options.apiUrl, force: options.force },
|
|
180
|
-
});
|
|
181
|
-
// Load bridge config
|
|
182
|
-
resolved.bridgeConfigStore.load();
|
|
183
|
-
// Check if already paired — require --force to re-register
|
|
184
|
-
if (resolved.bridgeConfigStore.isPaired() && !options.force) {
|
|
185
|
-
resolved.stderr.write("Bridge is already registered. Use --force to re-register.\n");
|
|
186
|
-
return 1;
|
|
187
|
-
}
|
|
188
|
-
// Step 1: Check for existing valid token; if none, run device flow
|
|
189
|
-
let accessToken = await resolved.credentialsStore.getValidToken();
|
|
190
|
-
if (!accessToken) {
|
|
191
|
-
resolved.stdout.write("No valid credentials found. Starting login...\n");
|
|
192
|
-
const result = await resolved.performDeviceFlow(options.keycloakUrl, options.realm, "spekn-cli", { stdout: resolved.stdout, stderr: resolved.stderr });
|
|
193
|
-
// Step 2: Save Keycloak credentials (bridge login doubles as auth login)
|
|
194
|
-
const claims = decodeJwtPayload(result.accessToken);
|
|
195
|
-
const sub = typeof claims?.["sub"] === "string" ? claims["sub"] : "unknown";
|
|
196
|
-
const email = typeof claims?.["email"] === "string" ? claims["email"] : "unknown";
|
|
197
|
-
const name = typeof claims?.["name"] === "string"
|
|
198
|
-
? claims["name"]
|
|
199
|
-
: typeof claims?.["preferred_username"] === "string"
|
|
200
|
-
? claims["preferred_username"]
|
|
201
|
-
: undefined;
|
|
202
|
-
const credentials = {
|
|
203
|
-
accessToken: result.accessToken,
|
|
204
|
-
refreshToken: result.refreshToken,
|
|
205
|
-
expiresAt: Date.now() + result.expiresIn * 1000,
|
|
206
|
-
keycloakUrl: options.keycloakUrl,
|
|
207
|
-
realm: options.realm,
|
|
208
|
-
user: { sub, email, name },
|
|
209
|
-
};
|
|
210
|
-
resolved.credentialsStore.save(credentials);
|
|
211
|
-
resolved.stdout.write(`Authenticated as ${email}\n`);
|
|
212
|
-
accessToken = result.accessToken;
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
const storedCreds = resolved.credentialsStore.load();
|
|
216
|
-
const email = storedCreds?.user?.email ?? "unknown";
|
|
217
|
-
resolved.stdout.write(`Using existing credentials (${email})\n`);
|
|
218
|
-
}
|
|
219
|
-
// Resolve organization ID from credentials store or env
|
|
220
|
-
const storedCreds = resolved.credentialsStore.load();
|
|
221
|
-
const organizationId = storedCreds?.organizationId ?? process.env["SPEKN_ORGANIZATION_ID"] ?? "";
|
|
222
|
-
// Step 3: POST to bridge.registerWithToken
|
|
223
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
-
const client = (0, client_1.createTRPCProxyClient)({
|
|
225
|
-
links: [
|
|
226
|
-
(0, client_1.httpBatchLink)({
|
|
227
|
-
url: (0, trpc_url_1.normalizeTrpcUrl)(options.apiUrl),
|
|
228
|
-
headers: {
|
|
229
|
-
authorization: `Bearer ${accessToken}`,
|
|
230
|
-
"x-organization-id": organizationId,
|
|
231
|
-
},
|
|
232
|
-
}),
|
|
233
|
-
],
|
|
234
|
-
});
|
|
235
|
-
const result = await client.bridge.registerWithToken.mutate({
|
|
236
|
-
deviceName: options.deviceName,
|
|
237
|
-
});
|
|
238
|
-
// Step 4: Save result to BridgeConfigStore
|
|
239
|
-
resolved.bridgeConfigStore.setPairing({
|
|
240
|
-
deviceId: result.deviceId,
|
|
241
|
-
deviceName: options.deviceName,
|
|
242
|
-
credential: result.credential,
|
|
243
|
-
pairedAt: new Date().toISOString(),
|
|
244
|
-
saasOrigin: result.saasOrigin,
|
|
245
|
-
});
|
|
246
|
-
resolved.bridgeConfigStore.update({ saasWsUrl: result.saasWsUrl });
|
|
247
|
-
resolved.stdout.write(`\nBridge registered as '${options.deviceName}'. Run: spekn bridge start\n`);
|
|
248
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
249
|
-
source: "cli.bridge.login",
|
|
250
|
-
level: "info",
|
|
251
|
-
message: "Bridge login completed",
|
|
252
|
-
details: { deviceId: result.deviceId, organizationId: result.organizationId },
|
|
253
|
-
});
|
|
254
|
-
return 0;
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
if (error instanceof help_error_1.HelpRequestedError) {
|
|
258
|
-
printHelp(resolved.stderr);
|
|
259
|
-
return 0;
|
|
260
|
-
}
|
|
261
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
262
|
-
resolved.stderr.write(`Error: ${message}\n`);
|
|
263
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
264
|
-
source: "cli.bridge.login",
|
|
265
|
-
level: "error",
|
|
266
|
-
message,
|
|
267
|
-
});
|
|
268
|
-
return 1;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function main() {
|
|
272
|
-
const exitCode = await runBridgeLoginCli(process.argv.slice(2));
|
|
273
|
-
process.exit(exitCode);
|
|
274
|
-
}
|
|
275
|
-
if (require.main === module) {
|
|
276
|
-
void main();
|
|
277
|
-
}
|