@spekn/cli 1.0.1 → 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/dist/main.js +34806 -29538
- 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/app.d.ts +7 -0
- package/dist/tui/app.js +122 -0
- package/dist/tui/args.d.ts +8 -0
- package/dist/tui/args.js +57 -0
- package/dist/tui/capabilities/policy.d.ts +7 -0
- package/dist/tui/capabilities/policy.js +64 -0
- 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/components/frame.d.ts +8 -0
- package/dist/tui/components/frame.js +8 -0
- package/dist/tui/components/status-bar.d.ts +8 -0
- package/dist/tui/components/status-bar.js +8 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +23 -0
- package/dist/tui/index.mjs +6999 -6938
- package/dist/tui/keymap/use-global-keymap.d.ts +19 -0
- package/dist/tui/keymap/use-global-keymap.js +82 -0
- package/dist/tui/navigation/nav-items.d.ts +3 -0
- package/dist/tui/navigation/nav-items.js +18 -0
- 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/screens/bridge.d.ts +8 -0
- package/dist/tui/screens/bridge.js +19 -0
- package/dist/tui/screens/decisions.d.ts +5 -0
- package/dist/tui/screens/decisions.js +28 -0
- package/dist/tui/screens/export.d.ts +5 -0
- package/dist/tui/screens/export.js +16 -0
- package/dist/tui/screens/home.d.ts +5 -0
- package/dist/tui/screens/home.js +33 -0
- package/dist/tui/screens/locked.d.ts +5 -0
- package/dist/tui/screens/locked.js +9 -0
- package/dist/tui/screens/specs.d.ts +5 -0
- package/dist/tui/screens/specs.js +31 -0
- package/dist/tui/services/client.d.ts +1 -0
- package/dist/tui/services/client.js +18 -0
- package/dist/tui/services/context-service.d.ts +19 -0
- package/dist/tui/services/context-service.js +246 -0
- package/dist/tui/shared-enums.d.ts +16 -0
- package/dist/tui/shared-enums.js +19 -0
- package/dist/tui/state/use-app-state.d.ts +35 -0
- package/dist/tui/state/use-app-state.js +177 -0
- package/dist/tui/types.d.ts +77 -0
- package/dist/tui/types.js +2 -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 +6 -3
|
@@ -0,0 +1,2029 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/auth/credentials-store.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var CliCredentialsSchema = z.object({
|
|
10
|
+
accessToken: z.string(),
|
|
11
|
+
refreshToken: z.string(),
|
|
12
|
+
expiresAt: z.number(),
|
|
13
|
+
keycloakUrl: z.string(),
|
|
14
|
+
realm: z.string(),
|
|
15
|
+
organizationId: z.string().optional(),
|
|
16
|
+
user: z.object({
|
|
17
|
+
sub: z.string(),
|
|
18
|
+
email: z.string(),
|
|
19
|
+
name: z.string().optional()
|
|
20
|
+
}).optional()
|
|
21
|
+
});
|
|
22
|
+
var TokenResponseSchema = z.object({
|
|
23
|
+
access_token: z.string(),
|
|
24
|
+
refresh_token: z.string(),
|
|
25
|
+
expires_in: z.number()
|
|
26
|
+
});
|
|
27
|
+
var CredentialsStore = class {
|
|
28
|
+
static {
|
|
29
|
+
__name(this, "CredentialsStore");
|
|
30
|
+
}
|
|
31
|
+
configDir;
|
|
32
|
+
credentialsPath;
|
|
33
|
+
constructor(configDir) {
|
|
34
|
+
this.configDir = configDir ?? path.join(os.homedir(), ".spekn");
|
|
35
|
+
this.credentialsPath = path.join(this.configDir, "credentials.json");
|
|
36
|
+
}
|
|
37
|
+
load() {
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(this.credentialsPath, "utf-8");
|
|
40
|
+
return CliCredentialsSchema.parse(JSON.parse(raw));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
save(creds) {
|
|
46
|
+
fs.mkdirSync(this.configDir, {
|
|
47
|
+
recursive: true,
|
|
48
|
+
mode: 448
|
|
49
|
+
});
|
|
50
|
+
const json = JSON.stringify(creds, null, 2);
|
|
51
|
+
fs.writeFileSync(this.credentialsPath, json, {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
mode: 384
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
clear() {
|
|
57
|
+
try {
|
|
58
|
+
fs.rmSync(this.credentialsPath);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code !== "ENOENT") {
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async getValidToken() {
|
|
66
|
+
const creds = this.load();
|
|
67
|
+
if (creds === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const bufferMs = 3e4;
|
|
71
|
+
if (Date.now() + bufferMs < creds.expiresAt) {
|
|
72
|
+
return creds.accessToken;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const tokenUrl = `${creds.keycloakUrl}/realms/${creds.realm}/protocol/openid-connect/token`;
|
|
76
|
+
const body = new URLSearchParams({
|
|
77
|
+
grant_type: "refresh_token",
|
|
78
|
+
client_id: "spekn-cli",
|
|
79
|
+
refresh_token: creds.refreshToken
|
|
80
|
+
});
|
|
81
|
+
const res = await fetch(tokenUrl, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body,
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const data = TokenResponseSchema.parse(await res.json());
|
|
92
|
+
const updated = {
|
|
93
|
+
...creds,
|
|
94
|
+
accessToken: data.access_token,
|
|
95
|
+
refreshToken: data.refresh_token,
|
|
96
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
97
|
+
};
|
|
98
|
+
this.save(updated);
|
|
99
|
+
return updated.accessToken;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/auth/jwt.ts
|
|
107
|
+
function decodeJwtPayload(token) {
|
|
108
|
+
try {
|
|
109
|
+
const parts = token.split(".");
|
|
110
|
+
if (parts.length !== 3) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
114
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
115
|
+
const json = Buffer.from(padded, "base64").toString("utf-8");
|
|
116
|
+
return JSON.parse(json);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
__name(decodeJwtPayload, "decodeJwtPayload");
|
|
122
|
+
|
|
123
|
+
// src/services/cli-runner.ts
|
|
124
|
+
import { spawn } from "child_process";
|
|
125
|
+
var CliRunner = class {
|
|
126
|
+
static {
|
|
127
|
+
__name(this, "CliRunner");
|
|
128
|
+
}
|
|
129
|
+
apiUrl;
|
|
130
|
+
credentialsStore = new CredentialsStore();
|
|
131
|
+
constructor(apiUrl) {
|
|
132
|
+
this.apiUrl = apiUrl;
|
|
133
|
+
}
|
|
134
|
+
resolveCliEntry() {
|
|
135
|
+
const cliEntry = process.argv[1];
|
|
136
|
+
return typeof cliEntry === "string" && cliEntry.length > 0 ? cliEntry : null;
|
|
137
|
+
}
|
|
138
|
+
buildCliEnv(options) {
|
|
139
|
+
const env = {
|
|
140
|
+
...process.env
|
|
141
|
+
};
|
|
142
|
+
delete env.SPEKN_ORGANIZATION_ID;
|
|
143
|
+
if (options?.organizationId) {
|
|
144
|
+
env.SPEKN_ORGANIZATION_ID = options.organizationId;
|
|
145
|
+
}
|
|
146
|
+
if (options?.interactionEnabled) {
|
|
147
|
+
env.SPEKN_INTERACTION_MODE = "json-stdio";
|
|
148
|
+
} else {
|
|
149
|
+
delete env.SPEKN_INTERACTION_MODE;
|
|
150
|
+
}
|
|
151
|
+
return env;
|
|
152
|
+
}
|
|
153
|
+
async respondToInteraction(child, request, onProgress, requestInteraction) {
|
|
154
|
+
const requestPromise = requestInteraction?.(request);
|
|
155
|
+
const value = await Promise.race([
|
|
156
|
+
requestPromise,
|
|
157
|
+
new Promise((resolve) => setTimeout(() => resolve(void 0), 12e4))
|
|
158
|
+
]);
|
|
159
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
160
|
+
if (value === "skip") {
|
|
161
|
+
child.stdin.write(`${JSON.stringify({
|
|
162
|
+
id: request.id,
|
|
163
|
+
skip: true
|
|
164
|
+
})}
|
|
165
|
+
`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const allowed = new Set(request.options.map((option) => option?.value).filter((optionValue) => typeof optionValue === "string"));
|
|
169
|
+
if (typeof value === "string" && allowed.has(value)) {
|
|
170
|
+
child.stdin.write(`${JSON.stringify({
|
|
171
|
+
id: request.id,
|
|
172
|
+
value
|
|
173
|
+
})}
|
|
174
|
+
`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const fallbackValue = request.options[0]?.value;
|
|
178
|
+
if (typeof fallbackValue === "string") {
|
|
179
|
+
onProgress?.(`[interaction] Auto-selecting default: ${fallbackValue}`);
|
|
180
|
+
child.stdin.write(`${JSON.stringify({
|
|
181
|
+
id: request.id,
|
|
182
|
+
value: fallbackValue
|
|
183
|
+
})}
|
|
184
|
+
`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
child.stdin.write(`${JSON.stringify({
|
|
188
|
+
id: request.id
|
|
189
|
+
})}
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
192
|
+
async runCliCommand(args, options) {
|
|
193
|
+
const cliEntry = this.resolveCliEntry();
|
|
194
|
+
if (!cliEntry) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
exitCode: 1,
|
|
198
|
+
stdoutLines: [],
|
|
199
|
+
stderrLines: [],
|
|
200
|
+
outputLines: [],
|
|
201
|
+
output: "Could not resolve CLI entrypoint.",
|
|
202
|
+
error: "Could not resolve CLI entrypoint."
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const includeAuthToken = options?.includeAuthToken !== false;
|
|
206
|
+
const token = includeAuthToken ? await this.credentialsStore.getValidToken().catch(() => null) : null;
|
|
207
|
+
const env = this.buildCliEnv({
|
|
208
|
+
organizationId: options?.organizationId,
|
|
209
|
+
interactionEnabled: Boolean(options?.requestInteraction)
|
|
210
|
+
});
|
|
211
|
+
if (includeAuthToken && token) {
|
|
212
|
+
env.SPEKN_AUTH_TOKEN = token;
|
|
213
|
+
} else if (!includeAuthToken) {
|
|
214
|
+
delete env.SPEKN_AUTH_TOKEN;
|
|
215
|
+
}
|
|
216
|
+
const finalArgs = [
|
|
217
|
+
cliEntry,
|
|
218
|
+
...args
|
|
219
|
+
];
|
|
220
|
+
const stdoutLines = [];
|
|
221
|
+
const stderrLines = [];
|
|
222
|
+
const outputLines = [];
|
|
223
|
+
const maxLines = options?.maxOutputLines ?? 250;
|
|
224
|
+
let stdoutBuffer = "";
|
|
225
|
+
let stderrBuffer = "";
|
|
226
|
+
const pushLine = /* @__PURE__ */ __name((line, stream) => {
|
|
227
|
+
const target = stream === "stdout" ? stdoutLines : stderrLines;
|
|
228
|
+
target.push(line);
|
|
229
|
+
outputLines.push(line);
|
|
230
|
+
if (target.length > maxLines) target.shift();
|
|
231
|
+
if (outputLines.length > maxLines) outputLines.shift();
|
|
232
|
+
options?.onProgress?.(line);
|
|
233
|
+
}, "pushLine");
|
|
234
|
+
const processLine = /* @__PURE__ */ __name((line, stream, child) => {
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
if (!trimmed) return;
|
|
237
|
+
const activityMarker = "[spekn-activity] ";
|
|
238
|
+
const activityIdx = trimmed.indexOf(activityMarker);
|
|
239
|
+
if (activityIdx >= 0) {
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(trimmed.slice(activityIdx + activityMarker.length));
|
|
242
|
+
if (parsed.type === "spekn.activity") {
|
|
243
|
+
options?.onActivity?.(parsed);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const marker = "[spekn-interaction] ";
|
|
250
|
+
const markerIndex = trimmed.indexOf(marker);
|
|
251
|
+
if (options?.requestInteraction && markerIndex >= 0) {
|
|
252
|
+
const payloadRaw = trimmed.slice(markerIndex + marker.length);
|
|
253
|
+
let payload = null;
|
|
254
|
+
try {
|
|
255
|
+
payload = JSON.parse(payloadRaw);
|
|
256
|
+
} catch {
|
|
257
|
+
payload = null;
|
|
258
|
+
}
|
|
259
|
+
if (payload?.type === "spekn.interaction.request" && typeof payload.id === "string" && Array.isArray(payload.options)) {
|
|
260
|
+
options.onProgress?.(`[interaction] ${payload.title ?? "Selection required"}`);
|
|
261
|
+
const request = {
|
|
262
|
+
id: payload.id,
|
|
263
|
+
title: payload.title ?? "Selection required",
|
|
264
|
+
message: payload.message ?? "Choose an option",
|
|
265
|
+
options: payload.options,
|
|
266
|
+
allowSkip: payload.allowSkip === true
|
|
267
|
+
};
|
|
268
|
+
void this.respondToInteraction(child, request, options.onProgress, options.requestInteraction).catch(() => {
|
|
269
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
270
|
+
child.stdin.write(`${JSON.stringify({
|
|
271
|
+
id: request.id
|
|
272
|
+
})}
|
|
273
|
+
`);
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
pushLine(trimmed, stream);
|
|
279
|
+
}, "processLine");
|
|
280
|
+
try {
|
|
281
|
+
const child = spawn(process.execPath, finalArgs, {
|
|
282
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
283
|
+
env,
|
|
284
|
+
stdio: [
|
|
285
|
+
"pipe",
|
|
286
|
+
"pipe",
|
|
287
|
+
"pipe"
|
|
288
|
+
]
|
|
289
|
+
});
|
|
290
|
+
const onData = /* @__PURE__ */ __name((stream, data) => {
|
|
291
|
+
const chunk = String(data);
|
|
292
|
+
if (stream === "stdout") {
|
|
293
|
+
stdoutBuffer += chunk;
|
|
294
|
+
} else {
|
|
295
|
+
stderrBuffer += chunk;
|
|
296
|
+
}
|
|
297
|
+
const buffer = stream === "stdout" ? stdoutBuffer : stderrBuffer;
|
|
298
|
+
const lines = buffer.split(/\r?\n|\r/);
|
|
299
|
+
const remainder = lines.pop() ?? "";
|
|
300
|
+
if (stream === "stdout") {
|
|
301
|
+
stdoutBuffer = remainder;
|
|
302
|
+
} else {
|
|
303
|
+
stderrBuffer = remainder;
|
|
304
|
+
}
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
processLine(line, stream, child);
|
|
307
|
+
}
|
|
308
|
+
}, "onData");
|
|
309
|
+
child.stdout?.on("data", (data) => onData("stdout", data));
|
|
310
|
+
child.stderr?.on("data", (data) => onData("stderr", data));
|
|
311
|
+
if (options?.signal) {
|
|
312
|
+
if (options.signal.aborted) {
|
|
313
|
+
child.kill("SIGTERM");
|
|
314
|
+
} else {
|
|
315
|
+
const onAbort = /* @__PURE__ */ __name(() => {
|
|
316
|
+
child.kill("SIGTERM");
|
|
317
|
+
}, "onAbort");
|
|
318
|
+
options.signal.addEventListener("abort", onAbort, {
|
|
319
|
+
once: true
|
|
320
|
+
});
|
|
321
|
+
child.on("close", () => options.signal?.removeEventListener("abort", onAbort));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
325
|
+
child.on("error", reject);
|
|
326
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
327
|
+
});
|
|
328
|
+
if (stdoutBuffer.trim().length > 0) {
|
|
329
|
+
processLine(stdoutBuffer, "stdout", child);
|
|
330
|
+
}
|
|
331
|
+
if (stderrBuffer.trim().length > 0) {
|
|
332
|
+
processLine(stderrBuffer, "stderr", child);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
success: exitCode === 0,
|
|
336
|
+
exitCode,
|
|
337
|
+
stdoutLines,
|
|
338
|
+
stderrLines,
|
|
339
|
+
outputLines,
|
|
340
|
+
output: outputLines.join("\n")
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
exitCode: 1,
|
|
347
|
+
stdoutLines,
|
|
348
|
+
stderrLines,
|
|
349
|
+
outputLines,
|
|
350
|
+
output: message,
|
|
351
|
+
error: message
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async runCliJson(args, cwd, organizationId) {
|
|
356
|
+
const result = await this.runCliCommand(args, {
|
|
357
|
+
cwd,
|
|
358
|
+
organizationId,
|
|
359
|
+
includeAuthToken: true
|
|
360
|
+
});
|
|
361
|
+
if (!result.success && result.output.trim().length === 0) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
error: result.error ?? "CLI command failed."
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const output = result.stdoutLines.join("\n").trim() || result.output.trim();
|
|
368
|
+
const jsonStart = output.indexOf("{");
|
|
369
|
+
if (jsonStart === -1) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
error: output || "Command produced no JSON output."
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(output.slice(jsonStart));
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
value: parsed
|
|
380
|
+
};
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
error: message
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/services/auth-service.ts
|
|
392
|
+
var AuthService = class {
|
|
393
|
+
static {
|
|
394
|
+
__name(this, "AuthService");
|
|
395
|
+
}
|
|
396
|
+
credentialsStore = new CredentialsStore();
|
|
397
|
+
cliRunner;
|
|
398
|
+
constructor(apiUrl) {
|
|
399
|
+
this.cliRunner = new CliRunner(apiUrl);
|
|
400
|
+
}
|
|
401
|
+
async checkAuthentication() {
|
|
402
|
+
try {
|
|
403
|
+
const token = await this.credentialsStore.getValidToken();
|
|
404
|
+
return token || null;
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async authenticateViaCli(onProgress) {
|
|
410
|
+
const result = await this.cliRunner.runCliCommand([
|
|
411
|
+
"auth",
|
|
412
|
+
"login"
|
|
413
|
+
], {
|
|
414
|
+
cwd: process.cwd(),
|
|
415
|
+
includeAuthToken: false,
|
|
416
|
+
onProgress
|
|
417
|
+
});
|
|
418
|
+
return result.success;
|
|
419
|
+
}
|
|
420
|
+
extractUserEmail(token) {
|
|
421
|
+
try {
|
|
422
|
+
const claims = decodeJwtPayload(token);
|
|
423
|
+
return typeof claims?.["email"] === "string" ? claims["email"] : void 0;
|
|
424
|
+
} catch {
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
extractTokenExpiry(token) {
|
|
429
|
+
try {
|
|
430
|
+
const claims = decodeJwtPayload(token);
|
|
431
|
+
if (typeof claims?.["exp"] === "number") {
|
|
432
|
+
return claims["exp"] * 1e3;
|
|
433
|
+
}
|
|
434
|
+
return void 0;
|
|
435
|
+
} catch {
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
extractPermissions(token) {
|
|
440
|
+
const claims = decodeJwtPayload(token);
|
|
441
|
+
return Array.isArray(claims?.permissions) ? claims.permissions.filter((item) => typeof item === "string") : [];
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/shared-enums.ts
|
|
446
|
+
var OrganizationPlan = {
|
|
447
|
+
FREE: "free",
|
|
448
|
+
PRO: "pro",
|
|
449
|
+
TEAM: "team",
|
|
450
|
+
ENTERPRISE: "enterprise"
|
|
451
|
+
};
|
|
452
|
+
var WorkflowPhase = {
|
|
453
|
+
SPECIFY: "specify",
|
|
454
|
+
CLARIFY: "clarify",
|
|
455
|
+
PLAN: "plan",
|
|
456
|
+
IMPLEMENT: "implement",
|
|
457
|
+
VERIFY: "verify",
|
|
458
|
+
COMPLETE: "complete"
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/services/client.ts
|
|
462
|
+
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
463
|
+
|
|
464
|
+
// src/utils/trpc-url.ts
|
|
465
|
+
function normalizeTrpcUrl(apiUrl) {
|
|
466
|
+
if (apiUrl.endsWith("/trpc")) {
|
|
467
|
+
return apiUrl;
|
|
468
|
+
}
|
|
469
|
+
if (apiUrl.endsWith("/")) {
|
|
470
|
+
return `${apiUrl}trpc`;
|
|
471
|
+
}
|
|
472
|
+
return `${apiUrl}/trpc`;
|
|
473
|
+
}
|
|
474
|
+
__name(normalizeTrpcUrl, "normalizeTrpcUrl");
|
|
475
|
+
|
|
476
|
+
// src/services/client.ts
|
|
477
|
+
function createApiClient(apiUrl, token, organizationId) {
|
|
478
|
+
return createTRPCProxyClient({
|
|
479
|
+
links: [
|
|
480
|
+
httpBatchLink({
|
|
481
|
+
url: normalizeTrpcUrl(apiUrl),
|
|
482
|
+
headers: {
|
|
483
|
+
authorization: token ? `Bearer ${token}` : "",
|
|
484
|
+
"x-organization-id": organizationId
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
]
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
__name(createApiClient, "createApiClient");
|
|
491
|
+
|
|
492
|
+
// src/utils/project-context.ts
|
|
493
|
+
import fs2 from "fs";
|
|
494
|
+
import os2 from "os";
|
|
495
|
+
import path2 from "path";
|
|
496
|
+
var LOCAL_CONTEXT_FILE = ".spekn";
|
|
497
|
+
var GLOBAL_CONTEXT_PATH = path2.join(os2.homedir(), ".spekn", "context.json");
|
|
498
|
+
var GLOBAL_CONFIG_PATH = path2.join(os2.homedir(), ".spekn", "config.json");
|
|
499
|
+
var GLOBAL_ERROR_LOG_PATH = "~/.spekn/error.log";
|
|
500
|
+
var DEFAULT_GLOBAL_CONFIG = {
|
|
501
|
+
logging: {
|
|
502
|
+
enabled: false,
|
|
503
|
+
path: GLOBAL_ERROR_LOG_PATH,
|
|
504
|
+
level: "info"
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var LOG_LEVEL_RANK = {
|
|
508
|
+
debug: 10,
|
|
509
|
+
info: 20,
|
|
510
|
+
warn: 30,
|
|
511
|
+
error: 40
|
|
512
|
+
};
|
|
513
|
+
function loadContextFile(filePath) {
|
|
514
|
+
try {
|
|
515
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
516
|
+
const parsed = JSON.parse(raw);
|
|
517
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
518
|
+
return parsed;
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
__name(loadContextFile, "loadContextFile");
|
|
524
|
+
function loadGlobalContextData() {
|
|
525
|
+
try {
|
|
526
|
+
const raw = fs2.readFileSync(GLOBAL_CONTEXT_PATH, "utf-8");
|
|
527
|
+
const parsed = JSON.parse(raw);
|
|
528
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
529
|
+
return parsed;
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
__name(loadGlobalContextData, "loadGlobalContextData");
|
|
535
|
+
function getGlobalProjects(global) {
|
|
536
|
+
if (!global) return [];
|
|
537
|
+
return Array.isArray(global.projects) ? global.projects : [];
|
|
538
|
+
}
|
|
539
|
+
__name(getGlobalProjects, "getGlobalProjects");
|
|
540
|
+
function isRoot(dirPath) {
|
|
541
|
+
return path2.dirname(dirPath) === dirPath;
|
|
542
|
+
}
|
|
543
|
+
__name(isRoot, "isRoot");
|
|
544
|
+
function findNearestLocalContextFile(startDir = process.cwd()) {
|
|
545
|
+
let current = path2.resolve(startDir);
|
|
546
|
+
while (true) {
|
|
547
|
+
const candidate = path2.join(current, LOCAL_CONTEXT_FILE);
|
|
548
|
+
if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
|
|
549
|
+
return candidate;
|
|
550
|
+
}
|
|
551
|
+
if (isRoot(current)) return null;
|
|
552
|
+
current = path2.dirname(current);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
__name(findNearestLocalContextFile, "findNearestLocalContextFile");
|
|
556
|
+
function resolveContextWorkspaceRoot(input) {
|
|
557
|
+
const fallback = path2.resolve(input?.fallbackDir ?? process.cwd());
|
|
558
|
+
try {
|
|
559
|
+
const raw = fs2.readFileSync(GLOBAL_CONTEXT_PATH, "utf-8");
|
|
560
|
+
const global = JSON.parse(raw);
|
|
561
|
+
const projects = getGlobalProjects(global);
|
|
562
|
+
const selectedProjectId = input?.projectId ?? global.lastUsedProjectId;
|
|
563
|
+
const selected = projects.find((entry) => entry?.id === selectedProjectId) ?? projects.find((entry) => Array.isArray(entry?.repoPaths) && entry.repoPaths.length > 0);
|
|
564
|
+
const repoPaths = Array.isArray(selected?.repoPaths) ? selected.repoPaths : [];
|
|
565
|
+
const existingPath = repoPaths.find((repoPath) => {
|
|
566
|
+
try {
|
|
567
|
+
return fs2.existsSync(repoPath);
|
|
568
|
+
} catch {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
const resolvedPath = existingPath ?? repoPaths[0];
|
|
573
|
+
if (typeof resolvedPath === "string" && resolvedPath.trim().length > 0) {
|
|
574
|
+
return path2.resolve(resolvedPath);
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
return fallback;
|
|
579
|
+
}
|
|
580
|
+
__name(resolveContextWorkspaceRoot, "resolveContextWorkspaceRoot");
|
|
581
|
+
function resolveDeclaredContext(input) {
|
|
582
|
+
const localPath = input.repoPath ? path2.join(path2.resolve(input.repoPath), LOCAL_CONTEXT_FILE) : findNearestLocalContextFile();
|
|
583
|
+
const local = localPath ? loadContextFile(localPath) : null;
|
|
584
|
+
const global = loadGlobalContextData();
|
|
585
|
+
const globalProjectId = global?.lastUsedProjectId;
|
|
586
|
+
const globalProjectOrganizationId = (globalProjectId ? getGlobalProjects(global).find((project) => project.id === globalProjectId)?.organizationId : void 0) ?? global?.defaultOrganizationId;
|
|
587
|
+
const projectId = input.explicitProjectId ?? local?.projectId ?? globalProjectId;
|
|
588
|
+
const organizationId = input.explicitOrganizationId ?? local?.organizationId ?? (projectId ? getGlobalProjects(global).find((project) => project.id === projectId)?.organizationId : void 0) ?? globalProjectOrganizationId ?? input.credentialsOrganizationId;
|
|
589
|
+
return {
|
|
590
|
+
projectId,
|
|
591
|
+
organizationId
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
__name(resolveDeclaredContext, "resolveDeclaredContext");
|
|
595
|
+
function saveLocalContext(repoPath, context) {
|
|
596
|
+
const filePath = path2.join(path2.resolve(repoPath), LOCAL_CONTEXT_FILE);
|
|
597
|
+
const content = {
|
|
598
|
+
projectId: context.projectId,
|
|
599
|
+
organizationId: context.organizationId,
|
|
600
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
601
|
+
};
|
|
602
|
+
fs2.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n", "utf-8");
|
|
603
|
+
}
|
|
604
|
+
__name(saveLocalContext, "saveLocalContext");
|
|
605
|
+
function saveGlobalContext(repoPathInput, context) {
|
|
606
|
+
const dirPath = path2.dirname(GLOBAL_CONTEXT_PATH);
|
|
607
|
+
fs2.mkdirSync(dirPath, {
|
|
608
|
+
recursive: true,
|
|
609
|
+
mode: 448
|
|
610
|
+
});
|
|
611
|
+
const existing = loadGlobalContextData();
|
|
612
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
613
|
+
const repoPath = path2.resolve(repoPathInput);
|
|
614
|
+
const organizationId = context.organizationId ?? existing?.defaultOrganizationId ?? "";
|
|
615
|
+
const previousProjects = getGlobalProjects(existing);
|
|
616
|
+
const updatedProjects = previousProjects.map((project) => project.id === context.projectId ? {
|
|
617
|
+
...project,
|
|
618
|
+
organizationId: organizationId || project.organizationId,
|
|
619
|
+
lastUsed: now,
|
|
620
|
+
repoPaths: Array.from(/* @__PURE__ */ new Set([
|
|
621
|
+
...project.repoPaths ?? [],
|
|
622
|
+
repoPath
|
|
623
|
+
]))
|
|
624
|
+
} : project);
|
|
625
|
+
if (!updatedProjects.some((project) => project.id === context.projectId) && organizationId) {
|
|
626
|
+
updatedProjects.unshift({
|
|
627
|
+
id: context.projectId,
|
|
628
|
+
organizationId,
|
|
629
|
+
lastUsed: now,
|
|
630
|
+
repoPaths: [
|
|
631
|
+
repoPath
|
|
632
|
+
]
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const content = {
|
|
636
|
+
defaultOrganizationId: organizationId || existing?.defaultOrganizationId,
|
|
637
|
+
projects: updatedProjects.slice(0, 10),
|
|
638
|
+
lastUsedProjectId: context.projectId,
|
|
639
|
+
preferences: existing?.preferences,
|
|
640
|
+
repoSync: existing?.repoSync,
|
|
641
|
+
updatedAt: now
|
|
642
|
+
};
|
|
643
|
+
fs2.writeFileSync(GLOBAL_CONTEXT_PATH, JSON.stringify(content, null, 2) + "\n", {
|
|
644
|
+
encoding: "utf-8",
|
|
645
|
+
mode: 384
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
__name(saveGlobalContext, "saveGlobalContext");
|
|
649
|
+
function saveGlobalContextSelectionOnly(context) {
|
|
650
|
+
const dirPath = path2.dirname(GLOBAL_CONTEXT_PATH);
|
|
651
|
+
fs2.mkdirSync(dirPath, {
|
|
652
|
+
recursive: true,
|
|
653
|
+
mode: 448
|
|
654
|
+
});
|
|
655
|
+
const existing = loadGlobalContextData();
|
|
656
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
657
|
+
const organizationId = context.organizationId ?? existing?.defaultOrganizationId ?? "";
|
|
658
|
+
const previousProjects = getGlobalProjects(existing);
|
|
659
|
+
const updatedProjects = previousProjects.map((project) => project.id === context.projectId ? {
|
|
660
|
+
...project,
|
|
661
|
+
organizationId: organizationId || project.organizationId,
|
|
662
|
+
lastUsed: now
|
|
663
|
+
} : project);
|
|
664
|
+
if (!updatedProjects.some((project) => project.id === context.projectId) && organizationId) {
|
|
665
|
+
updatedProjects.unshift({
|
|
666
|
+
id: context.projectId,
|
|
667
|
+
organizationId,
|
|
668
|
+
lastUsed: now,
|
|
669
|
+
repoPaths: []
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
const content = {
|
|
673
|
+
defaultOrganizationId: organizationId || existing?.defaultOrganizationId,
|
|
674
|
+
projects: updatedProjects.slice(0, 10),
|
|
675
|
+
lastUsedProjectId: context.projectId,
|
|
676
|
+
preferences: existing?.preferences,
|
|
677
|
+
repoSync: existing?.repoSync,
|
|
678
|
+
updatedAt: now
|
|
679
|
+
};
|
|
680
|
+
fs2.writeFileSync(GLOBAL_CONTEXT_PATH, JSON.stringify(content, null, 2) + "\n", {
|
|
681
|
+
encoding: "utf-8",
|
|
682
|
+
mode: 384
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
__name(saveGlobalContextSelectionOnly, "saveGlobalContextSelectionOnly");
|
|
686
|
+
function ensureGitignoreHasSpekn(repoPath) {
|
|
687
|
+
const gitignorePath = path2.join(path2.resolve(repoPath), ".gitignore");
|
|
688
|
+
const entry = ".spekn";
|
|
689
|
+
if (!fs2.existsSync(gitignorePath)) {
|
|
690
|
+
fs2.writeFileSync(gitignorePath, `${entry}
|
|
691
|
+
`, "utf-8");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const content = fs2.readFileSync(gitignorePath, "utf-8");
|
|
695
|
+
const lines = content.split(/\r?\n/);
|
|
696
|
+
if (lines.some((line) => line.trim() === entry)) return;
|
|
697
|
+
const needsNewline = content.length > 0 && !content.endsWith("\n");
|
|
698
|
+
const toAppend = `${needsNewline ? "\n" : ""}${entry}
|
|
699
|
+
`;
|
|
700
|
+
fs2.appendFileSync(gitignorePath, toAppend, "utf-8");
|
|
701
|
+
}
|
|
702
|
+
__name(ensureGitignoreHasSpekn, "ensureGitignoreHasSpekn");
|
|
703
|
+
function hasLocalContext(repoPath) {
|
|
704
|
+
const startDir = repoPath ? path2.resolve(repoPath) : process.cwd();
|
|
705
|
+
return findNearestLocalContextFile(startDir) !== null;
|
|
706
|
+
}
|
|
707
|
+
__name(hasLocalContext, "hasLocalContext");
|
|
708
|
+
function persistProjectContext(repoPath, context) {
|
|
709
|
+
saveLocalContext(repoPath, context);
|
|
710
|
+
saveGlobalContext(repoPath, context);
|
|
711
|
+
ensureGitignoreHasSpekn(repoPath);
|
|
712
|
+
}
|
|
713
|
+
__name(persistProjectContext, "persistProjectContext");
|
|
714
|
+
function persistSelectedProjectContext(context) {
|
|
715
|
+
saveGlobalContextSelectionOnly(context);
|
|
716
|
+
}
|
|
717
|
+
__name(persistSelectedProjectContext, "persistSelectedProjectContext");
|
|
718
|
+
function persistProjectContextWithoutRepoPath(repoPath, context) {
|
|
719
|
+
saveLocalContext(repoPath, context);
|
|
720
|
+
saveGlobalContextSelectionOnly(context);
|
|
721
|
+
ensureGitignoreHasSpekn(repoPath);
|
|
722
|
+
}
|
|
723
|
+
__name(persistProjectContextWithoutRepoPath, "persistProjectContextWithoutRepoPath");
|
|
724
|
+
function loadGlobalContextConfig() {
|
|
725
|
+
try {
|
|
726
|
+
const raw = fs2.readFileSync(GLOBAL_CONFIG_PATH, "utf-8");
|
|
727
|
+
const parsed = JSON.parse(raw);
|
|
728
|
+
const levelCandidate = parsed?.logging?.level;
|
|
729
|
+
const level = levelCandidate === "debug" || levelCandidate === "info" || levelCandidate === "warn" || levelCandidate === "error" ? levelCandidate : DEFAULT_GLOBAL_CONFIG.logging.level;
|
|
730
|
+
return {
|
|
731
|
+
logging: {
|
|
732
|
+
enabled: typeof parsed?.logging?.enabled === "boolean" ? parsed.logging.enabled : DEFAULT_GLOBAL_CONFIG.logging.enabled,
|
|
733
|
+
path: typeof parsed?.logging?.path === "string" && parsed.logging.path.trim().length > 0 ? parsed.logging.path : DEFAULT_GLOBAL_CONFIG.logging.path,
|
|
734
|
+
level
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
} catch {
|
|
738
|
+
return DEFAULT_GLOBAL_CONFIG;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
__name(loadGlobalContextConfig, "loadGlobalContextConfig");
|
|
742
|
+
function saveGlobalContextConfig(config) {
|
|
743
|
+
const dirPath = path2.dirname(GLOBAL_CONFIG_PATH);
|
|
744
|
+
fs2.mkdirSync(dirPath, {
|
|
745
|
+
recursive: true,
|
|
746
|
+
mode: 448
|
|
747
|
+
});
|
|
748
|
+
fs2.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
749
|
+
encoding: "utf-8",
|
|
750
|
+
mode: 384
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
__name(saveGlobalContextConfig, "saveGlobalContextConfig");
|
|
754
|
+
function ensureGlobalContextConfig() {
|
|
755
|
+
const config = loadGlobalContextConfig();
|
|
756
|
+
if (!fs2.existsSync(GLOBAL_CONFIG_PATH)) {
|
|
757
|
+
saveGlobalContextConfig(config);
|
|
758
|
+
}
|
|
759
|
+
return config;
|
|
760
|
+
}
|
|
761
|
+
__name(ensureGlobalContextConfig, "ensureGlobalContextConfig");
|
|
762
|
+
function expandHomePath(filePath) {
|
|
763
|
+
if (filePath === "~") return os2.homedir();
|
|
764
|
+
if (filePath.startsWith("~/")) {
|
|
765
|
+
return path2.join(os2.homedir(), filePath.slice(2));
|
|
766
|
+
}
|
|
767
|
+
return filePath;
|
|
768
|
+
}
|
|
769
|
+
__name(expandHomePath, "expandHomePath");
|
|
770
|
+
function appendGlobalStructuredLog(input) {
|
|
771
|
+
try {
|
|
772
|
+
const config = ensureGlobalContextConfig();
|
|
773
|
+
if (!config.logging.enabled) return;
|
|
774
|
+
if (LOG_LEVEL_RANK[input.level] < LOG_LEVEL_RANK[config.logging.level]) return;
|
|
775
|
+
const logPath = path2.resolve(expandHomePath(config.logging.path));
|
|
776
|
+
const dirPath = path2.dirname(logPath);
|
|
777
|
+
fs2.mkdirSync(dirPath, {
|
|
778
|
+
recursive: true,
|
|
779
|
+
mode: 448
|
|
780
|
+
});
|
|
781
|
+
const payload = {
|
|
782
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
783
|
+
source: input.source,
|
|
784
|
+
level: input.level,
|
|
785
|
+
message: input.message,
|
|
786
|
+
details: input.details ?? {}
|
|
787
|
+
};
|
|
788
|
+
fs2.appendFileSync(logPath, `${JSON.stringify(payload)}
|
|
789
|
+
`, {
|
|
790
|
+
encoding: "utf-8",
|
|
791
|
+
mode: 384
|
|
792
|
+
});
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
__name(appendGlobalStructuredLog, "appendGlobalStructuredLog");
|
|
797
|
+
function appendGlobalErrorLog(input) {
|
|
798
|
+
appendGlobalStructuredLog({
|
|
799
|
+
source: input.source,
|
|
800
|
+
level: "error",
|
|
801
|
+
message: input.message,
|
|
802
|
+
details: input.details
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
__name(appendGlobalErrorLog, "appendGlobalErrorLog");
|
|
806
|
+
|
|
807
|
+
// src/services/bootstrap-service.ts
|
|
808
|
+
function normalizePlan(raw) {
|
|
809
|
+
if (raw === OrganizationPlan.PRO) return OrganizationPlan.PRO;
|
|
810
|
+
if (raw === OrganizationPlan.TEAM) return OrganizationPlan.TEAM;
|
|
811
|
+
if (raw === OrganizationPlan.ENTERPRISE) return OrganizationPlan.ENTERPRISE;
|
|
812
|
+
return OrganizationPlan.FREE;
|
|
813
|
+
}
|
|
814
|
+
__name(normalizePlan, "normalizePlan");
|
|
815
|
+
function normalizeRole(raw) {
|
|
816
|
+
if (raw === "owner" || raw === "admin" || raw === "member" || raw === "viewer") {
|
|
817
|
+
return raw;
|
|
818
|
+
}
|
|
819
|
+
return "member";
|
|
820
|
+
}
|
|
821
|
+
__name(normalizeRole, "normalizeRole");
|
|
822
|
+
var BootstrapService = class {
|
|
823
|
+
static {
|
|
824
|
+
__name(this, "BootstrapService");
|
|
825
|
+
}
|
|
826
|
+
apiUrl;
|
|
827
|
+
credentialsStore = new CredentialsStore();
|
|
828
|
+
authService;
|
|
829
|
+
constructor(apiUrl) {
|
|
830
|
+
this.apiUrl = apiUrl;
|
|
831
|
+
this.authService = new AuthService(apiUrl);
|
|
832
|
+
}
|
|
833
|
+
async bootstrap(projectIdArg) {
|
|
834
|
+
const token = await this.credentialsStore.getValidToken();
|
|
835
|
+
if (!token) {
|
|
836
|
+
throw new Error("No valid credentials. Run `spekn auth login` first.");
|
|
837
|
+
}
|
|
838
|
+
const permissions = this.authService.extractPermissions(token);
|
|
839
|
+
const stored = this.credentialsStore.load();
|
|
840
|
+
const declared = resolveDeclaredContext({
|
|
841
|
+
explicitProjectId: projectIdArg,
|
|
842
|
+
repoPath: process.cwd(),
|
|
843
|
+
credentialsOrganizationId: stored?.organizationId,
|
|
844
|
+
envOrganizationId: process.env.SPEKN_ORGANIZATION_ID
|
|
845
|
+
});
|
|
846
|
+
if (!declared.projectId) {
|
|
847
|
+
throw new Error("PROJECT_CONTEXT_REQUIRED");
|
|
848
|
+
}
|
|
849
|
+
const fallbackOrg = declared.organizationId ?? "";
|
|
850
|
+
const bootstrapClient = createApiClient(this.apiUrl, token, fallbackOrg);
|
|
851
|
+
const orgs = await bootstrapClient.organization.list.query();
|
|
852
|
+
if (orgs.length === 0) {
|
|
853
|
+
throw new Error("No organization membership found for this account.");
|
|
854
|
+
}
|
|
855
|
+
const org = orgs.find((candidate) => candidate.id === fallbackOrg) ?? orgs[0];
|
|
856
|
+
const organizationId = org.id;
|
|
857
|
+
const client2 = createApiClient(this.apiUrl, token, organizationId);
|
|
858
|
+
const projects = await client2.project.list.query({
|
|
859
|
+
limit: 20,
|
|
860
|
+
offset: 0
|
|
861
|
+
});
|
|
862
|
+
if (projects.length === 0) {
|
|
863
|
+
throw new Error("ONBOARDING_REQUIRED");
|
|
864
|
+
}
|
|
865
|
+
const project = projects.find((candidate) => candidate.id === declared.projectId);
|
|
866
|
+
if (!project) {
|
|
867
|
+
throw new Error("ONBOARDING_REQUIRED");
|
|
868
|
+
}
|
|
869
|
+
const repoPath = resolveContextWorkspaceRoot({
|
|
870
|
+
projectId: project.id,
|
|
871
|
+
fallbackDir: process.cwd()
|
|
872
|
+
});
|
|
873
|
+
return {
|
|
874
|
+
boot: {
|
|
875
|
+
apiUrl: this.apiUrl,
|
|
876
|
+
organizationId,
|
|
877
|
+
organizationName: org.name,
|
|
878
|
+
role: normalizeRole(org.role),
|
|
879
|
+
plan: normalizePlan(org.plan),
|
|
880
|
+
projectId: project.id,
|
|
881
|
+
projectName: project.name,
|
|
882
|
+
repoPath,
|
|
883
|
+
permissions
|
|
884
|
+
},
|
|
885
|
+
client: client2
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
hasDeclaredProjectContext(projectIdArg) {
|
|
889
|
+
const stored = this.credentialsStore.load();
|
|
890
|
+
const declared = resolveDeclaredContext({
|
|
891
|
+
explicitProjectId: projectIdArg,
|
|
892
|
+
repoPath: process.cwd(),
|
|
893
|
+
credentialsOrganizationId: stored?.organizationId,
|
|
894
|
+
envOrganizationId: process.env.SPEKN_ORGANIZATION_ID
|
|
895
|
+
});
|
|
896
|
+
return Boolean(declared.projectId);
|
|
897
|
+
}
|
|
898
|
+
async loadWorkflowSummary(client2, projectId) {
|
|
899
|
+
const states = await client2.workflowState.listByProject.query({
|
|
900
|
+
projectId
|
|
901
|
+
});
|
|
902
|
+
const first = Array.isArray(states) && states.length > 0 ? states[0] : null;
|
|
903
|
+
const currentPhase = first?.currentPhase ?? null;
|
|
904
|
+
const blockedCount = Array.isArray(states) ? states.filter((state) => state.specificationLockStatus === "locked").length : 0;
|
|
905
|
+
return {
|
|
906
|
+
currentPhase,
|
|
907
|
+
blockedCount,
|
|
908
|
+
hasVerificationEvidence: Boolean(first?.hasVerificationEvidence),
|
|
909
|
+
hasPlanningArtifacts: Boolean(first?.hasPlanningArtifacts)
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
hasLocalProjectContext(repoPath) {
|
|
913
|
+
return hasLocalContext(repoPath);
|
|
914
|
+
}
|
|
915
|
+
persistContext(organizationId, projectId) {
|
|
916
|
+
persistSelectedProjectContext({
|
|
917
|
+
organizationId,
|
|
918
|
+
projectId
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/services/bridge-service.ts
|
|
924
|
+
import fs3 from "fs";
|
|
925
|
+
import os3 from "os";
|
|
926
|
+
import path3 from "path";
|
|
927
|
+
import { spawn as spawn2 } from "child_process";
|
|
928
|
+
var DEFAULT_BRIDGE_CONFIG = {
|
|
929
|
+
port: 19550,
|
|
930
|
+
pairing: null
|
|
931
|
+
};
|
|
932
|
+
function loadLocalBridgeConfig() {
|
|
933
|
+
const configPath = path3.join(os3.homedir(), ".spekn", "bridge", "config.json");
|
|
934
|
+
try {
|
|
935
|
+
const raw = fs3.readFileSync(configPath, "utf-8");
|
|
936
|
+
const parsed = JSON.parse(raw);
|
|
937
|
+
return {
|
|
938
|
+
port: typeof parsed.port === "number" ? parsed.port : DEFAULT_BRIDGE_CONFIG.port,
|
|
939
|
+
pairing: parsed.pairing ?? DEFAULT_BRIDGE_CONFIG.pairing
|
|
940
|
+
};
|
|
941
|
+
} catch {
|
|
942
|
+
return DEFAULT_BRIDGE_CONFIG;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
__name(loadLocalBridgeConfig, "loadLocalBridgeConfig");
|
|
946
|
+
async function loadWebMcpChannels(port) {
|
|
947
|
+
try {
|
|
948
|
+
const res = await fetch(`http://127.0.0.1:${port}/webmcp/channels`);
|
|
949
|
+
if (!res.ok) return [];
|
|
950
|
+
const data = await res.json();
|
|
951
|
+
return data.channels ?? [];
|
|
952
|
+
} catch {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
__name(loadWebMcpChannels, "loadWebMcpChannels");
|
|
957
|
+
var BridgeService = class {
|
|
958
|
+
static {
|
|
959
|
+
__name(this, "BridgeService");
|
|
960
|
+
}
|
|
961
|
+
cliRunner;
|
|
962
|
+
constructor(apiUrl) {
|
|
963
|
+
this.cliRunner = new CliRunner(apiUrl);
|
|
964
|
+
}
|
|
965
|
+
async loadBridgeSummary(client2) {
|
|
966
|
+
const [flag, devices, metrics] = await Promise.all([
|
|
967
|
+
client2.bridge.getFeatureFlag.query().catch(() => ({
|
|
968
|
+
enabled: false
|
|
969
|
+
})),
|
|
970
|
+
client2.bridge.listDevices.query().catch(() => []),
|
|
971
|
+
client2.bridge.getMetrics.query().catch(() => ({
|
|
972
|
+
connectedDevices: 0,
|
|
973
|
+
authFailures: 0
|
|
974
|
+
}))
|
|
975
|
+
]);
|
|
976
|
+
return {
|
|
977
|
+
featureEnabled: Boolean(flag.enabled),
|
|
978
|
+
devices: Array.isArray(devices) ? devices.map((device) => ({
|
|
979
|
+
id: device.id,
|
|
980
|
+
name: device.name,
|
|
981
|
+
status: device.status,
|
|
982
|
+
isDefault: Boolean(device.isDefault),
|
|
983
|
+
lastSeenAt: device.lastSeenAt
|
|
984
|
+
})) : [],
|
|
985
|
+
connectedDevices: Number(metrics.connectedDevices ?? 0),
|
|
986
|
+
authFailures: Number(metrics.authFailures ?? 0)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
async loadLocalBridgeSummary() {
|
|
990
|
+
const config = loadLocalBridgeConfig();
|
|
991
|
+
let running = false;
|
|
992
|
+
let uptimeSec;
|
|
993
|
+
try {
|
|
994
|
+
const response = await fetch(`http://127.0.0.1:${config.port}/health`);
|
|
995
|
+
if (response.ok) {
|
|
996
|
+
const payload = await response.json();
|
|
997
|
+
running = true;
|
|
998
|
+
uptimeSec = Number(payload.uptime ?? 0);
|
|
999
|
+
}
|
|
1000
|
+
} catch {
|
|
1001
|
+
running = false;
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
paired: config.pairing !== null,
|
|
1005
|
+
deviceId: config.pairing?.deviceId,
|
|
1006
|
+
deviceName: config.pairing?.deviceName,
|
|
1007
|
+
port: config.port,
|
|
1008
|
+
running,
|
|
1009
|
+
uptimeSec
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
startLocalBridgeDetached() {
|
|
1013
|
+
const cliEntry = this.cliRunner.resolveCliEntry();
|
|
1014
|
+
if (!cliEntry) return;
|
|
1015
|
+
const args = [
|
|
1016
|
+
cliEntry,
|
|
1017
|
+
"bridge",
|
|
1018
|
+
"start"
|
|
1019
|
+
];
|
|
1020
|
+
const child = spawn2(process.execPath, args, {
|
|
1021
|
+
detached: true,
|
|
1022
|
+
stdio: "ignore"
|
|
1023
|
+
});
|
|
1024
|
+
child.unref();
|
|
1025
|
+
}
|
|
1026
|
+
async stopLocalBridge(configPort) {
|
|
1027
|
+
const port = configPort ?? loadLocalBridgeConfig().port;
|
|
1028
|
+
try {
|
|
1029
|
+
await fetch(`http://127.0.0.1:${port}/shutdown`, {
|
|
1030
|
+
method: "POST"
|
|
1031
|
+
});
|
|
1032
|
+
} catch {
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async loadBridgeLogs(port, since) {
|
|
1036
|
+
const p = port ?? loadLocalBridgeConfig().port;
|
|
1037
|
+
try {
|
|
1038
|
+
const url = since ? `http://127.0.0.1:${p}/logs?since=${since}` : `http://127.0.0.1:${p}/logs`;
|
|
1039
|
+
const res = await fetch(url);
|
|
1040
|
+
if (!res.ok) return [];
|
|
1041
|
+
const data = await res.json();
|
|
1042
|
+
return data.logs;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
// src/services/decision-service.ts
|
|
1050
|
+
var DecisionService = class {
|
|
1051
|
+
static {
|
|
1052
|
+
__name(this, "DecisionService");
|
|
1053
|
+
}
|
|
1054
|
+
async loadDecisions(client2, projectId) {
|
|
1055
|
+
const result = await client2.decision.getAll.query({
|
|
1056
|
+
projectId,
|
|
1057
|
+
limit: 50,
|
|
1058
|
+
offset: 0
|
|
1059
|
+
});
|
|
1060
|
+
const decisions = Array.isArray(result?.decisions) ? result.decisions : [];
|
|
1061
|
+
return decisions.map((decision) => ({
|
|
1062
|
+
id: decision.id,
|
|
1063
|
+
title: decision.title,
|
|
1064
|
+
status: decision.status,
|
|
1065
|
+
decisionType: decision.decisionType,
|
|
1066
|
+
specAnchor: decision.specAnchor,
|
|
1067
|
+
createdAt: decision.createdAt
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
1070
|
+
async resolveDecision(client2, projectId, decisionId, status, reason, existingRationale) {
|
|
1071
|
+
await client2.decision.update.mutate({
|
|
1072
|
+
projectId,
|
|
1073
|
+
id: decisionId,
|
|
1074
|
+
data: {
|
|
1075
|
+
status,
|
|
1076
|
+
rationale: reason || existingRationale
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
async deleteDecision(client2, projectId, decisionId, mode = "archive") {
|
|
1081
|
+
const deleteMutation = client2?.decision?.delete?.mutate;
|
|
1082
|
+
if (typeof deleteMutation !== "function") {
|
|
1083
|
+
throw new Error("Decision delete route is unavailable.");
|
|
1084
|
+
}
|
|
1085
|
+
await deleteMutation({
|
|
1086
|
+
projectId,
|
|
1087
|
+
id: decisionId,
|
|
1088
|
+
mode
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// src/services/export-service.ts
|
|
1094
|
+
var ExportService = class {
|
|
1095
|
+
static {
|
|
1096
|
+
__name(this, "ExportService");
|
|
1097
|
+
}
|
|
1098
|
+
apiUrl;
|
|
1099
|
+
cliRunner;
|
|
1100
|
+
constructor(apiUrl) {
|
|
1101
|
+
this.apiUrl = apiUrl;
|
|
1102
|
+
this.cliRunner = new CliRunner(apiUrl);
|
|
1103
|
+
}
|
|
1104
|
+
async previewExport(client2, projectId, format) {
|
|
1105
|
+
const result = await client2.export.preview.query({
|
|
1106
|
+
projectId,
|
|
1107
|
+
formatId: format
|
|
1108
|
+
});
|
|
1109
|
+
return {
|
|
1110
|
+
content: String(result.content ?? ""),
|
|
1111
|
+
anchorCount: Number(result.anchorCount ?? 0),
|
|
1112
|
+
specGeneration: typeof result.specGeneration === "number" ? result.specGeneration : void 0,
|
|
1113
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.filter((warning) => typeof warning === "string") : void 0
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
async generateExport(client2, projectId, format) {
|
|
1117
|
+
const result = await client2.export.generate.mutate({
|
|
1118
|
+
projectId,
|
|
1119
|
+
formatId: format
|
|
1120
|
+
});
|
|
1121
|
+
return {
|
|
1122
|
+
content: String(result.content ?? ""),
|
|
1123
|
+
anchorCount: Number(result.anchorCount ?? 0),
|
|
1124
|
+
specGeneration: typeof result.specGeneration === "number" ? result.specGeneration : void 0,
|
|
1125
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.filter((warning) => typeof warning === "string") : void 0
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
async discoverExportCapabilities(projectId, organizationId) {
|
|
1129
|
+
const fallback = {
|
|
1130
|
+
plan: "free",
|
|
1131
|
+
modes: [
|
|
1132
|
+
"global"
|
|
1133
|
+
],
|
|
1134
|
+
formats: [
|
|
1135
|
+
{
|
|
1136
|
+
id: "agents-md"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
id: "claude-md"
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
id: "cursorrules"
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
id: "gemini-md"
|
|
1146
|
+
}
|
|
1147
|
+
]
|
|
1148
|
+
};
|
|
1149
|
+
const cliResult = await this.cliRunner.runCliJson([
|
|
1150
|
+
"export",
|
|
1151
|
+
"discover",
|
|
1152
|
+
"--project",
|
|
1153
|
+
projectId,
|
|
1154
|
+
"--api-url",
|
|
1155
|
+
this.apiUrl,
|
|
1156
|
+
"--json"
|
|
1157
|
+
], void 0, organizationId);
|
|
1158
|
+
if (!cliResult.ok) {
|
|
1159
|
+
return fallback;
|
|
1160
|
+
}
|
|
1161
|
+
const result = cliResult.value;
|
|
1162
|
+
const modes = Array.isArray(result?.modes) ? result.modes.filter((mode) => mode === "global" || mode === "scoped") : fallback.modes;
|
|
1163
|
+
const formats = Array.isArray(result?.formats) ? result.formats.map((format) => {
|
|
1164
|
+
if (format?.id !== "agents-md" && format?.id !== "claude-md" && format?.id !== "cursorrules" && format?.id !== "gemini-md") {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
const formatModes = Array.isArray(format?.modes) ? format.modes.filter((mode) => mode === "global" || mode === "scoped") : void 0;
|
|
1168
|
+
return {
|
|
1169
|
+
id: format.id,
|
|
1170
|
+
name: typeof format?.name === "string" ? format.name : void 0,
|
|
1171
|
+
filename: typeof format?.filename === "string" ? format.filename : void 0,
|
|
1172
|
+
modes: formatModes
|
|
1173
|
+
};
|
|
1174
|
+
}).filter((format) => format !== null) : fallback.formats;
|
|
1175
|
+
return {
|
|
1176
|
+
plan: result?.plan === "pro" || result?.plan === "team" || result?.plan === "enterprise" ? result.plan : "free",
|
|
1177
|
+
modes: modes.length > 0 ? modes : fallback.modes,
|
|
1178
|
+
formats: formats.length > 0 ? formats : fallback.formats
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
async exportStatus(projectId, format, options) {
|
|
1182
|
+
const args = [
|
|
1183
|
+
"export",
|
|
1184
|
+
"status",
|
|
1185
|
+
"--project",
|
|
1186
|
+
projectId,
|
|
1187
|
+
"--format",
|
|
1188
|
+
format,
|
|
1189
|
+
"--api-url",
|
|
1190
|
+
this.apiUrl,
|
|
1191
|
+
"--json"
|
|
1192
|
+
];
|
|
1193
|
+
if (options?.mode) args.push("--mode", options.mode);
|
|
1194
|
+
if (options?.scopePath) args.push("--scope", options.scopePath);
|
|
1195
|
+
const cliResult = await this.cliRunner.runCliJson(args, void 0, options?.organizationId);
|
|
1196
|
+
if (!cliResult.ok) {
|
|
1197
|
+
return {
|
|
1198
|
+
overall: "missing",
|
|
1199
|
+
targets: []
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const result = cliResult.value;
|
|
1203
|
+
const overall = result?.overall === "diverged" || result?.overall === "outdated" || result?.overall === "up-to-date" ? result.overall : "missing";
|
|
1204
|
+
const targets = Array.isArray(result?.targets) ? result.targets.map((target) => {
|
|
1205
|
+
const status = target?.status === "diverged" || target?.status === "outdated" || target?.status === "up-to-date" ? target.status : "missing";
|
|
1206
|
+
const kind = target?.kind === "canonical" ? "canonical" : "entrypoint";
|
|
1207
|
+
if (typeof target?.path !== "string") return null;
|
|
1208
|
+
return {
|
|
1209
|
+
path: target.path,
|
|
1210
|
+
kind,
|
|
1211
|
+
status
|
|
1212
|
+
};
|
|
1213
|
+
}).filter((target) => target !== null) : [];
|
|
1214
|
+
return {
|
|
1215
|
+
overall,
|
|
1216
|
+
targets
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
async deliverExport(projectId, format, delivery, options) {
|
|
1220
|
+
const args = [
|
|
1221
|
+
"export",
|
|
1222
|
+
"--project",
|
|
1223
|
+
projectId,
|
|
1224
|
+
"--format",
|
|
1225
|
+
format,
|
|
1226
|
+
"--delivery",
|
|
1227
|
+
delivery,
|
|
1228
|
+
"--api-url",
|
|
1229
|
+
this.apiUrl,
|
|
1230
|
+
"--json"
|
|
1231
|
+
];
|
|
1232
|
+
if (options?.mode) args.push("--mode", options.mode);
|
|
1233
|
+
if (options?.scopePath) args.push("--scope", options.scopePath);
|
|
1234
|
+
const exportCwd = delivery === "download" ? resolveContextWorkspaceRoot({
|
|
1235
|
+
projectId,
|
|
1236
|
+
fallbackDir: process.cwd()
|
|
1237
|
+
}) : process.cwd();
|
|
1238
|
+
const cliResult = await this.cliRunner.runCliJson(args, exportCwd, options?.organizationId);
|
|
1239
|
+
if (!cliResult.ok) {
|
|
1240
|
+
throw new Error(cliResult.error);
|
|
1241
|
+
}
|
|
1242
|
+
const result = cliResult.value;
|
|
1243
|
+
if (delivery === "commit") {
|
|
1244
|
+
return {
|
|
1245
|
+
message: typeof result?.commitSha === "string" ? `Committed to ${result.branch ?? "branch"}: ${result.commitSha}` : "Export commit completed."
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (delivery === "pr") {
|
|
1249
|
+
return {
|
|
1250
|
+
message: typeof result?.prUrl === "string" ? `PR #${result.prNumber ?? "?"}: ${result.prUrl}` : "Export PR created."
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
if (delivery === "download") {
|
|
1254
|
+
const targets = Array.isArray(result?.targets) ? result.targets.map((target) => {
|
|
1255
|
+
if (typeof target?.path !== "string") return null;
|
|
1256
|
+
return {
|
|
1257
|
+
path: target.path,
|
|
1258
|
+
kind: target?.kind === "canonical" ? "canonical" : "entrypoint",
|
|
1259
|
+
status: "up-to-date"
|
|
1260
|
+
};
|
|
1261
|
+
}).filter((target) => target !== null) : [];
|
|
1262
|
+
const count = targets.length;
|
|
1263
|
+
return {
|
|
1264
|
+
message: `Wrote ${count} export file(s) to ${exportCwd}.`,
|
|
1265
|
+
localStatus: {
|
|
1266
|
+
overall: "up-to-date",
|
|
1267
|
+
targets
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
return {
|
|
1272
|
+
message: "Export copy payload generated."
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// src/services/organization-service.ts
|
|
1278
|
+
var OrganizationService = class {
|
|
1279
|
+
static {
|
|
1280
|
+
__name(this, "OrganizationService");
|
|
1281
|
+
}
|
|
1282
|
+
apiUrl;
|
|
1283
|
+
credentialsStore = new CredentialsStore();
|
|
1284
|
+
constructor(apiUrl) {
|
|
1285
|
+
this.apiUrl = apiUrl;
|
|
1286
|
+
}
|
|
1287
|
+
async listOrganizations() {
|
|
1288
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1289
|
+
if (!token) {
|
|
1290
|
+
throw new Error("No valid credentials. Run `spekn auth login` first.");
|
|
1291
|
+
}
|
|
1292
|
+
const stored = this.credentialsStore.load();
|
|
1293
|
+
const fallbackOrg = stored?.organizationId ?? process.env.SPEKN_ORGANIZATION_ID ?? "";
|
|
1294
|
+
const bootstrapClient = createApiClient(this.apiUrl, token, fallbackOrg);
|
|
1295
|
+
const orgs = await bootstrapClient.organization.list.query();
|
|
1296
|
+
return orgs.map((org) => ({
|
|
1297
|
+
id: org.id,
|
|
1298
|
+
name: org.name,
|
|
1299
|
+
plan: org.plan,
|
|
1300
|
+
role: org.role
|
|
1301
|
+
}));
|
|
1302
|
+
}
|
|
1303
|
+
async createOrganization(input) {
|
|
1304
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1305
|
+
if (!token) {
|
|
1306
|
+
throw new Error("No valid credentials. Run `spekn auth login` first.");
|
|
1307
|
+
}
|
|
1308
|
+
const stored = this.credentialsStore.load();
|
|
1309
|
+
const fallbackOrg = stored?.organizationId ?? process.env.SPEKN_ORGANIZATION_ID ?? "";
|
|
1310
|
+
const bootstrapClient = createApiClient(this.apiUrl, token, fallbackOrg);
|
|
1311
|
+
const createdOrg = await bootstrapClient.organization.create.mutate(input);
|
|
1312
|
+
return {
|
|
1313
|
+
id: createdOrg.id,
|
|
1314
|
+
name: createdOrg.name,
|
|
1315
|
+
plan: createdOrg.plan,
|
|
1316
|
+
role: "owner"
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
async listProjects(organizationId) {
|
|
1320
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1321
|
+
if (!token) {
|
|
1322
|
+
throw new Error("No valid credentials. Run `spekn auth login` first.");
|
|
1323
|
+
}
|
|
1324
|
+
const client2 = createApiClient(this.apiUrl, token, organizationId);
|
|
1325
|
+
const projects = await client2.project.list.query({
|
|
1326
|
+
limit: 100,
|
|
1327
|
+
offset: 0
|
|
1328
|
+
});
|
|
1329
|
+
return projects.map((project) => ({
|
|
1330
|
+
id: project.id,
|
|
1331
|
+
name: project.name
|
|
1332
|
+
}));
|
|
1333
|
+
}
|
|
1334
|
+
async createProject(organizationId, name) {
|
|
1335
|
+
try {
|
|
1336
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1337
|
+
if (!token) {
|
|
1338
|
+
throw new Error("No valid credentials. Run `spekn auth login` first.");
|
|
1339
|
+
}
|
|
1340
|
+
const client2 = createApiClient(this.apiUrl, token, organizationId);
|
|
1341
|
+
const created = await client2.project.create.mutate({
|
|
1342
|
+
name: name.trim()
|
|
1343
|
+
});
|
|
1344
|
+
return {
|
|
1345
|
+
id: created.id,
|
|
1346
|
+
name: String(created.name ?? name.trim())
|
|
1347
|
+
};
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1350
|
+
appendGlobalErrorLog({
|
|
1351
|
+
source: "wizard.createProject",
|
|
1352
|
+
message,
|
|
1353
|
+
details: {
|
|
1354
|
+
apiUrl: this.apiUrl,
|
|
1355
|
+
organizationId,
|
|
1356
|
+
projectName: name.trim(),
|
|
1357
|
+
cwd: process.cwd()
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
canCreateProject(org) {
|
|
1364
|
+
const role = String(org?.role ?? "").toLowerCase();
|
|
1365
|
+
return role === "owner" || role === "admin";
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// src/services/repo-service.ts
|
|
1370
|
+
import { execFileSync } from "child_process";
|
|
1371
|
+
import path4 from "path";
|
|
1372
|
+
var RepoService = class {
|
|
1373
|
+
static {
|
|
1374
|
+
__name(this, "RepoService");
|
|
1375
|
+
}
|
|
1376
|
+
apiUrl;
|
|
1377
|
+
credentialsStore = new CredentialsStore();
|
|
1378
|
+
cliRunner;
|
|
1379
|
+
constructor(apiUrl) {
|
|
1380
|
+
this.apiUrl = apiUrl;
|
|
1381
|
+
this.cliRunner = new CliRunner(apiUrl);
|
|
1382
|
+
}
|
|
1383
|
+
attachOrSyncCurrentRepository = /* @__PURE__ */ __name(async (organizationId, projectId, onProgress, _agentType, requestInteraction, onActivity) => {
|
|
1384
|
+
const repoPath = process.cwd();
|
|
1385
|
+
let args = [];
|
|
1386
|
+
let lastOutputLines = [];
|
|
1387
|
+
try {
|
|
1388
|
+
const remoteUrl = this.execGit(repoPath, [
|
|
1389
|
+
"remote",
|
|
1390
|
+
"get-url",
|
|
1391
|
+
"origin"
|
|
1392
|
+
]);
|
|
1393
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1394
|
+
if (!token) {
|
|
1395
|
+
throw new Error("No valid credentials. Run `spekn auth login` first.");
|
|
1396
|
+
}
|
|
1397
|
+
const client2 = createApiClient(this.apiUrl, token, organizationId);
|
|
1398
|
+
const repos = await client2.gitRepository.list.query({
|
|
1399
|
+
projectId,
|
|
1400
|
+
limit: 100,
|
|
1401
|
+
offset: 0
|
|
1402
|
+
});
|
|
1403
|
+
const isAlreadyAttached = repos.some((repo) => repo.repositoryUrl === remoteUrl);
|
|
1404
|
+
const cliEntry = this.cliRunner.resolveCliEntry();
|
|
1405
|
+
if (!cliEntry) {
|
|
1406
|
+
throw new Error("Could not resolve CLI entrypoint for repo register.");
|
|
1407
|
+
}
|
|
1408
|
+
const runAnalyze = !isAlreadyAttached;
|
|
1409
|
+
args = isAlreadyAttached ? [
|
|
1410
|
+
cliEntry,
|
|
1411
|
+
"repo",
|
|
1412
|
+
"sync",
|
|
1413
|
+
"--project-id",
|
|
1414
|
+
projectId,
|
|
1415
|
+
"--path",
|
|
1416
|
+
repoPath,
|
|
1417
|
+
"--api-url",
|
|
1418
|
+
this.apiUrl,
|
|
1419
|
+
"--no-analyze"
|
|
1420
|
+
] : [
|
|
1421
|
+
cliEntry,
|
|
1422
|
+
"repo",
|
|
1423
|
+
"register",
|
|
1424
|
+
"--project-id",
|
|
1425
|
+
projectId,
|
|
1426
|
+
"--path",
|
|
1427
|
+
repoPath,
|
|
1428
|
+
"--api-url",
|
|
1429
|
+
this.apiUrl,
|
|
1430
|
+
"--analyze"
|
|
1431
|
+
];
|
|
1432
|
+
if (isAlreadyAttached) {
|
|
1433
|
+
onProgress?.("Repository already attached to this project. Syncing metadata only (no analysis)...");
|
|
1434
|
+
} else {
|
|
1435
|
+
onProgress?.("Repository not attached yet. Registering and running analysis...");
|
|
1436
|
+
}
|
|
1437
|
+
const runResult = await this.cliRunner.runCliCommand(args.slice(1), {
|
|
1438
|
+
cwd: repoPath,
|
|
1439
|
+
organizationId,
|
|
1440
|
+
onProgress,
|
|
1441
|
+
requestInteraction,
|
|
1442
|
+
onActivity
|
|
1443
|
+
});
|
|
1444
|
+
lastOutputLines = runResult.outputLines;
|
|
1445
|
+
const exitCode = runResult.exitCode;
|
|
1446
|
+
if (exitCode !== 0) {
|
|
1447
|
+
appendGlobalErrorLog({
|
|
1448
|
+
source: "wizard.registerCurrentRepository",
|
|
1449
|
+
message: `Repository registration/analysis failed (exit ${exitCode}).`,
|
|
1450
|
+
details: {
|
|
1451
|
+
apiUrl: this.apiUrl,
|
|
1452
|
+
organizationId,
|
|
1453
|
+
projectId,
|
|
1454
|
+
repoPath,
|
|
1455
|
+
command: process.execPath,
|
|
1456
|
+
args,
|
|
1457
|
+
lastOutputLines: lastOutputLines.slice(-80)
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
throw new Error(`Repository registration/analysis failed (exit ${exitCode}).`);
|
|
1461
|
+
}
|
|
1462
|
+
if (runAnalyze) {
|
|
1463
|
+
persistProjectContext(repoPath, {
|
|
1464
|
+
projectId,
|
|
1465
|
+
organizationId
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
persistProjectContextWithoutRepoPath(repoPath, {
|
|
1469
|
+
projectId,
|
|
1470
|
+
organizationId
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
success: true,
|
|
1475
|
+
analyzed: runAnalyze
|
|
1476
|
+
};
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1479
|
+
appendGlobalErrorLog({
|
|
1480
|
+
source: "wizard.registerCurrentRepository",
|
|
1481
|
+
message,
|
|
1482
|
+
details: {
|
|
1483
|
+
apiUrl: this.apiUrl,
|
|
1484
|
+
organizationId,
|
|
1485
|
+
projectId,
|
|
1486
|
+
repoPath,
|
|
1487
|
+
command: process.execPath,
|
|
1488
|
+
args,
|
|
1489
|
+
lastOutputLines: lastOutputLines.slice(-80)
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
}, "attachOrSyncCurrentRepository");
|
|
1495
|
+
async syncRepositoryViaCli(organizationId, projectId, repoPathInput, options) {
|
|
1496
|
+
const repoPath = path4.resolve(repoPathInput || process.cwd());
|
|
1497
|
+
if (!this.cliRunner.resolveCliEntry()) {
|
|
1498
|
+
return {
|
|
1499
|
+
success: false,
|
|
1500
|
+
output: "Could not resolve CLI entrypoint.",
|
|
1501
|
+
exitCode: 1
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1505
|
+
if (!token) {
|
|
1506
|
+
return {
|
|
1507
|
+
success: false,
|
|
1508
|
+
output: "No valid credentials. Run `spekn auth login` first.",
|
|
1509
|
+
exitCode: 1
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
const args = [
|
|
1513
|
+
"repo",
|
|
1514
|
+
"sync",
|
|
1515
|
+
"--project-id",
|
|
1516
|
+
projectId,
|
|
1517
|
+
"--path",
|
|
1518
|
+
repoPath,
|
|
1519
|
+
"--api-url",
|
|
1520
|
+
this.apiUrl
|
|
1521
|
+
];
|
|
1522
|
+
if (options?.analyze === false) {
|
|
1523
|
+
args.push("--no-analyze");
|
|
1524
|
+
}
|
|
1525
|
+
if (options?.importToProject) {
|
|
1526
|
+
args.push("--import-to-project");
|
|
1527
|
+
}
|
|
1528
|
+
if (typeof options?.maxFiles === "number" && Number.isFinite(options.maxFiles)) {
|
|
1529
|
+
args.push("--max-files", String(Math.max(1, Math.floor(options.maxFiles))));
|
|
1530
|
+
}
|
|
1531
|
+
if (options?.analysisEngine) {
|
|
1532
|
+
args.push("--analysis-engine", options.analysisEngine);
|
|
1533
|
+
}
|
|
1534
|
+
if (options?.agent && options.agent.trim().length > 0) {
|
|
1535
|
+
args.push("--agent", options.agent.trim());
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const runResult = await this.cliRunner.runCliCommand(args, {
|
|
1539
|
+
cwd: repoPath,
|
|
1540
|
+
organizationId,
|
|
1541
|
+
onProgress: options?.onProgress,
|
|
1542
|
+
requestInteraction: options?.requestInteraction,
|
|
1543
|
+
onActivity: options?.onActivity,
|
|
1544
|
+
signal: options?.signal
|
|
1545
|
+
});
|
|
1546
|
+
return {
|
|
1547
|
+
success: runResult.success,
|
|
1548
|
+
output: runResult.output,
|
|
1549
|
+
exitCode: runResult.exitCode
|
|
1550
|
+
};
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1553
|
+
return {
|
|
1554
|
+
success: false,
|
|
1555
|
+
output: message,
|
|
1556
|
+
exitCode: 1
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async registerRepositoryViaCli(organizationId, projectId, repoPathInput, options) {
|
|
1561
|
+
const repoPath = path4.resolve(repoPathInput || process.cwd());
|
|
1562
|
+
if (!this.cliRunner.resolveCliEntry()) {
|
|
1563
|
+
return {
|
|
1564
|
+
success: false,
|
|
1565
|
+
output: "Could not resolve CLI entrypoint.",
|
|
1566
|
+
exitCode: 1
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const token = await this.credentialsStore.getValidToken();
|
|
1570
|
+
if (!token) {
|
|
1571
|
+
return {
|
|
1572
|
+
success: false,
|
|
1573
|
+
output: "No valid credentials. Run `spekn auth login` first.",
|
|
1574
|
+
exitCode: 1
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
const args = [
|
|
1578
|
+
"repo",
|
|
1579
|
+
"register",
|
|
1580
|
+
"--project-id",
|
|
1581
|
+
projectId,
|
|
1582
|
+
"--path",
|
|
1583
|
+
repoPath,
|
|
1584
|
+
"--api-url",
|
|
1585
|
+
this.apiUrl
|
|
1586
|
+
];
|
|
1587
|
+
if (options?.primary) {
|
|
1588
|
+
args.push("--primary");
|
|
1589
|
+
}
|
|
1590
|
+
if (options?.analyze === false) {
|
|
1591
|
+
args.push("--no-analyze");
|
|
1592
|
+
} else {
|
|
1593
|
+
args.push("--analyze");
|
|
1594
|
+
}
|
|
1595
|
+
if (options?.agent && options.agent.trim().length > 0) {
|
|
1596
|
+
args.push("--agent", options.agent.trim());
|
|
1597
|
+
}
|
|
1598
|
+
try {
|
|
1599
|
+
const runResult = await this.cliRunner.runCliCommand(args, {
|
|
1600
|
+
cwd: repoPath,
|
|
1601
|
+
organizationId,
|
|
1602
|
+
onProgress: options?.onProgress,
|
|
1603
|
+
requestInteraction: options?.requestInteraction,
|
|
1604
|
+
onActivity: options?.onActivity,
|
|
1605
|
+
signal: options?.signal
|
|
1606
|
+
});
|
|
1607
|
+
if (runResult.success) {
|
|
1608
|
+
persistProjectContext(repoPath, {
|
|
1609
|
+
projectId,
|
|
1610
|
+
organizationId
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
success: runResult.success,
|
|
1615
|
+
output: runResult.output,
|
|
1616
|
+
exitCode: runResult.exitCode
|
|
1617
|
+
};
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1620
|
+
return {
|
|
1621
|
+
success: false,
|
|
1622
|
+
output: message,
|
|
1623
|
+
exitCode: 1
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
async detachContextViaCli(_projectId, repoPath = process.cwd()) {
|
|
1628
|
+
const result = await this.cliRunner.runCliCommand([
|
|
1629
|
+
"repo",
|
|
1630
|
+
"detach",
|
|
1631
|
+
"--path",
|
|
1632
|
+
repoPath
|
|
1633
|
+
], {
|
|
1634
|
+
cwd: repoPath,
|
|
1635
|
+
includeAuthToken: false
|
|
1636
|
+
});
|
|
1637
|
+
return {
|
|
1638
|
+
success: result.success,
|
|
1639
|
+
output: result.output || result.error || "Unknown detach error"
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
execGit(repoPath, args) {
|
|
1643
|
+
try {
|
|
1644
|
+
return execFileSync("git", [
|
|
1645
|
+
"-C",
|
|
1646
|
+
repoPath,
|
|
1647
|
+
...args
|
|
1648
|
+
], {
|
|
1649
|
+
encoding: "utf-8",
|
|
1650
|
+
stdio: [
|
|
1651
|
+
"pipe",
|
|
1652
|
+
"pipe",
|
|
1653
|
+
"pipe"
|
|
1654
|
+
]
|
|
1655
|
+
}).trim();
|
|
1656
|
+
} catch {
|
|
1657
|
+
throw new Error("Could not read git metadata. Run TUI from a git repository with an 'origin' remote.");
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// src/services/spec-service.ts
|
|
1663
|
+
var SpecService = class {
|
|
1664
|
+
static {
|
|
1665
|
+
__name(this, "SpecService");
|
|
1666
|
+
}
|
|
1667
|
+
async loadSpecs(client2, projectId) {
|
|
1668
|
+
const specs = await client2.specification.list.query({
|
|
1669
|
+
projectId,
|
|
1670
|
+
limit: 50,
|
|
1671
|
+
offset: 0
|
|
1672
|
+
});
|
|
1673
|
+
return (Array.isArray(specs) ? specs : []).map((spec) => {
|
|
1674
|
+
const frontmatter = spec.frontmatter ?? {};
|
|
1675
|
+
const hints = frontmatter.hints ?? {};
|
|
1676
|
+
const aiContext = frontmatter.aiContext ?? {};
|
|
1677
|
+
const acp = frontmatter.acp ?? {};
|
|
1678
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((tag) => typeof tag === "string") : [];
|
|
1679
|
+
const aiFocusAreas = Array.isArray(aiContext.focusAreas) ? aiContext.focusAreas.filter((area) => typeof area === "string") : [];
|
|
1680
|
+
const acpAllowedAgents = Array.isArray(acp.allowedAgents) ? acp.allowedAgents.filter((agent) => typeof agent === "string") : [];
|
|
1681
|
+
const countArray = /* @__PURE__ */ __name((value) => Array.isArray(value) ? value.length : 0, "countArray");
|
|
1682
|
+
return {
|
|
1683
|
+
id: spec.id,
|
|
1684
|
+
specRef: typeof frontmatter.specRef === "string" ? frontmatter.specRef : typeof spec.specRef === "string" ? spec.specRef : void 0,
|
|
1685
|
+
frontmatter,
|
|
1686
|
+
title: spec.title,
|
|
1687
|
+
status: spec.status,
|
|
1688
|
+
generationNumber: typeof spec.generationNumber === "number" ? spec.generationNumber : 1,
|
|
1689
|
+
generationStatus: typeof spec.generationStatus === "string" ? spec.generationStatus : spec.status,
|
|
1690
|
+
updatedAt: spec.updatedAt,
|
|
1691
|
+
type: typeof frontmatter.type === "string" ? frontmatter.type : void 0,
|
|
1692
|
+
content: typeof spec.content === "string" ? spec.content : void 0,
|
|
1693
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
1694
|
+
author: typeof frontmatter.author === "string" ? frontmatter.author : void 0,
|
|
1695
|
+
hintCounts: {
|
|
1696
|
+
constraints: countArray(hints.constraints),
|
|
1697
|
+
requirements: countArray(hints.requirements),
|
|
1698
|
+
technical: countArray(hints.technical),
|
|
1699
|
+
guidance: countArray(hints.guidance)
|
|
1700
|
+
},
|
|
1701
|
+
relationCounts: {
|
|
1702
|
+
dependsOn: countArray(frontmatter.dependsOn),
|
|
1703
|
+
conflictsWith: countArray(frontmatter.conflictsWith),
|
|
1704
|
+
compatibleWith: countArray(frontmatter.compatibleWith)
|
|
1705
|
+
},
|
|
1706
|
+
acpPolicyMode: typeof acp.policyMode === "string" ? acp.policyMode : void 0,
|
|
1707
|
+
acpAllowedAgents: acpAllowedAgents.length > 0 ? acpAllowedAgents : void 0,
|
|
1708
|
+
aiTokenBudget: typeof aiContext.tokenBudget === "number" ? aiContext.tokenBudget : void 0,
|
|
1709
|
+
aiFocusAreas: aiFocusAreas.length > 0 ? aiFocusAreas : void 0
|
|
1710
|
+
};
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
async updateSpecificationContent(client2, projectId, specificationId, content) {
|
|
1714
|
+
await client2.specification.update.mutate({
|
|
1715
|
+
projectId,
|
|
1716
|
+
id: specificationId,
|
|
1717
|
+
data: {
|
|
1718
|
+
content,
|
|
1719
|
+
changeType: "patch",
|
|
1720
|
+
changeDescription: "Edited from TUI editor"
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
async updateSpecificationStatus(client2, projectId, specificationId, status) {
|
|
1725
|
+
await client2.specification.update.mutate({
|
|
1726
|
+
projectId,
|
|
1727
|
+
id: specificationId,
|
|
1728
|
+
data: {
|
|
1729
|
+
status,
|
|
1730
|
+
changeType: "metadata",
|
|
1731
|
+
changeDescription: `Status changed to ${status} from TUI`
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
async deleteSpecification(client2, projectId, specificationId, mode = "archive") {
|
|
1736
|
+
const deleteMutation = client2?.specification?.delete?.mutate;
|
|
1737
|
+
if (typeof deleteMutation !== "function") {
|
|
1738
|
+
throw new Error("Specification delete route is unavailable.");
|
|
1739
|
+
}
|
|
1740
|
+
await deleteMutation({
|
|
1741
|
+
projectId,
|
|
1742
|
+
id: specificationId,
|
|
1743
|
+
mode
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
async refineSpecificationWithAi(client2, input) {
|
|
1747
|
+
const refineMutation = client2?.specification?.refine?.mutate;
|
|
1748
|
+
if (typeof refineMutation !== "function") {
|
|
1749
|
+
throw new Error("Specification refine route is unavailable.");
|
|
1750
|
+
}
|
|
1751
|
+
const result = await refineMutation({
|
|
1752
|
+
projectId: input.projectId,
|
|
1753
|
+
specificationContent: input.specContent,
|
|
1754
|
+
userMessage: input.userMessage,
|
|
1755
|
+
agentType: input.agentType
|
|
1756
|
+
});
|
|
1757
|
+
if (typeof result?.content !== "string") {
|
|
1758
|
+
throw new Error("AI refinement response was missing content.");
|
|
1759
|
+
}
|
|
1760
|
+
return result.content;
|
|
1761
|
+
}
|
|
1762
|
+
async createSpecification(client2, projectId, title, content, status = "draft") {
|
|
1763
|
+
const createMutation = client2?.specification?.create?.mutate;
|
|
1764
|
+
if (typeof createMutation !== "function") {
|
|
1765
|
+
throw new Error("Specification create route is unavailable.");
|
|
1766
|
+
}
|
|
1767
|
+
const result = await createMutation({
|
|
1768
|
+
projectId,
|
|
1769
|
+
title,
|
|
1770
|
+
content,
|
|
1771
|
+
status
|
|
1772
|
+
});
|
|
1773
|
+
return {
|
|
1774
|
+
id: result.id
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
async listGenerations(client2, projectId, specId) {
|
|
1778
|
+
const query = client2?.specification?.listGenerations?.query;
|
|
1779
|
+
if (typeof query !== "function") {
|
|
1780
|
+
throw new Error("Specification listGenerations route is unavailable.");
|
|
1781
|
+
}
|
|
1782
|
+
const result = await query({
|
|
1783
|
+
projectId,
|
|
1784
|
+
specificationId: specId
|
|
1785
|
+
});
|
|
1786
|
+
return Array.isArray(result) ? result : [];
|
|
1787
|
+
}
|
|
1788
|
+
async startNewGeneration(client2, projectId, specId, fromGenerationId) {
|
|
1789
|
+
const mutation = client2?.specification?.startNewGeneration?.mutate;
|
|
1790
|
+
if (typeof mutation !== "function") {
|
|
1791
|
+
throw new Error("Specification startNewGeneration route is unavailable.");
|
|
1792
|
+
}
|
|
1793
|
+
return await mutation({
|
|
1794
|
+
projectId,
|
|
1795
|
+
specificationId: specId,
|
|
1796
|
+
fromGenerationId
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
async lockGeneration(client2, projectId, specId) {
|
|
1800
|
+
const mutation = client2?.specification?.lock?.mutate;
|
|
1801
|
+
if (typeof mutation !== "function") {
|
|
1802
|
+
throw new Error("Specification lock route is unavailable.");
|
|
1803
|
+
}
|
|
1804
|
+
await mutation({
|
|
1805
|
+
projectId,
|
|
1806
|
+
specificationId: specId
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
async unlockGeneration(client2, projectId, specId) {
|
|
1810
|
+
const mutation = client2?.specification?.unlock?.mutate;
|
|
1811
|
+
if (typeof mutation !== "function") {
|
|
1812
|
+
throw new Error("Specification unlock route is unavailable.");
|
|
1813
|
+
}
|
|
1814
|
+
await mutation({
|
|
1815
|
+
projectId,
|
|
1816
|
+
specificationId: specId
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
async deleteGeneration(client2, projectId, generationId) {
|
|
1820
|
+
const mutation = client2?.specification?.deleteGeneration?.mutate;
|
|
1821
|
+
if (typeof mutation !== "function") {
|
|
1822
|
+
throw new Error("Specification deleteGeneration route is unavailable.");
|
|
1823
|
+
}
|
|
1824
|
+
await mutation({
|
|
1825
|
+
projectId,
|
|
1826
|
+
generationId
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// src/services/context-service.ts
|
|
1832
|
+
var TuiContextService = class {
|
|
1833
|
+
static {
|
|
1834
|
+
__name(this, "TuiContextService");
|
|
1835
|
+
}
|
|
1836
|
+
apiUrl;
|
|
1837
|
+
authService;
|
|
1838
|
+
bootstrapService;
|
|
1839
|
+
bridgeService;
|
|
1840
|
+
decisionService;
|
|
1841
|
+
exportService;
|
|
1842
|
+
organizationService;
|
|
1843
|
+
repoService;
|
|
1844
|
+
specService;
|
|
1845
|
+
constructor(apiUrl) {
|
|
1846
|
+
this.apiUrl = apiUrl;
|
|
1847
|
+
this.authService = new AuthService(apiUrl);
|
|
1848
|
+
this.bootstrapService = new BootstrapService(apiUrl);
|
|
1849
|
+
this.bridgeService = new BridgeService(apiUrl);
|
|
1850
|
+
this.decisionService = new DecisionService();
|
|
1851
|
+
this.exportService = new ExportService(apiUrl);
|
|
1852
|
+
this.organizationService = new OrganizationService(apiUrl);
|
|
1853
|
+
this.repoService = new RepoService(apiUrl);
|
|
1854
|
+
this.specService = new SpecService();
|
|
1855
|
+
}
|
|
1856
|
+
// ── Bootstrap ──────────────────────────────────────────────────────
|
|
1857
|
+
async bootstrap(projectIdArg) {
|
|
1858
|
+
return this.bootstrapService.bootstrap(projectIdArg);
|
|
1859
|
+
}
|
|
1860
|
+
hasDeclaredProjectContext(projectIdArg) {
|
|
1861
|
+
return this.bootstrapService.hasDeclaredProjectContext(projectIdArg);
|
|
1862
|
+
}
|
|
1863
|
+
hasLocalProjectContext(repoPath) {
|
|
1864
|
+
return this.bootstrapService.hasLocalProjectContext(repoPath);
|
|
1865
|
+
}
|
|
1866
|
+
async loadWorkflowSummary(client2, projectId) {
|
|
1867
|
+
return this.bootstrapService.loadWorkflowSummary(client2, projectId);
|
|
1868
|
+
}
|
|
1869
|
+
persistContext(organizationId, projectId) {
|
|
1870
|
+
this.bootstrapService.persistContext(organizationId, projectId);
|
|
1871
|
+
}
|
|
1872
|
+
// ── Auth ───────────────────────────────────────────────────────────
|
|
1873
|
+
async checkAuthentication() {
|
|
1874
|
+
return this.authService.checkAuthentication();
|
|
1875
|
+
}
|
|
1876
|
+
async authenticateViaCli(onProgress) {
|
|
1877
|
+
return this.authService.authenticateViaCli(onProgress);
|
|
1878
|
+
}
|
|
1879
|
+
extractUserEmail(token) {
|
|
1880
|
+
return this.authService.extractUserEmail(token);
|
|
1881
|
+
}
|
|
1882
|
+
extractTokenExpiry(token) {
|
|
1883
|
+
return this.authService.extractTokenExpiry(token);
|
|
1884
|
+
}
|
|
1885
|
+
// ── Organization & Project ─────────────────────────────────────────
|
|
1886
|
+
async listOrganizations() {
|
|
1887
|
+
return this.organizationService.listOrganizations();
|
|
1888
|
+
}
|
|
1889
|
+
async createOrganization(input) {
|
|
1890
|
+
return this.organizationService.createOrganization(input);
|
|
1891
|
+
}
|
|
1892
|
+
async listProjects(organizationId) {
|
|
1893
|
+
return this.organizationService.listProjects(organizationId);
|
|
1894
|
+
}
|
|
1895
|
+
async createProject(organizationId, name) {
|
|
1896
|
+
return this.organizationService.createProject(organizationId, name);
|
|
1897
|
+
}
|
|
1898
|
+
canCreateProject(org) {
|
|
1899
|
+
return this.organizationService.canCreateProject(org);
|
|
1900
|
+
}
|
|
1901
|
+
// ── Specs ──────────────────────────────────────────────────────────
|
|
1902
|
+
async loadSpecs(client2, projectId) {
|
|
1903
|
+
return this.specService.loadSpecs(client2, projectId);
|
|
1904
|
+
}
|
|
1905
|
+
async updateSpecificationContent(client2, projectId, specificationId, content) {
|
|
1906
|
+
return this.specService.updateSpecificationContent(client2, projectId, specificationId, content);
|
|
1907
|
+
}
|
|
1908
|
+
async updateSpecificationStatus(client2, projectId, specificationId, status) {
|
|
1909
|
+
return this.specService.updateSpecificationStatus(client2, projectId, specificationId, status);
|
|
1910
|
+
}
|
|
1911
|
+
async deleteSpecification(client2, projectId, specificationId, mode = "archive") {
|
|
1912
|
+
return this.specService.deleteSpecification(client2, projectId, specificationId, mode);
|
|
1913
|
+
}
|
|
1914
|
+
async refineSpecificationWithAi(client2, input) {
|
|
1915
|
+
return this.specService.refineSpecificationWithAi(client2, input);
|
|
1916
|
+
}
|
|
1917
|
+
async createSpecification(client2, projectId, title, content, status = "draft") {
|
|
1918
|
+
return this.specService.createSpecification(client2, projectId, title, content, status);
|
|
1919
|
+
}
|
|
1920
|
+
async listGenerations(client2, projectId, specId) {
|
|
1921
|
+
return this.specService.listGenerations(client2, projectId, specId);
|
|
1922
|
+
}
|
|
1923
|
+
async startNewGeneration(client2, projectId, specId, fromGenerationId) {
|
|
1924
|
+
return this.specService.startNewGeneration(client2, projectId, specId, fromGenerationId);
|
|
1925
|
+
}
|
|
1926
|
+
async lockGeneration(client2, projectId, specId) {
|
|
1927
|
+
return this.specService.lockGeneration(client2, projectId, specId);
|
|
1928
|
+
}
|
|
1929
|
+
async unlockGeneration(client2, projectId, specId) {
|
|
1930
|
+
return this.specService.unlockGeneration(client2, projectId, specId);
|
|
1931
|
+
}
|
|
1932
|
+
async deleteGeneration(client2, projectId, generationId) {
|
|
1933
|
+
return this.specService.deleteGeneration(client2, projectId, generationId);
|
|
1934
|
+
}
|
|
1935
|
+
// ── Decisions ──────────────────────────────────────────────────────
|
|
1936
|
+
async loadDecisions(client2, projectId) {
|
|
1937
|
+
return this.decisionService.loadDecisions(client2, projectId);
|
|
1938
|
+
}
|
|
1939
|
+
async resolveDecision(client2, projectId, decisionId, status, reason, existingRationale) {
|
|
1940
|
+
return this.decisionService.resolveDecision(client2, projectId, decisionId, status, reason, existingRationale);
|
|
1941
|
+
}
|
|
1942
|
+
async deleteDecision(client2, projectId, decisionId, mode = "archive") {
|
|
1943
|
+
return this.decisionService.deleteDecision(client2, projectId, decisionId, mode);
|
|
1944
|
+
}
|
|
1945
|
+
// ── Export ─────────────────────────────────────────────────────────
|
|
1946
|
+
async previewExport(client2, projectId, format) {
|
|
1947
|
+
return this.exportService.previewExport(client2, projectId, format);
|
|
1948
|
+
}
|
|
1949
|
+
async generateExport(client2, projectId, format) {
|
|
1950
|
+
return this.exportService.generateExport(client2, projectId, format);
|
|
1951
|
+
}
|
|
1952
|
+
async discoverExportCapabilities(projectId, organizationId) {
|
|
1953
|
+
return this.exportService.discoverExportCapabilities(projectId, organizationId);
|
|
1954
|
+
}
|
|
1955
|
+
async exportStatus(projectId, format, options) {
|
|
1956
|
+
return this.exportService.exportStatus(projectId, format, options);
|
|
1957
|
+
}
|
|
1958
|
+
async deliverExport(projectId, format, delivery, options) {
|
|
1959
|
+
return this.exportService.deliverExport(projectId, format, delivery, options);
|
|
1960
|
+
}
|
|
1961
|
+
// ── Bridge ─────────────────────────────────────────────────────────
|
|
1962
|
+
async loadBridgeSummary(client2) {
|
|
1963
|
+
return this.bridgeService.loadBridgeSummary(client2);
|
|
1964
|
+
}
|
|
1965
|
+
async loadLocalBridgeSummary() {
|
|
1966
|
+
return this.bridgeService.loadLocalBridgeSummary();
|
|
1967
|
+
}
|
|
1968
|
+
startLocalBridgeDetached() {
|
|
1969
|
+
this.bridgeService.startLocalBridgeDetached();
|
|
1970
|
+
}
|
|
1971
|
+
async stopLocalBridge(configPort) {
|
|
1972
|
+
return this.bridgeService.stopLocalBridge(configPort);
|
|
1973
|
+
}
|
|
1974
|
+
async loadBridgeLogs(port, since) {
|
|
1975
|
+
return this.bridgeService.loadBridgeLogs(port, since);
|
|
1976
|
+
}
|
|
1977
|
+
// ── Repository ─────────────────────────────────────────────────────
|
|
1978
|
+
attachOrSyncCurrentRepository = /* @__PURE__ */ __name(async (organizationId, projectId, onProgress, _agentType, requestInteraction, onActivity) => {
|
|
1979
|
+
return this.repoService.attachOrSyncCurrentRepository(organizationId, projectId, onProgress, _agentType, requestInteraction, onActivity);
|
|
1980
|
+
}, "attachOrSyncCurrentRepository");
|
|
1981
|
+
async syncRepositoryViaCli(organizationId, projectId, repoPathInput, options) {
|
|
1982
|
+
return this.repoService.syncRepositoryViaCli(organizationId, projectId, repoPathInput, options);
|
|
1983
|
+
}
|
|
1984
|
+
async registerRepositoryViaCli(organizationId, projectId, repoPathInput, options) {
|
|
1985
|
+
return this.repoService.registerRepositoryViaCli(organizationId, projectId, repoPathInput, options);
|
|
1986
|
+
}
|
|
1987
|
+
async detachContextViaCli(_projectId, repoPath = process.cwd()) {
|
|
1988
|
+
return this.repoService.detachContextViaCli(_projectId, repoPath);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
// src/store/service-bridge.ts
|
|
1993
|
+
var service = null;
|
|
1994
|
+
var client = null;
|
|
1995
|
+
function initServiceBridge(apiUrl) {
|
|
1996
|
+
if (!service) {
|
|
1997
|
+
service = new TuiContextService(apiUrl);
|
|
1998
|
+
}
|
|
1999
|
+
return service;
|
|
2000
|
+
}
|
|
2001
|
+
__name(initServiceBridge, "initServiceBridge");
|
|
2002
|
+
function getService() {
|
|
2003
|
+
if (!service) {
|
|
2004
|
+
throw new Error("Service bridge not initialized. Call initServiceBridge() first.");
|
|
2005
|
+
}
|
|
2006
|
+
return service;
|
|
2007
|
+
}
|
|
2008
|
+
__name(getService, "getService");
|
|
2009
|
+
function getClient() {
|
|
2010
|
+
return client;
|
|
2011
|
+
}
|
|
2012
|
+
__name(getClient, "getClient");
|
|
2013
|
+
function setClient(c) {
|
|
2014
|
+
client = c;
|
|
2015
|
+
}
|
|
2016
|
+
__name(setClient, "setClient");
|
|
2017
|
+
|
|
2018
|
+
export {
|
|
2019
|
+
__name,
|
|
2020
|
+
OrganizationPlan,
|
|
2021
|
+
WorkflowPhase,
|
|
2022
|
+
persistProjectContext,
|
|
2023
|
+
appendGlobalStructuredLog,
|
|
2024
|
+
loadWebMcpChannels,
|
|
2025
|
+
initServiceBridge,
|
|
2026
|
+
getService,
|
|
2027
|
+
getClient,
|
|
2028
|
+
setClient
|
|
2029
|
+
};
|