@spekn/cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +58 -0
  2. package/dist/main.js +3707 -611
  3. package/dist/tui/index.mjs +2 -2
  4. package/package.json +29 -12
  5. package/dist/__tests__/export-cli.test.d.ts +0 -1
  6. package/dist/__tests__/export-cli.test.js +0 -70
  7. package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
  8. package/dist/__tests__/tui-args-policy.test.js +0 -50
  9. package/dist/acp-S2MHZOAD.mjs +0 -23
  10. package/dist/acp-UCCI44JY.mjs +0 -25
  11. package/dist/auth/credentials-store.d.ts +0 -2
  12. package/dist/auth/credentials-store.js +0 -5
  13. package/dist/auth/device-flow.d.ts +0 -36
  14. package/dist/auth/device-flow.js +0 -189
  15. package/dist/auth/jwt.d.ts +0 -1
  16. package/dist/auth/jwt.js +0 -6
  17. package/dist/auth/session.d.ts +0 -67
  18. package/dist/auth/session.js +0 -86
  19. package/dist/auth-login.d.ts +0 -34
  20. package/dist/auth-login.js +0 -202
  21. package/dist/auth-logout.d.ts +0 -25
  22. package/dist/auth-logout.js +0 -115
  23. package/dist/auth-status.d.ts +0 -24
  24. package/dist/auth-status.js +0 -109
  25. package/dist/backlog-generate.d.ts +0 -11
  26. package/dist/backlog-generate.js +0 -308
  27. package/dist/backlog-health.d.ts +0 -11
  28. package/dist/backlog-health.js +0 -287
  29. package/dist/bridge-login.d.ts +0 -40
  30. package/dist/bridge-login.js +0 -277
  31. package/dist/chunk-3PAYRI4G.mjs +0 -2428
  32. package/dist/chunk-M4CS3A25.mjs +0 -2426
  33. package/dist/commands/auth/login.d.ts +0 -30
  34. package/dist/commands/auth/login.js +0 -164
  35. package/dist/commands/auth/logout.d.ts +0 -25
  36. package/dist/commands/auth/logout.js +0 -115
  37. package/dist/commands/auth/status.d.ts +0 -24
  38. package/dist/commands/auth/status.js +0 -109
  39. package/dist/commands/backlog/generate.d.ts +0 -11
  40. package/dist/commands/backlog/generate.js +0 -308
  41. package/dist/commands/backlog/health.d.ts +0 -11
  42. package/dist/commands/backlog/health.js +0 -287
  43. package/dist/commands/bridge/login.d.ts +0 -36
  44. package/dist/commands/bridge/login.js +0 -258
  45. package/dist/commands/export.d.ts +0 -35
  46. package/dist/commands/export.js +0 -485
  47. package/dist/commands/marketplace-export.d.ts +0 -21
  48. package/dist/commands/marketplace-export.js +0 -214
  49. package/dist/commands/project-clean.d.ts +0 -1
  50. package/dist/commands/project-clean.js +0 -126
  51. package/dist/commands/repo/common.d.ts +0 -105
  52. package/dist/commands/repo/common.js +0 -775
  53. package/dist/commands/repo/detach.d.ts +0 -2
  54. package/dist/commands/repo/detach.js +0 -120
  55. package/dist/commands/repo/register.d.ts +0 -21
  56. package/dist/commands/repo/register.js +0 -175
  57. package/dist/commands/repo/sync.d.ts +0 -22
  58. package/dist/commands/repo/sync.js +0 -873
  59. package/dist/commands/skills-import-local.d.ts +0 -16
  60. package/dist/commands/skills-import-local.js +0 -352
  61. package/dist/commands/spec/drift-check.d.ts +0 -3
  62. package/dist/commands/spec/drift-check.js +0 -186
  63. package/dist/commands/spec/frontmatter.d.ts +0 -11
  64. package/dist/commands/spec/frontmatter.js +0 -219
  65. package/dist/commands/spec/lint.d.ts +0 -11
  66. package/dist/commands/spec/lint.js +0 -499
  67. package/dist/commands/spec/parse.d.ts +0 -11
  68. package/dist/commands/spec/parse.js +0 -162
  69. package/dist/export.d.ts +0 -35
  70. package/dist/export.js +0 -485
  71. package/dist/main.d.ts +0 -1
  72. package/dist/marketplace-export.d.ts +0 -21
  73. package/dist/marketplace-export.js +0 -214
  74. package/dist/project-clean.d.ts +0 -1
  75. package/dist/project-clean.js +0 -126
  76. package/dist/project-context.d.ts +0 -99
  77. package/dist/project-context.js +0 -376
  78. package/dist/repo-common.d.ts +0 -101
  79. package/dist/repo-common.js +0 -671
  80. package/dist/repo-detach.d.ts +0 -2
  81. package/dist/repo-detach.js +0 -102
  82. package/dist/repo-ingest.d.ts +0 -29
  83. package/dist/repo-ingest.js +0 -305
  84. package/dist/repo-register.d.ts +0 -21
  85. package/dist/repo-register.js +0 -175
  86. package/dist/repo-sync.d.ts +0 -16
  87. package/dist/repo-sync.js +0 -152
  88. package/dist/resources/prompt-loader.d.ts +0 -1
  89. package/dist/resources/prompt-loader.js +0 -62
  90. package/dist/skills-import-local.d.ts +0 -16
  91. package/dist/skills-import-local.js +0 -352
  92. package/dist/spec-drift-check.d.ts +0 -3
  93. package/dist/spec-drift-check.js +0 -186
  94. package/dist/spec-frontmatter.d.ts +0 -11
  95. package/dist/spec-frontmatter.js +0 -219
  96. package/dist/spec-lint.d.ts +0 -11
  97. package/dist/spec-lint.js +0 -499
  98. package/dist/spec-parse.d.ts +0 -11
  99. package/dist/spec-parse.js +0 -162
  100. package/dist/stubs/dotenv.d.ts +0 -5
  101. package/dist/stubs/dotenv.js +0 -6
  102. package/dist/stubs/typeorm.d.ts +0 -22
  103. package/dist/stubs/typeorm.js +0 -28
  104. package/dist/tui/app.d.ts +0 -7
  105. package/dist/tui/app.js +0 -122
  106. package/dist/tui/args.d.ts +0 -8
  107. package/dist/tui/args.js +0 -57
  108. package/dist/tui/capabilities/policy.d.ts +0 -7
  109. package/dist/tui/capabilities/policy.js +0 -64
  110. package/dist/tui/components/frame.d.ts +0 -8
  111. package/dist/tui/components/frame.js +0 -8
  112. package/dist/tui/components/status-bar.d.ts +0 -8
  113. package/dist/tui/components/status-bar.js +0 -8
  114. package/dist/tui/index.d.ts +0 -2
  115. package/dist/tui/index.js +0 -23
  116. package/dist/tui/keymap/use-global-keymap.d.ts +0 -19
  117. package/dist/tui/keymap/use-global-keymap.js +0 -82
  118. package/dist/tui/navigation/nav-items.d.ts +0 -3
  119. package/dist/tui/navigation/nav-items.js +0 -18
  120. package/dist/tui/screens/bridge.d.ts +0 -8
  121. package/dist/tui/screens/bridge.js +0 -19
  122. package/dist/tui/screens/decisions.d.ts +0 -5
  123. package/dist/tui/screens/decisions.js +0 -28
  124. package/dist/tui/screens/export.d.ts +0 -5
  125. package/dist/tui/screens/export.js +0 -16
  126. package/dist/tui/screens/home.d.ts +0 -5
  127. package/dist/tui/screens/home.js +0 -33
  128. package/dist/tui/screens/locked.d.ts +0 -5
  129. package/dist/tui/screens/locked.js +0 -9
  130. package/dist/tui/screens/specs.d.ts +0 -5
  131. package/dist/tui/screens/specs.js +0 -31
  132. package/dist/tui/services/client.d.ts +0 -1
  133. package/dist/tui/services/client.js +0 -18
  134. package/dist/tui/services/context-service.d.ts +0 -19
  135. package/dist/tui/services/context-service.js +0 -246
  136. package/dist/tui/shared-enums.d.ts +0 -16
  137. package/dist/tui/shared-enums.js +0 -19
  138. package/dist/tui/state/use-app-state.d.ts +0 -35
  139. package/dist/tui/state/use-app-state.js +0 -177
  140. package/dist/tui/types.d.ts +0 -77
  141. package/dist/tui/types.js +0 -2
  142. package/dist/tui-bundle.d.ts +0 -1
  143. package/dist/tui-bundle.js +0 -5
  144. package/dist/tui-entry.mjs +0 -1407
  145. package/dist/utils/cli-runtime.d.ts +0 -5
  146. package/dist/utils/cli-runtime.js +0 -22
  147. package/dist/utils/help-error.d.ts +0 -7
  148. package/dist/utils/help-error.js +0 -14
  149. package/dist/utils/interaction.d.ts +0 -19
  150. package/dist/utils/interaction.js +0 -93
  151. package/dist/utils/structured-log.d.ts +0 -7
  152. package/dist/utils/structured-log.js +0 -112
  153. package/dist/utils/trpc-url.d.ts +0 -4
  154. package/dist/utils/trpc-url.js +0 -15
@@ -1,1407 +0,0 @@
1
- #!/usr/bin/env node
2
- // Bundled TUI entry (ESM) — loaded via dynamic import() from CJS main.js
3
- var __defProp = Object.defineProperty;
4
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
-
6
- // src/tui/index.tsx
7
- import React10 from "react";
8
- import { render } from "ink";
9
-
10
- // src/tui/args.ts
11
- var VIEW_ALIASES = {
12
- home: "home",
13
- specs: "specs",
14
- export: "export",
15
- decisions: "decisions",
16
- bridge: "bridge"
17
- };
18
- function parseTuiArgs(args) {
19
- let projectId;
20
- let apiUrl = process.env.SPEKN_API_URL ?? "http://localhost:3000";
21
- let initialView = "home";
22
- let noColor = false;
23
- for (let i = 0; i < args.length; i++) {
24
- const arg = args[i];
25
- if ((arg === "--project-id" || arg === "--project") && args[i + 1]) {
26
- projectId = args[++i];
27
- continue;
28
- }
29
- if (arg.startsWith("--project-id=")) {
30
- projectId = arg.slice("--project-id=".length);
31
- continue;
32
- }
33
- if (arg === "--api-url" && args[i + 1]) {
34
- apiUrl = args[++i];
35
- continue;
36
- }
37
- if (arg.startsWith("--api-url=")) {
38
- apiUrl = arg.slice("--api-url=".length);
39
- continue;
40
- }
41
- if (arg === "--view" && args[i + 1]) {
42
- const candidate = VIEW_ALIASES[String(args[++i]).toLowerCase()];
43
- if (candidate) initialView = candidate;
44
- continue;
45
- }
46
- if (arg.startsWith("--view=")) {
47
- const candidate = VIEW_ALIASES[arg.slice("--view=".length).toLowerCase()];
48
- if (candidate) initialView = candidate;
49
- continue;
50
- }
51
- if (arg === "--no-color") {
52
- noColor = true;
53
- continue;
54
- }
55
- }
56
- return {
57
- projectId,
58
- apiUrl,
59
- initialView,
60
- noColor
61
- };
62
- }
63
- __name(parseTuiArgs, "parseTuiArgs");
64
-
65
- // src/tui/app.tsx
66
- import React9 from "react";
67
- import { Box as Box9, Text as Text8 } from "ink";
68
- import { Alert as Alert2, TextInput, ThemeProvider, extendTheme, defaultTheme } from "@inkjs/ui";
69
-
70
- // src/tui/state/use-app-state.tsx
71
- import { useCallback, useEffect, useMemo, useState } from "react";
72
-
73
- // src/tui/shared-enums.ts
74
- var OrganizationPlan = {
75
- FREE: "free",
76
- PRO: "pro",
77
- TEAM: "team",
78
- ENTERPRISE: "enterprise"
79
- };
80
- var WorkflowPhase = {
81
- SPECIFY: "specify",
82
- CLARIFY: "clarify",
83
- PLAN: "plan",
84
- IMPLEMENT: "implement",
85
- VERIFY: "verify",
86
- COMPLETE: "complete"
87
- };
88
-
89
- // src/tui/capabilities/policy.ts
90
- var TIER_ORDER = {
91
- [OrganizationPlan.FREE]: 0,
92
- [OrganizationPlan.PRO]: 1,
93
- [OrganizationPlan.TEAM]: 2,
94
- [OrganizationPlan.ENTERPRISE]: 3
95
- };
96
- var NAV_DEFINITIONS = [
97
- {
98
- id: "home",
99
- label: "Home",
100
- description: "Next actions and workflow pulse",
101
- requiredPlan: OrganizationPlan.FREE
102
- },
103
- {
104
- id: "specs",
105
- label: "Specs",
106
- description: "Manage governed specifications",
107
- requiredPlan: OrganizationPlan.FREE
108
- },
109
- {
110
- id: "export",
111
- label: "Export",
112
- description: "Generate CLAUDE.md / .cursorrules",
113
- requiredPlan: OrganizationPlan.FREE
114
- },
115
- {
116
- id: "decisions",
117
- label: "Decision Log",
118
- description: "Review decisions and rationale",
119
- requiredPlan: OrganizationPlan.FREE
120
- },
121
- {
122
- id: "bridge",
123
- label: "Local Bridge",
124
- description: "Bridge status and controls",
125
- requiredPlan: OrganizationPlan.PRO
126
- },
127
- {
128
- id: "active-runs",
129
- label: "Active Runs",
130
- description: "Realtime orchestration dashboard",
131
- requiredPlan: OrganizationPlan.TEAM
132
- },
133
- {
134
- id: "phase-gates",
135
- label: "Phase Gates",
136
- description: "Approve and unblock workflow phases",
137
- requiredPlan: OrganizationPlan.TEAM
138
- },
139
- {
140
- id: "skills-marketplace",
141
- label: "Skills Marketplace",
142
- description: "Manage shared managed skills",
143
- requiredPlan: OrganizationPlan.TEAM
144
- },
145
- {
146
- id: "org-governance",
147
- label: "Org Governance",
148
- description: "Compliance, policy, deployment gates",
149
- requiredPlan: OrganizationPlan.ENTERPRISE
150
- }
151
- ];
152
- var GATE_DISABLED_PHASES = [
153
- WorkflowPhase.SPECIFY,
154
- WorkflowPhase.CLARIFY
155
- ];
156
- function meetsMinimumTier(current, required) {
157
- return TIER_ORDER[current] >= TIER_ORDER[required];
158
- }
159
- __name(meetsMinimumTier, "meetsMinimumTier");
160
- function resolveNavPolicy(ctx) {
161
- return NAV_DEFINITIONS.map((item) => {
162
- if (!meetsMinimumTier(ctx.plan, item.requiredPlan)) {
163
- return {
164
- ...item,
165
- state: "locked",
166
- reason: `Requires ${item.requiredPlan.toUpperCase()} tier`
167
- };
168
- }
169
- if (item.id === "phase-gates" && ctx.role === "viewer") {
170
- return {
171
- ...item,
172
- state: "disabled",
173
- reason: "Viewer role cannot approve gates"
174
- };
175
- }
176
- if (item.id === "phase-gates" && ctx.workflowPhase && GATE_DISABLED_PHASES.includes(ctx.workflowPhase)) {
177
- return {
178
- ...item,
179
- state: "disabled",
180
- reason: `Gate approvals are unavailable in ${ctx.workflowPhase} phase`
181
- };
182
- }
183
- return {
184
- ...item,
185
- state: "enabled"
186
- };
187
- });
188
- }
189
- __name(resolveNavPolicy, "resolveNavPolicy");
190
-
191
- // src/tui/services/context-service.ts
192
- import { spawn } from "child_process";
193
- import { BridgeConfigStore } from "@spekn/bridge";
194
-
195
- // src/auth/credentials-store.ts
196
- import * as fs from "fs";
197
- import * as os from "os";
198
- import * as path from "path";
199
- import { z } from "zod";
200
- var CliCredentialsSchema = z.object({
201
- accessToken: z.string(),
202
- refreshToken: z.string(),
203
- expiresAt: z.number(),
204
- keycloakUrl: z.string(),
205
- realm: z.string(),
206
- organizationId: z.string().optional(),
207
- user: z.object({
208
- sub: z.string(),
209
- email: z.string(),
210
- name: z.string().optional()
211
- }).optional()
212
- });
213
- var TokenResponseSchema = z.object({
214
- access_token: z.string(),
215
- refresh_token: z.string(),
216
- expires_in: z.number()
217
- });
218
- var CredentialsStore = class {
219
- static {
220
- __name(this, "CredentialsStore");
221
- }
222
- configDir;
223
- credentialsPath;
224
- constructor(configDir) {
225
- this.configDir = configDir ?? path.join(os.homedir(), ".spekn");
226
- this.credentialsPath = path.join(this.configDir, "credentials.json");
227
- }
228
- /**
229
- * Load credentials from disk.
230
- * Returns null if the file does not exist or cannot be parsed.
231
- */
232
- load() {
233
- try {
234
- const raw = fs.readFileSync(this.credentialsPath, "utf-8");
235
- return CliCredentialsSchema.parse(JSON.parse(raw));
236
- } catch {
237
- return null;
238
- }
239
- }
240
- /**
241
- * Persist credentials to disk with 0600 permissions (owner read/write only).
242
- */
243
- save(creds) {
244
- fs.mkdirSync(this.configDir, {
245
- recursive: true,
246
- mode: 448
247
- });
248
- const json = JSON.stringify(creds, null, 2);
249
- fs.writeFileSync(this.credentialsPath, json, {
250
- encoding: "utf-8",
251
- mode: 384
252
- });
253
- }
254
- /**
255
- * Delete the credentials file if it exists.
256
- */
257
- clear() {
258
- try {
259
- fs.rmSync(this.credentialsPath);
260
- } catch (err) {
261
- if (err.code !== "ENOENT") {
262
- throw err;
263
- }
264
- }
265
- }
266
- /**
267
- * Return a valid access token, refreshing via Keycloak if needed.
268
- *
269
- * - Returns the stored accessToken when it has more than 30 seconds of
270
- * remaining validity.
271
- * - Attempts a refresh_token grant when the token is expired or about to
272
- * expire. Saves the updated credentials and returns the new accessToken.
273
- * - Returns null when no credentials are stored or the refresh fails.
274
- */
275
- async getValidToken() {
276
- const creds = this.load();
277
- if (creds === null) {
278
- return null;
279
- }
280
- const BUFFER_MS = 3e4;
281
- if (Date.now() + BUFFER_MS < creds.expiresAt) {
282
- return creds.accessToken;
283
- }
284
- try {
285
- const tokenUrl = `${creds.keycloakUrl}/realms/${creds.realm}/protocol/openid-connect/token`;
286
- const body = new URLSearchParams({
287
- grant_type: "refresh_token",
288
- client_id: "spekn-cli",
289
- refresh_token: creds.refreshToken
290
- });
291
- const res = await fetch(tokenUrl, {
292
- method: "POST",
293
- body,
294
- headers: {
295
- "Content-Type": "application/x-www-form-urlencoded"
296
- }
297
- });
298
- if (!res.ok) {
299
- return null;
300
- }
301
- const data = TokenResponseSchema.parse(await res.json());
302
- const updated = {
303
- ...creds,
304
- accessToken: data.access_token,
305
- refreshToken: data.refresh_token,
306
- expiresAt: Date.now() + data.expires_in * 1e3
307
- };
308
- this.save(updated);
309
- return updated.accessToken;
310
- } catch {
311
- return null;
312
- }
313
- }
314
- };
315
-
316
- // src/tui/services/client.ts
317
- import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
318
-
319
- // src/utils/trpc-url.ts
320
- function normalizeTrpcUrl(apiUrl) {
321
- if (apiUrl.endsWith("/trpc")) {
322
- return apiUrl;
323
- }
324
- if (apiUrl.endsWith("/")) {
325
- return `${apiUrl}trpc`;
326
- }
327
- return `${apiUrl}/trpc`;
328
- }
329
- __name(normalizeTrpcUrl, "normalizeTrpcUrl");
330
-
331
- // src/tui/services/client.ts
332
- function createApiClient(apiUrl, token, organizationId) {
333
- return createTRPCProxyClient({
334
- links: [
335
- httpBatchLink({
336
- url: normalizeTrpcUrl(apiUrl),
337
- headers: {
338
- authorization: token ? `Bearer ${token}` : "",
339
- "x-organization-id": organizationId
340
- }
341
- })
342
- ]
343
- });
344
- }
345
- __name(createApiClient, "createApiClient");
346
-
347
- // src/tui/services/context-service.ts
348
- function decodeJwtPayload(token) {
349
- try {
350
- const parts = token.split(".");
351
- if (parts.length !== 3) return null;
352
- const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
353
- const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
354
- const json = Buffer.from(padded, "base64").toString("utf-8");
355
- return JSON.parse(json);
356
- } catch {
357
- return null;
358
- }
359
- }
360
- __name(decodeJwtPayload, "decodeJwtPayload");
361
- function normalizePlan(raw) {
362
- if (raw === OrganizationPlan.PRO) return OrganizationPlan.PRO;
363
- if (raw === OrganizationPlan.TEAM) return OrganizationPlan.TEAM;
364
- if (raw === OrganizationPlan.ENTERPRISE) return OrganizationPlan.ENTERPRISE;
365
- return OrganizationPlan.FREE;
366
- }
367
- __name(normalizePlan, "normalizePlan");
368
- function normalizeRole(raw) {
369
- if (raw === "owner" || raw === "admin" || raw === "member" || raw === "viewer") {
370
- return raw;
371
- }
372
- return "member";
373
- }
374
- __name(normalizeRole, "normalizeRole");
375
- var TuiContextService = class {
376
- static {
377
- __name(this, "TuiContextService");
378
- }
379
- apiUrl;
380
- credentialsStore = new CredentialsStore();
381
- bridgeConfigStore = new BridgeConfigStore();
382
- constructor(apiUrl) {
383
- this.apiUrl = apiUrl;
384
- }
385
- async bootstrap(projectIdArg) {
386
- const token = await this.credentialsStore.getValidToken();
387
- if (!token) {
388
- throw new Error("No valid credentials. Run `spekn auth login` first.");
389
- }
390
- const claims = decodeJwtPayload(token);
391
- const permissions = Array.isArray(claims?.permissions) ? claims?.permissions.filter((item) => typeof item === "string") : [];
392
- const stored = this.credentialsStore.load();
393
- const fallbackOrg = stored?.organizationId ?? process.env.SPEKN_ORGANIZATION_ID ?? "";
394
- const bootstrapClient = createApiClient(this.apiUrl, token, fallbackOrg);
395
- const orgs = await bootstrapClient.organization.list.query();
396
- if (orgs.length === 0) {
397
- throw new Error("No organization membership found for this account.");
398
- }
399
- const org = orgs.find((candidate) => candidate.id === fallbackOrg) ?? orgs[0];
400
- const organizationId = org.id;
401
- const client = createApiClient(this.apiUrl, token, organizationId);
402
- const projects = await client.project.list.query({
403
- limit: 20,
404
- offset: 0
405
- });
406
- if (projects.length === 0) {
407
- throw new Error("No projects found for this organization. Create one in Spekn first.");
408
- }
409
- const project = projects.find((candidate) => candidate.id === projectIdArg) ?? projects[0];
410
- return {
411
- boot: {
412
- apiUrl: this.apiUrl,
413
- organizationId,
414
- organizationName: org.name,
415
- role: normalizeRole(org.role),
416
- plan: normalizePlan(org.plan),
417
- projectId: project.id,
418
- projectName: project.name,
419
- permissions
420
- },
421
- client
422
- };
423
- }
424
- async loadSpecs(client, projectId) {
425
- const specs = await client.specification.list.query({
426
- projectId,
427
- limit: 50,
428
- offset: 0
429
- });
430
- return (Array.isArray(specs) ? specs : []).map((spec) => ({
431
- id: spec.id,
432
- title: spec.title,
433
- status: spec.status,
434
- version: spec.version,
435
- updatedAt: spec.updatedAt,
436
- type: spec.frontmatter?.type
437
- }));
438
- }
439
- async loadDecisions(client, projectId) {
440
- const result = await client.decision.getAll.query({
441
- projectId,
442
- limit: 50,
443
- offset: 0
444
- });
445
- const decisions = Array.isArray(result?.decisions) ? result.decisions : [];
446
- return decisions.map((decision) => ({
447
- id: decision.id,
448
- title: decision.title,
449
- status: decision.status,
450
- decisionType: decision.decisionType,
451
- specAnchor: decision.specAnchor,
452
- createdAt: decision.createdAt
453
- }));
454
- }
455
- async loadWorkflowSummary(client, projectId) {
456
- const states = await client.workflowState.listByProject.query({
457
- projectId
458
- });
459
- const first = Array.isArray(states) && states.length > 0 ? states[0] : null;
460
- const currentPhase = first?.currentPhase ?? null;
461
- const blockedCount = Array.isArray(states) ? states.filter((state) => state.specificationLockStatus === "locked").length : 0;
462
- return {
463
- currentPhase,
464
- blockedCount,
465
- hasVerificationEvidence: Boolean(first?.hasVerificationEvidence),
466
- hasPlanningArtifacts: Boolean(first?.hasPlanningArtifacts)
467
- };
468
- }
469
- async previewExport(client, projectId, format) {
470
- const result = await client.export.preview.query({
471
- projectId,
472
- formatId: format
473
- });
474
- return {
475
- content: String(result.content ?? ""),
476
- anchorCount: Number(result.anchorCount ?? 0),
477
- specVersion: typeof result.specVersion === "string" ? result.specVersion : void 0,
478
- warnings: Array.isArray(result.warnings) ? result.warnings.filter((warning) => typeof warning === "string") : void 0
479
- };
480
- }
481
- async generateExport(client, projectId, format) {
482
- const result = await client.export.generate.mutate({
483
- projectId,
484
- formatId: format
485
- });
486
- return {
487
- content: String(result.content ?? ""),
488
- anchorCount: Number(result.anchorCount ?? 0),
489
- specVersion: typeof result.specVersion === "string" ? result.specVersion : void 0,
490
- warnings: Array.isArray(result.warnings) ? result.warnings.filter((warning) => typeof warning === "string") : void 0
491
- };
492
- }
493
- async loadBridgeSummary(client) {
494
- const [flag, devices, metrics] = await Promise.all([
495
- client.bridge.getFeatureFlag.query().catch(() => ({
496
- enabled: false
497
- })),
498
- client.bridge.listDevices.query().catch(() => []),
499
- client.bridge.getMetrics.query().catch(() => ({
500
- connectedDevices: 0,
501
- authFailures: 0
502
- }))
503
- ]);
504
- return {
505
- featureEnabled: Boolean(flag.enabled),
506
- devices: Array.isArray(devices) ? devices.map((device) => ({
507
- id: device.id,
508
- name: device.name,
509
- status: device.status,
510
- isDefault: Boolean(device.isDefault),
511
- lastSeenAt: device.lastSeenAt
512
- })) : [],
513
- connectedDevices: Number(metrics.connectedDevices ?? 0),
514
- authFailures: Number(metrics.authFailures ?? 0)
515
- };
516
- }
517
- async loadLocalBridgeSummary() {
518
- this.bridgeConfigStore.load();
519
- const config = this.bridgeConfigStore.get();
520
- let running = false;
521
- let uptimeSec;
522
- try {
523
- const response = await fetch(`http://127.0.0.1:${config.port}/health`);
524
- if (response.ok) {
525
- const payload = await response.json();
526
- running = true;
527
- uptimeSec = Number(payload.uptime ?? 0);
528
- }
529
- } catch {
530
- running = false;
531
- }
532
- return {
533
- paired: this.bridgeConfigStore.isPaired(),
534
- deviceId: config.pairing?.deviceId,
535
- deviceName: config.pairing?.deviceName,
536
- port: config.port,
537
- running,
538
- uptimeSec
539
- };
540
- }
541
- startLocalBridgeDetached() {
542
- const args = [
543
- process.argv[1] ?? "",
544
- "bridge",
545
- "start"
546
- ];
547
- const child = spawn(process.execPath, args, {
548
- detached: true,
549
- stdio: "ignore"
550
- });
551
- child.unref();
552
- }
553
- async stopLocalBridge(configPort) {
554
- const port = configPort ?? this.bridgeConfigStore.get().port;
555
- try {
556
- await fetch(`http://127.0.0.1:${port}/shutdown`, {
557
- method: "POST"
558
- });
559
- } catch {
560
- }
561
- }
562
- };
563
-
564
- // src/tui/state/use-app-state.tsx
565
- var EMPTY_WORKFLOW = {
566
- currentPhase: null,
567
- blockedCount: 0,
568
- hasPlanningArtifacts: false,
569
- hasVerificationEvidence: false
570
- };
571
- function useAppState(apiUrl, initialScreen, projectId) {
572
- const service = useMemo(() => new TuiContextService(apiUrl), [
573
- apiUrl
574
- ]);
575
- const [state, setState] = useState({
576
- boot: null,
577
- client: null,
578
- loading: true,
579
- error: null,
580
- screen: initialScreen,
581
- navPolicy: [],
582
- specs: [],
583
- decisions: [],
584
- workflow: EMPTY_WORKFLOW,
585
- bridge: null,
586
- localBridge: null,
587
- exportFormat: "claude-md",
588
- exportPreview: null,
589
- statusLine: "Bootstrapping...",
590
- logs: [],
591
- searchQuery: "",
592
- showHelp: false,
593
- commandMode: false
594
- });
595
- const appendLog = useCallback((entry) => {
596
- setState((prev) => ({
597
- ...prev,
598
- logs: [
599
- entry,
600
- ...prev.logs
601
- ].slice(0, 40)
602
- }));
603
- }, []);
604
- const refresh = useCallback(async () => {
605
- setState((prev) => ({
606
- ...prev,
607
- loading: true,
608
- statusLine: "Loading context..."
609
- }));
610
- try {
611
- const { boot, client } = await service.bootstrap(projectId);
612
- const [specs, decisions, workflow, bridge, localBridge] = await Promise.all([
613
- service.loadSpecs(client, boot.projectId),
614
- service.loadDecisions(client, boot.projectId),
615
- service.loadWorkflowSummary(client, boot.projectId),
616
- service.loadBridgeSummary(client),
617
- service.loadLocalBridgeSummary()
618
- ]);
619
- const capabilityContext = {
620
- plan: boot.plan,
621
- role: boot.role,
622
- workflowPhase: workflow.currentPhase,
623
- permissions: boot.permissions
624
- };
625
- const navPolicy = resolveNavPolicy(capabilityContext);
626
- setState((prev) => ({
627
- ...prev,
628
- boot,
629
- client,
630
- specs,
631
- decisions,
632
- workflow,
633
- bridge,
634
- localBridge,
635
- navPolicy,
636
- loading: false,
637
- error: null,
638
- statusLine: "Ready"
639
- }));
640
- } catch (error) {
641
- const message = error instanceof Error ? error.message : String(error);
642
- setState((prev) => ({
643
- ...prev,
644
- loading: false,
645
- error: message,
646
- statusLine: "Error"
647
- }));
648
- appendLog(`[error] ${message}`);
649
- }
650
- }, [
651
- appendLog,
652
- projectId,
653
- service
654
- ]);
655
- useEffect(() => {
656
- void refresh();
657
- }, [
658
- refresh
659
- ]);
660
- useEffect(() => {
661
- const timer = setInterval(() => {
662
- if (!state.client || !state.boot) return;
663
- void Promise.all([
664
- service.loadWorkflowSummary(state.client, state.boot.projectId),
665
- service.loadLocalBridgeSummary()
666
- ]).then(([workflow, localBridge]) => {
667
- setState((prev) => {
668
- const ctx = {
669
- plan: prev.boot?.plan ?? prev.navPolicy[0]?.requiredPlan ?? prev.boot?.plan ?? "free",
670
- role: prev.boot?.role ?? "member",
671
- workflowPhase: workflow.currentPhase,
672
- permissions: prev.boot?.permissions ?? []
673
- };
674
- return {
675
- ...prev,
676
- workflow,
677
- localBridge,
678
- navPolicy: resolveNavPolicy(ctx)
679
- };
680
- });
681
- }).catch(() => void 0);
682
- }, 6e3);
683
- return () => clearInterval(timer);
684
- }, [
685
- service,
686
- state.boot,
687
- state.client,
688
- state.navPolicy
689
- ]);
690
- const setScreen = useCallback((screen) => {
691
- setState((prev) => ({
692
- ...prev,
693
- screen
694
- }));
695
- }, []);
696
- const toggleHelp = useCallback(() => {
697
- setState((prev) => ({
698
- ...prev,
699
- showHelp: !prev.showHelp
700
- }));
701
- }, []);
702
- const setSearchQuery = useCallback((searchQuery) => {
703
- setState((prev) => ({
704
- ...prev,
705
- searchQuery
706
- }));
707
- }, []);
708
- const setCommandMode = useCallback((commandMode) => {
709
- setState((prev) => ({
710
- ...prev,
711
- commandMode
712
- }));
713
- }, []);
714
- const setExportFormat = useCallback((exportFormat) => {
715
- setState((prev) => ({
716
- ...prev,
717
- exportFormat
718
- }));
719
- }, []);
720
- const previewExport = useCallback(async () => {
721
- if (!state.client || !state.boot) return;
722
- setState((prev) => ({
723
- ...prev,
724
- statusLine: "Previewing export..."
725
- }));
726
- try {
727
- const preview = await service.previewExport(state.client, state.boot.projectId, state.exportFormat);
728
- setState((prev) => ({
729
- ...prev,
730
- exportPreview: preview,
731
- statusLine: "Export preview ready"
732
- }));
733
- appendLog(`[export] Previewed ${state.exportFormat} (${preview.anchorCount} anchors)`);
734
- } catch (error) {
735
- const message = error instanceof Error ? error.message : String(error);
736
- appendLog(`[error] Export preview failed: ${message}`);
737
- setState((prev) => ({
738
- ...prev,
739
- statusLine: "Export preview failed"
740
- }));
741
- }
742
- }, [
743
- appendLog,
744
- service,
745
- state.boot,
746
- state.client,
747
- state.exportFormat
748
- ]);
749
- const generateExport = useCallback(async () => {
750
- if (!state.client || !state.boot) return;
751
- setState((prev) => ({
752
- ...prev,
753
- statusLine: "Generating export..."
754
- }));
755
- try {
756
- const output = await service.generateExport(state.client, state.boot.projectId, state.exportFormat);
757
- setState((prev) => ({
758
- ...prev,
759
- exportPreview: output,
760
- statusLine: "Export generated"
761
- }));
762
- appendLog(`[export] Generated ${state.exportFormat} (${output.anchorCount} anchors)`);
763
- } catch (error) {
764
- const message = error instanceof Error ? error.message : String(error);
765
- appendLog(`[error] Export generation failed: ${message}`);
766
- setState((prev) => ({
767
- ...prev,
768
- statusLine: "Export generation failed"
769
- }));
770
- }
771
- }, [
772
- appendLog,
773
- service,
774
- state.boot,
775
- state.client,
776
- state.exportFormat
777
- ]);
778
- const bridgeStart = useCallback(() => {
779
- service.startLocalBridgeDetached();
780
- appendLog("[bridge] Started local bridge process (detached)");
781
- setState((prev) => ({
782
- ...prev,
783
- statusLine: "Bridge start triggered (detached)"
784
- }));
785
- }, [
786
- appendLog,
787
- service
788
- ]);
789
- const bridgeStop = useCallback(async () => {
790
- await service.stopLocalBridge(state.localBridge?.port);
791
- appendLog("[bridge] Stop signal sent");
792
- setState((prev) => ({
793
- ...prev,
794
- statusLine: "Bridge stop signal sent"
795
- }));
796
- }, [
797
- appendLog,
798
- service,
799
- state.localBridge?.port
800
- ]);
801
- return {
802
- state,
803
- refresh,
804
- setScreen,
805
- toggleHelp,
806
- setSearchQuery,
807
- setCommandMode,
808
- setExportFormat,
809
- previewExport,
810
- generateExport,
811
- bridgeStart,
812
- bridgeStop,
813
- appendLog
814
- };
815
- }
816
- __name(useAppState, "useAppState");
817
-
818
- // src/tui/keymap/use-global-keymap.ts
819
- import { useInput } from "ink";
820
-
821
- // src/tui/navigation/nav-items.ts
822
- function nextScreen(current, items) {
823
- const index = items.findIndex((item) => item.id === current);
824
- if (index === -1 || items.length === 0) return "home";
825
- const target = items[(index + 1) % items.length];
826
- return target?.id ?? "home";
827
- }
828
- __name(nextScreen, "nextScreen");
829
- function previousScreen(current, items) {
830
- const index = items.findIndex((item) => item.id === current);
831
- if (index === -1 || items.length === 0) return "home";
832
- const target = items[(index - 1 + items.length) % items.length];
833
- return target?.id ?? "home";
834
- }
835
- __name(previousScreen, "previousScreen");
836
-
837
- // src/tui/keymap/use-global-keymap.ts
838
- function useGlobalKeymap(options) {
839
- useInput((input, key) => {
840
- if (options.commandMode) {
841
- if (key.escape) {
842
- options.onCommandModeToggle(false);
843
- }
844
- return;
845
- }
846
- if (key.ctrl && input === "c") {
847
- process.exit(0);
848
- return;
849
- }
850
- if (input === "?") {
851
- options.onHelpToggle();
852
- return;
853
- }
854
- if (input === ":") {
855
- options.onCommandModeToggle(true);
856
- return;
857
- }
858
- if (input === "/") {
859
- options.onSearchToggle();
860
- return;
861
- }
862
- if (key.escape) {
863
- options.onSearchClear();
864
- return;
865
- }
866
- if (input === "j" || key.downArrow) {
867
- options.onNavigate(nextScreen(options.screen, options.navPolicy));
868
- return;
869
- }
870
- if (input === "k" || key.upArrow) {
871
- options.onNavigate(previousScreen(options.screen, options.navPolicy));
872
- return;
873
- }
874
- if (input === "h" || key.leftArrow) {
875
- options.onNavigate(previousScreen(options.screen, options.navPolicy));
876
- return;
877
- }
878
- if (input === "l" || key.rightArrow) {
879
- options.onNavigate(nextScreen(options.screen, options.navPolicy));
880
- return;
881
- }
882
- if (input === "r") {
883
- options.onRefresh();
884
- return;
885
- }
886
- if (options.screen === "export" && input === "p") {
887
- options.onExportPreview();
888
- return;
889
- }
890
- if (options.screen === "export" && input === "g") {
891
- options.onExportGenerate();
892
- return;
893
- }
894
- if (options.screen === "bridge" && input === "s") {
895
- options.onBridgeStart();
896
- return;
897
- }
898
- if (options.screen === "bridge" && input === "x") {
899
- options.onBridgeStop();
900
- return;
901
- }
902
- if (input === "1") options.onNavigate("home");
903
- if (input === "2") options.onNavigate("specs");
904
- if (input === "3") options.onNavigate("export");
905
- if (input === "4") options.onNavigate("decisions");
906
- if (input === "5") options.onNavigate("bridge");
907
- });
908
- }
909
- __name(useGlobalKeymap, "useGlobalKeymap");
910
-
911
- // src/tui/components/frame.tsx
912
- import React from "react";
913
- import { Box, Text } from "ink";
914
- function Frame({ title, children, dim = false }) {
915
- return /* @__PURE__ */ React.createElement(Box, {
916
- flexDirection: "column",
917
- borderStyle: "round",
918
- borderColor: dim ? "gray" : "cyan",
919
- paddingX: 1,
920
- paddingY: 0,
921
- marginRight: 1,
922
- minHeight: 7
923
- }, /* @__PURE__ */ React.createElement(Text, {
924
- bold: true,
925
- color: dim ? "gray" : "cyan"
926
- }, title), /* @__PURE__ */ React.createElement(Box, {
927
- flexDirection: "column",
928
- marginTop: 0
929
- }, children));
930
- }
931
- __name(Frame, "Frame");
932
-
933
- // src/tui/components/status-bar.tsx
934
- import React2 from "react";
935
- import { Box as Box2, Text as Text2 } from "ink";
936
- function StatusBar({ left, center, right }) {
937
- return /* @__PURE__ */ React2.createElement(Box2, {
938
- borderStyle: "single",
939
- borderColor: "gray",
940
- paddingX: 1,
941
- justifyContent: "space-between"
942
- }, /* @__PURE__ */ React2.createElement(Text2, {
943
- color: "gray"
944
- }, left), /* @__PURE__ */ React2.createElement(Text2, null, center), /* @__PURE__ */ React2.createElement(Text2, {
945
- color: "gray"
946
- }, right));
947
- }
948
- __name(StatusBar, "StatusBar");
949
-
950
- // src/tui/screens/home.tsx
951
- import React3 from "react";
952
- import { Box as Box3, Text as Text3 } from "ink";
953
- import { Spinner, StatusMessage, UnorderedList } from "@inkjs/ui";
954
- function nextBestAction(state) {
955
- if (state.error) return "Fix authentication/context errors first";
956
- if (!state.exportPreview) return "Generate context export (press 3, then g)";
957
- if (state.workflow.blockedCount > 0) return "Review blocked workflow states";
958
- if (!state.workflow.hasPlanningArtifacts) return "Create planning artifacts in PLAN phase";
959
- if (!state.workflow.hasVerificationEvidence) return "Capture verification evidence";
960
- return "Review Decision Log and continue implementation";
961
- }
962
- __name(nextBestAction, "nextBestAction");
963
- function phaseVariant(state) {
964
- if (state.workflow.blockedCount > 0) return "warning";
965
- if (state.workflow.currentPhase) return "info";
966
- return "success";
967
- }
968
- __name(phaseVariant, "phaseVariant");
969
- function HomeScreen({ state }) {
970
- if (state.loading) {
971
- return /* @__PURE__ */ React3.createElement(Spinner, {
972
- label: "Loading dashboard..."
973
- });
974
- }
975
- const action = nextBestAction(state);
976
- return /* @__PURE__ */ React3.createElement(Box3, {
977
- flexDirection: "column"
978
- }, /* @__PURE__ */ React3.createElement(Text3, {
979
- bold: true,
980
- color: "green"
981
- }, "Next Best Action"), /* @__PURE__ */ React3.createElement(Text3, null, action), /* @__PURE__ */ React3.createElement(Box3, {
982
- marginTop: 1,
983
- flexDirection: "column"
984
- }, /* @__PURE__ */ React3.createElement(Text3, {
985
- bold: true
986
- }, "Workflow"), /* @__PURE__ */ React3.createElement(StatusMessage, {
987
- variant: phaseVariant(state)
988
- }, "Phase: ", state.workflow.currentPhase ?? "n/a", " | Blocked: ", state.workflow.blockedCount)), /* @__PURE__ */ React3.createElement(Box3, {
989
- marginTop: 1,
990
- flexDirection: "column"
991
- }, /* @__PURE__ */ React3.createElement(Text3, {
992
- bold: true
993
- }, "Unfinished Work"), /* @__PURE__ */ React3.createElement(UnorderedList, null, /* @__PURE__ */ React3.createElement(UnorderedList.Item, null, /* @__PURE__ */ React3.createElement(Text3, null, "Specs: ", state.specs.length)), /* @__PURE__ */ React3.createElement(UnorderedList.Item, null, /* @__PURE__ */ React3.createElement(Text3, null, "Decisions: ", state.decisions.length)), /* @__PURE__ */ React3.createElement(UnorderedList.Item, null, /* @__PURE__ */ React3.createElement(Text3, null, "Export ready: ", state.exportPreview ? "yes" : "no")))), /* @__PURE__ */ React3.createElement(Box3, {
994
- marginTop: 1
995
- }, /* @__PURE__ */ React3.createElement(Text3, {
996
- color: "gray"
997
- }, "Quick actions: [2] Specs [3] Export [4] Decisions [5] Bridge")));
998
- }
999
- __name(HomeScreen, "HomeScreen");
1000
-
1001
- // src/tui/screens/specs.tsx
1002
- import React4 from "react";
1003
- import { Box as Box4, Text as Text4 } from "ink";
1004
- import { Badge, Spinner as Spinner2, UnorderedList as UnorderedList2 } from "@inkjs/ui";
1005
- function statusBadgeColor(status) {
1006
- if (status === "locked") return "green";
1007
- if (status === "draft") return "yellow";
1008
- if (status === "review") return "blue";
1009
- if (status === "archived") return "red";
1010
- return "yellow";
1011
- }
1012
- __name(statusBadgeColor, "statusBadgeColor");
1013
- function filterItems(items, query) {
1014
- if (!query.trim()) return items;
1015
- const normalized = query.trim().toLowerCase();
1016
- return items.filter((item) => item.title.toLowerCase().includes(normalized));
1017
- }
1018
- __name(filterItems, "filterItems");
1019
- function SpecsScreen({ state }) {
1020
- if (state.loading) {
1021
- return /* @__PURE__ */ React4.createElement(Spinner2, {
1022
- label: "Loading specifications..."
1023
- });
1024
- }
1025
- const specs = filterItems(state.specs, state.searchQuery).slice(0, 7);
1026
- const selected = specs[0];
1027
- return /* @__PURE__ */ React4.createElement(Box4, {
1028
- flexDirection: "column"
1029
- }, /* @__PURE__ */ React4.createElement(Text4, {
1030
- bold: true
1031
- }, "Specifications ", state.searchQuery ? `(filter: ${state.searchQuery})` : ""), specs.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, {
1032
- color: "gray"
1033
- }, "No specs in current view.") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(UnorderedList2, null, specs.map((spec, index) => /* @__PURE__ */ React4.createElement(UnorderedList2.Item, {
1034
- key: spec.id
1035
- }, /* @__PURE__ */ React4.createElement(Text4, {
1036
- color: index === 0 ? "green" : void 0
1037
- }, spec.title, " ", /* @__PURE__ */ React4.createElement(Badge, {
1038
- color: statusBadgeColor(spec.status)
1039
- }, spec.status), " v", spec.version)))), state.specs.length > 7 ? /* @__PURE__ */ React4.createElement(Text4, {
1040
- color: "gray"
1041
- }, "...showing first 7 specs (Miller's law limit)") : null), /* @__PURE__ */ React4.createElement(Box4, {
1042
- marginTop: 1,
1043
- flexDirection: "column"
1044
- }, /* @__PURE__ */ React4.createElement(Text4, {
1045
- bold: true
1046
- }, "Anchor Detail"), selected ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Text4, null, "ID: ", selected.id), /* @__PURE__ */ React4.createElement(Text4, null, "Status: ", selected.status), /* @__PURE__ */ React4.createElement(Text4, null, "Type: ", selected.type ?? "n/a"), /* @__PURE__ */ React4.createElement(Text4, null, "Updated: ", selected.updatedAt ?? "n/a")) : /* @__PURE__ */ React4.createElement(Text4, {
1047
- color: "gray"
1048
- }, "Select a specification to view details.")));
1049
- }
1050
- __name(SpecsScreen, "SpecsScreen");
1051
-
1052
- // src/tui/screens/export.tsx
1053
- import React5 from "react";
1054
- import { Box as Box5, Text as Text5 } from "ink";
1055
- import { Spinner as Spinner3, StatusMessage as StatusMessage2 } from "@inkjs/ui";
1056
- function ExportScreen({ state }) {
1057
- if (state.loading) {
1058
- return /* @__PURE__ */ React5.createElement(Spinner3, {
1059
- label: "Loading export..."
1060
- });
1061
- }
1062
- const preview = state.exportPreview;
1063
- return /* @__PURE__ */ React5.createElement(Box5, {
1064
- flexDirection: "column"
1065
- }, /* @__PURE__ */ React5.createElement(Text5, {
1066
- bold: true
1067
- }, "Context Export"), /* @__PURE__ */ React5.createElement(Text5, null, "Format: ", state.exportFormat), /* @__PURE__ */ React5.createElement(Text5, {
1068
- color: "gray"
1069
- }, "Commands: p=preview, g=generate, :=run command palette"), /* @__PURE__ */ React5.createElement(Box5, {
1070
- marginTop: 1,
1071
- flexDirection: "column"
1072
- }, /* @__PURE__ */ React5.createElement(Text5, {
1073
- bold: true
1074
- }, "Summary"), /* @__PURE__ */ React5.createElement(Text5, null, "Anchors: ", preview?.anchorCount ?? 0), /* @__PURE__ */ React5.createElement(Text5, null, "Spec version: ", preview?.specVersion ?? "n/a"), preview?.warnings?.length ? /* @__PURE__ */ React5.createElement(StatusMessage2, {
1075
- variant: "warning"
1076
- }, "Warnings: ", preview.warnings.join("; ")) : /* @__PURE__ */ React5.createElement(Text5, null, "Warnings: none")), state.statusLine.includes("generated") || state.statusLine.includes("Export generated") ? /* @__PURE__ */ React5.createElement(Box5, {
1077
- marginTop: 1
1078
- }, /* @__PURE__ */ React5.createElement(StatusMessage2, {
1079
- variant: "success"
1080
- }, "Export generated successfully")) : state.statusLine.includes("failed") ? /* @__PURE__ */ React5.createElement(Box5, {
1081
- marginTop: 1
1082
- }, /* @__PURE__ */ React5.createElement(StatusMessage2, {
1083
- variant: "error"
1084
- }, "Export generation failed")) : null, /* @__PURE__ */ React5.createElement(Box5, {
1085
- marginTop: 1,
1086
- flexDirection: "column"
1087
- }, /* @__PURE__ */ React5.createElement(Text5, {
1088
- bold: true
1089
- }, "Preview (first 12 lines)"), preview ? preview.content.split("\n").slice(0, 12).map((line, index) => /* @__PURE__ */ React5.createElement(Text5, {
1090
- key: `preview-${index}`
1091
- }, line)) : /* @__PURE__ */ React5.createElement(Text5, {
1092
- color: "gray"
1093
- }, "No preview yet.")));
1094
- }
1095
- __name(ExportScreen, "ExportScreen");
1096
-
1097
- // src/tui/screens/decisions.tsx
1098
- import React6 from "react";
1099
- import { Box as Box6, Text as Text6 } from "ink";
1100
- import { Badge as Badge2, Spinner as Spinner4, UnorderedList as UnorderedList3 } from "@inkjs/ui";
1101
- function statusBadgeColor2(status) {
1102
- if (status === "approved") return "green";
1103
- if (status === "pending") return "yellow";
1104
- if (status === "rejected") return "red";
1105
- return "blue";
1106
- }
1107
- __name(statusBadgeColor2, "statusBadgeColor");
1108
- function filterItems2(items, query) {
1109
- if (!query.trim()) return items;
1110
- const normalized = query.trim().toLowerCase();
1111
- return items.filter((item) => item.title.toLowerCase().includes(normalized));
1112
- }
1113
- __name(filterItems2, "filterItems");
1114
- function DecisionsScreen({ state }) {
1115
- if (state.loading) {
1116
- return /* @__PURE__ */ React6.createElement(Spinner4, {
1117
- label: "Loading decisions..."
1118
- });
1119
- }
1120
- const decisions = filterItems2(state.decisions, state.searchQuery).slice(0, 7);
1121
- return /* @__PURE__ */ React6.createElement(Box6, {
1122
- flexDirection: "column"
1123
- }, /* @__PURE__ */ React6.createElement(Text6, {
1124
- bold: true
1125
- }, "Decision Log"), decisions.length === 0 ? /* @__PURE__ */ React6.createElement(Text6, {
1126
- color: "gray"
1127
- }, "No decisions in current view.") : /* @__PURE__ */ React6.createElement(UnorderedList3, null, decisions.map((decision, index) => /* @__PURE__ */ React6.createElement(UnorderedList3.Item, {
1128
- key: decision.id
1129
- }, /* @__PURE__ */ React6.createElement(Text6, {
1130
- color: index === 0 ? "green" : void 0
1131
- }, decision.title, " ", /* @__PURE__ */ React6.createElement(Badge2, {
1132
- color: statusBadgeColor2(decision.status)
1133
- }, decision.status), " (", decision.decisionType, ") ", decision.specAnchor ? `@${decision.specAnchor}` : "")))), /* @__PURE__ */ React6.createElement(Box6, {
1134
- marginTop: 1,
1135
- flexDirection: "column"
1136
- }, /* @__PURE__ */ React6.createElement(Text6, {
1137
- bold: true
1138
- }, "Role-aware Actions"), state.boot?.role === "viewer" ? /* @__PURE__ */ React6.createElement(Text6, {
1139
- color: "yellow"
1140
- }, "Viewer role: read-only. Approvals disabled.") : /* @__PURE__ */ React6.createElement(Text6, {
1141
- color: "gray"
1142
- }, "Use command palette: :approve DECISION_ID or :reject DECISION_ID")));
1143
- }
1144
- __name(DecisionsScreen, "DecisionsScreen");
1145
-
1146
- // src/tui/screens/bridge.tsx
1147
- import React7, { useState as useState2 } from "react";
1148
- import { Box as Box7, Text as Text7 } from "ink";
1149
- import { Badge as Badge3, ConfirmInput, Spinner as Spinner5, StatusMessage as StatusMessage3 } from "@inkjs/ui";
1150
- function BridgeScreen({ state, onBridgeStop }) {
1151
- const [confirmingStop, setConfirmingStop] = useState2(false);
1152
- if (state.loading) {
1153
- return /* @__PURE__ */ React7.createElement(Spinner5, {
1154
- label: "Checking bridge health..."
1155
- });
1156
- }
1157
- return /* @__PURE__ */ React7.createElement(Box7, {
1158
- flexDirection: "column"
1159
- }, /* @__PURE__ */ React7.createElement(Text7, {
1160
- bold: true
1161
- }, "Local Bridge"), /* @__PURE__ */ React7.createElement(Text7, null, "Feature flag: ", state.bridge?.featureEnabled ? "enabled" : "disabled"), /* @__PURE__ */ React7.createElement(Text7, null, "Cloud connected devices: ", state.bridge?.connectedDevices ?? 0), /* @__PURE__ */ React7.createElement(Text7, null, "Cloud auth failures: ", state.bridge?.authFailures ?? 0), /* @__PURE__ */ React7.createElement(Box7, {
1162
- marginTop: 1,
1163
- flexDirection: "column"
1164
- }, /* @__PURE__ */ React7.createElement(Text7, {
1165
- bold: true
1166
- }, "Local Runtime"), state.localBridge?.running ? /* @__PURE__ */ React7.createElement(StatusMessage3, {
1167
- variant: "success"
1168
- }, "Running on port ", state.localBridge.port) : /* @__PURE__ */ React7.createElement(StatusMessage3, {
1169
- variant: "error"
1170
- }, "Stopped"), /* @__PURE__ */ React7.createElement(Text7, null, "Paired: ", state.localBridge?.paired ? "yes" : "no"), /* @__PURE__ */ React7.createElement(Text7, null, "Uptime: ", state.localBridge?.uptimeSec ?? 0, "s")), /* @__PURE__ */ React7.createElement(Box7, {
1171
- marginTop: 1,
1172
- flexDirection: "column"
1173
- }, /* @__PURE__ */ React7.createElement(Text7, {
1174
- bold: true
1175
- }, "Actions"), confirmingStop ? /* @__PURE__ */ React7.createElement(Box7, {
1176
- gap: 1
1177
- }, /* @__PURE__ */ React7.createElement(Text7, null, "Stop the bridge?"), /* @__PURE__ */ React7.createElement(ConfirmInput, {
1178
- onConfirm: /* @__PURE__ */ __name(() => {
1179
- setConfirmingStop(false);
1180
- onBridgeStop?.();
1181
- }, "onConfirm"),
1182
- onCancel: /* @__PURE__ */ __name(() => {
1183
- setConfirmingStop(false);
1184
- }, "onCancel")
1185
- })) : /* @__PURE__ */ React7.createElement(Text7, {
1186
- color: "gray"
1187
- }, "s=start (detached), x=stop signal, r=refresh")), /* @__PURE__ */ React7.createElement(Box7, {
1188
- marginTop: 1,
1189
- flexDirection: "column"
1190
- }, /* @__PURE__ */ React7.createElement(Text7, {
1191
- bold: true
1192
- }, "Registered Devices"), (state.bridge?.devices ?? []).slice(0, 7).map((device) => /* @__PURE__ */ React7.createElement(Text7, {
1193
- key: device.id
1194
- }, "- ", device.name, " ", /* @__PURE__ */ React7.createElement(Badge3, {
1195
- color: device.status === "online" ? "green" : device.status === "error" ? "red" : "yellow"
1196
- }, device.status), " ", device.isDefault ? "(default)" : ""))));
1197
- }
1198
- __name(BridgeScreen, "BridgeScreen");
1199
-
1200
- // src/tui/screens/locked.tsx
1201
- import React8 from "react";
1202
- import { Box as Box8 } from "ink";
1203
- import { Alert } from "@inkjs/ui";
1204
- function LockedScreen({ item }) {
1205
- return /* @__PURE__ */ React8.createElement(Box8, {
1206
- flexDirection: "column"
1207
- }, /* @__PURE__ */ React8.createElement(Alert, {
1208
- variant: "warning"
1209
- }, item?.label ?? "Feature", " is locked \u2014 ", item?.reason ?? "Upgrade required", ". ", item?.description ?? "Higher-tier feature preview", ". This feature is visible for discoverability but unavailable on your current plan."));
1210
- }
1211
- __name(LockedScreen, "LockedScreen");
1212
-
1213
- // src/tui/app.tsx
1214
- var speknTheme = extendTheme(defaultTheme, {
1215
- components: {
1216
- Spinner: {
1217
- styles: {
1218
- frame: /* @__PURE__ */ __name(() => ({
1219
- color: "#6366F1"
1220
- }), "frame")
1221
- }
1222
- }
1223
- }
1224
- });
1225
- function renderMainScreen(screen, state) {
1226
- if (screen === "home") return /* @__PURE__ */ React9.createElement(HomeScreen, {
1227
- state
1228
- });
1229
- if (screen === "specs") return /* @__PURE__ */ React9.createElement(SpecsScreen, {
1230
- state
1231
- });
1232
- if (screen === "export") return /* @__PURE__ */ React9.createElement(ExportScreen, {
1233
- state
1234
- });
1235
- if (screen === "decisions") return /* @__PURE__ */ React9.createElement(DecisionsScreen, {
1236
- state
1237
- });
1238
- if (screen === "bridge") return /* @__PURE__ */ React9.createElement(BridgeScreen, {
1239
- state
1240
- });
1241
- const nav = state.navPolicy.find((item) => item.id === screen);
1242
- return /* @__PURE__ */ React9.createElement(LockedScreen, {
1243
- item: nav
1244
- });
1245
- }
1246
- __name(renderMainScreen, "renderMainScreen");
1247
- function TuiApp({ apiUrl, initialScreen, projectId }) {
1248
- const { state, refresh, setScreen, toggleHelp, setSearchQuery, setCommandMode, setExportFormat, previewExport, generateExport, bridgeStart, bridgeStop, appendLog } = useAppState(apiUrl, initialScreen, projectId);
1249
- useGlobalKeymap({
1250
- screen: state.screen,
1251
- navPolicy: state.navPolicy,
1252
- commandMode: state.commandMode,
1253
- showHelp: state.showHelp,
1254
- onNavigate: setScreen,
1255
- onHelpToggle: toggleHelp,
1256
- onSearchToggle: /* @__PURE__ */ __name(() => setSearchQuery(state.searchQuery ? "" : "*"), "onSearchToggle"),
1257
- onSearchClear: /* @__PURE__ */ __name(() => setSearchQuery(""), "onSearchClear"),
1258
- onCommandModeToggle: setCommandMode,
1259
- onRefresh: /* @__PURE__ */ __name(() => {
1260
- void refresh();
1261
- }, "onRefresh"),
1262
- onExportPreview: /* @__PURE__ */ __name(() => {
1263
- void previewExport();
1264
- }, "onExportPreview"),
1265
- onExportGenerate: /* @__PURE__ */ __name(() => {
1266
- void generateExport();
1267
- }, "onExportGenerate"),
1268
- onBridgeStart: bridgeStart,
1269
- onBridgeStop: /* @__PURE__ */ __name(() => {
1270
- void bridgeStop();
1271
- }, "onBridgeStop")
1272
- });
1273
- const handleCommandSubmit = /* @__PURE__ */ __name((command) => {
1274
- const normalized = command.trim().toLowerCase();
1275
- setCommandMode(false);
1276
- if (!normalized) return;
1277
- if (normalized === "help") {
1278
- toggleHelp();
1279
- return;
1280
- }
1281
- if (normalized === "refresh" || normalized === "r") {
1282
- void refresh();
1283
- return;
1284
- }
1285
- if (normalized.startsWith("goto ")) {
1286
- const target = normalized.replace("goto ", "").trim();
1287
- setScreen(target);
1288
- appendLog(`[nav] goto ${target}`);
1289
- return;
1290
- }
1291
- if (normalized === "show specs") {
1292
- setScreen("specs");
1293
- return;
1294
- }
1295
- if (normalized === "show export") {
1296
- setScreen("export");
1297
- return;
1298
- }
1299
- if (normalized === "run export") {
1300
- void generateExport();
1301
- return;
1302
- }
1303
- if (normalized === "format claude") {
1304
- setExportFormat("claude-md");
1305
- appendLog("[export] Format switched to claude-md");
1306
- return;
1307
- }
1308
- if (normalized === "format cursor") {
1309
- setExportFormat("cursorrules");
1310
- appendLog("[export] Format switched to cursorrules");
1311
- return;
1312
- }
1313
- if (normalized === "bridge start") {
1314
- bridgeStart();
1315
- return;
1316
- }
1317
- if (normalized === "bridge stop") {
1318
- void bridgeStop();
1319
- return;
1320
- }
1321
- appendLog(`[command] Unknown: ${command}`);
1322
- }, "handleCommandSubmit");
1323
- const left = `${state.boot?.organizationName ?? "..."} / ${state.boot?.projectName ?? "..."}`;
1324
- const center = state.loading ? "Loading..." : state.statusLine;
1325
- const right = `${state.screen} | phase:${state.workflow.currentPhase ?? "n/a"} | role:${state.boot?.role ?? "n/a"} | tier:${state.boot?.plan ?? "n/a"}`;
1326
- return /* @__PURE__ */ React9.createElement(ThemeProvider, {
1327
- theme: speknTheme
1328
- }, /* @__PURE__ */ React9.createElement(Box9, {
1329
- flexDirection: "column"
1330
- }, /* @__PURE__ */ React9.createElement(Text8, {
1331
- bold: true,
1332
- color: "cyan"
1333
- }, "Spekn TUI - Technical Cockpit"), /* @__PURE__ */ React9.createElement(Text8, {
1334
- color: "gray"
1335
- }, "Shortcuts: j/k/h/l nav, : commands, ? help, r refresh, Ctrl+C quit"), state.error ? /* @__PURE__ */ React9.createElement(Box9, {
1336
- marginTop: 1
1337
- }, /* @__PURE__ */ React9.createElement(Alert2, {
1338
- variant: "error"
1339
- }, state.error)) : null, /* @__PURE__ */ React9.createElement(Box9, {
1340
- marginTop: 1
1341
- }, /* @__PURE__ */ React9.createElement(Box9, {
1342
- width: "28%",
1343
- flexDirection: "column"
1344
- }, /* @__PURE__ */ React9.createElement(Frame, {
1345
- title: "Navigation"
1346
- }, state.navPolicy.map((item, index) => /* @__PURE__ */ React9.createElement(Text8, {
1347
- key: item.id,
1348
- color: state.screen === item.id ? "green" : item.state === "locked" ? "yellow" : item.state === "disabled" ? "gray" : void 0
1349
- }, state.screen === item.id ? ">" : " ", " ", index + 1, ". ", item.label, item.state === "locked" ? " (locked)" : item.state === "disabled" ? " (disabled)" : "")))), /* @__PURE__ */ React9.createElement(Box9, {
1350
- width: "52%",
1351
- flexDirection: "column"
1352
- }, /* @__PURE__ */ React9.createElement(Frame, {
1353
- title: "Context"
1354
- }, renderMainScreen(state.screen, state))), /* @__PURE__ */ React9.createElement(Box9, {
1355
- width: "20%",
1356
- flexDirection: "column"
1357
- }, /* @__PURE__ */ React9.createElement(Frame, {
1358
- title: "Event Log",
1359
- dim: true
1360
- }, (state.logs.length === 0 ? [
1361
- "[system] ready"
1362
- ] : state.logs.slice(0, 7)).map((line, index) => /* @__PURE__ */ React9.createElement(Text8, {
1363
- key: `log-${index}`,
1364
- color: "gray"
1365
- }, line))))), state.showHelp ? /* @__PURE__ */ React9.createElement(Box9, {
1366
- marginTop: 1
1367
- }, /* @__PURE__ */ React9.createElement(Alert2, {
1368
- variant: "info"
1369
- }, "Help: j/k/h/l navigate | :help | :goto specs | :run export | format claude/cursor | bridge start/stop")) : null, state.commandMode ? /* @__PURE__ */ React9.createElement(Box9, {
1370
- marginTop: 1
1371
- }, /* @__PURE__ */ React9.createElement(Text8, {
1372
- color: "green"
1373
- }, ":"), /* @__PURE__ */ React9.createElement(TextInput, {
1374
- placeholder: "command...",
1375
- onSubmit: handleCommandSubmit
1376
- })) : null, /* @__PURE__ */ React9.createElement(StatusBar, {
1377
- left,
1378
- center,
1379
- right
1380
- })));
1381
- }
1382
- __name(TuiApp, "TuiApp");
1383
-
1384
- // src/tui/index.tsx
1385
- async function runTuiCli(args) {
1386
- try {
1387
- const options = parseTuiArgs(args);
1388
- if (options.noColor) {
1389
- process.env.FORCE_COLOR = "0";
1390
- }
1391
- render(/* @__PURE__ */ React10.createElement(TuiApp, {
1392
- apiUrl: options.apiUrl,
1393
- initialScreen: options.initialView,
1394
- projectId: options.projectId
1395
- }));
1396
- return 0;
1397
- } catch (error) {
1398
- const message = error instanceof Error ? error.message : String(error);
1399
- process.stderr.write(`Error: ${message}
1400
- `);
1401
- return 1;
1402
- }
1403
- }
1404
- __name(runTuiCli, "runTuiCli");
1405
- export {
1406
- runTuiCli
1407
- };