@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.
- package/README.md +58 -0
- package/dist/main.js +3707 -611
- package/dist/tui/index.mjs +2 -2
- package/package.json +29 -12
- package/dist/__tests__/export-cli.test.d.ts +0 -1
- package/dist/__tests__/export-cli.test.js +0 -70
- package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
- package/dist/__tests__/tui-args-policy.test.js +0 -50
- package/dist/acp-S2MHZOAD.mjs +0 -23
- package/dist/acp-UCCI44JY.mjs +0 -25
- package/dist/auth/credentials-store.d.ts +0 -2
- package/dist/auth/credentials-store.js +0 -5
- package/dist/auth/device-flow.d.ts +0 -36
- package/dist/auth/device-flow.js +0 -189
- package/dist/auth/jwt.d.ts +0 -1
- package/dist/auth/jwt.js +0 -6
- package/dist/auth/session.d.ts +0 -67
- package/dist/auth/session.js +0 -86
- package/dist/auth-login.d.ts +0 -34
- package/dist/auth-login.js +0 -202
- package/dist/auth-logout.d.ts +0 -25
- package/dist/auth-logout.js +0 -115
- package/dist/auth-status.d.ts +0 -24
- package/dist/auth-status.js +0 -109
- package/dist/backlog-generate.d.ts +0 -11
- package/dist/backlog-generate.js +0 -308
- package/dist/backlog-health.d.ts +0 -11
- package/dist/backlog-health.js +0 -287
- package/dist/bridge-login.d.ts +0 -40
- package/dist/bridge-login.js +0 -277
- package/dist/chunk-3PAYRI4G.mjs +0 -2428
- package/dist/chunk-M4CS3A25.mjs +0 -2426
- package/dist/commands/auth/login.d.ts +0 -30
- package/dist/commands/auth/login.js +0 -164
- package/dist/commands/auth/logout.d.ts +0 -25
- package/dist/commands/auth/logout.js +0 -115
- package/dist/commands/auth/status.d.ts +0 -24
- package/dist/commands/auth/status.js +0 -109
- package/dist/commands/backlog/generate.d.ts +0 -11
- package/dist/commands/backlog/generate.js +0 -308
- package/dist/commands/backlog/health.d.ts +0 -11
- package/dist/commands/backlog/health.js +0 -287
- package/dist/commands/bridge/login.d.ts +0 -36
- package/dist/commands/bridge/login.js +0 -258
- package/dist/commands/export.d.ts +0 -35
- package/dist/commands/export.js +0 -485
- package/dist/commands/marketplace-export.d.ts +0 -21
- package/dist/commands/marketplace-export.js +0 -214
- package/dist/commands/project-clean.d.ts +0 -1
- package/dist/commands/project-clean.js +0 -126
- package/dist/commands/repo/common.d.ts +0 -105
- package/dist/commands/repo/common.js +0 -775
- package/dist/commands/repo/detach.d.ts +0 -2
- package/dist/commands/repo/detach.js +0 -120
- package/dist/commands/repo/register.d.ts +0 -21
- package/dist/commands/repo/register.js +0 -175
- package/dist/commands/repo/sync.d.ts +0 -22
- package/dist/commands/repo/sync.js +0 -873
- package/dist/commands/skills-import-local.d.ts +0 -16
- package/dist/commands/skills-import-local.js +0 -352
- package/dist/commands/spec/drift-check.d.ts +0 -3
- package/dist/commands/spec/drift-check.js +0 -186
- package/dist/commands/spec/frontmatter.d.ts +0 -11
- package/dist/commands/spec/frontmatter.js +0 -219
- package/dist/commands/spec/lint.d.ts +0 -11
- package/dist/commands/spec/lint.js +0 -499
- package/dist/commands/spec/parse.d.ts +0 -11
- package/dist/commands/spec/parse.js +0 -162
- package/dist/export.d.ts +0 -35
- package/dist/export.js +0 -485
- package/dist/main.d.ts +0 -1
- package/dist/marketplace-export.d.ts +0 -21
- package/dist/marketplace-export.js +0 -214
- package/dist/project-clean.d.ts +0 -1
- package/dist/project-clean.js +0 -126
- package/dist/project-context.d.ts +0 -99
- package/dist/project-context.js +0 -376
- package/dist/repo-common.d.ts +0 -101
- package/dist/repo-common.js +0 -671
- package/dist/repo-detach.d.ts +0 -2
- package/dist/repo-detach.js +0 -102
- package/dist/repo-ingest.d.ts +0 -29
- package/dist/repo-ingest.js +0 -305
- package/dist/repo-register.d.ts +0 -21
- package/dist/repo-register.js +0 -175
- package/dist/repo-sync.d.ts +0 -16
- package/dist/repo-sync.js +0 -152
- package/dist/resources/prompt-loader.d.ts +0 -1
- package/dist/resources/prompt-loader.js +0 -62
- package/dist/skills-import-local.d.ts +0 -16
- package/dist/skills-import-local.js +0 -352
- package/dist/spec-drift-check.d.ts +0 -3
- package/dist/spec-drift-check.js +0 -186
- package/dist/spec-frontmatter.d.ts +0 -11
- package/dist/spec-frontmatter.js +0 -219
- package/dist/spec-lint.d.ts +0 -11
- package/dist/spec-lint.js +0 -499
- package/dist/spec-parse.d.ts +0 -11
- package/dist/spec-parse.js +0 -162
- package/dist/stubs/dotenv.d.ts +0 -5
- package/dist/stubs/dotenv.js +0 -6
- package/dist/stubs/typeorm.d.ts +0 -22
- package/dist/stubs/typeorm.js +0 -28
- package/dist/tui/app.d.ts +0 -7
- package/dist/tui/app.js +0 -122
- package/dist/tui/args.d.ts +0 -8
- package/dist/tui/args.js +0 -57
- package/dist/tui/capabilities/policy.d.ts +0 -7
- package/dist/tui/capabilities/policy.js +0 -64
- package/dist/tui/components/frame.d.ts +0 -8
- package/dist/tui/components/frame.js +0 -8
- package/dist/tui/components/status-bar.d.ts +0 -8
- package/dist/tui/components/status-bar.js +0 -8
- package/dist/tui/index.d.ts +0 -2
- package/dist/tui/index.js +0 -23
- package/dist/tui/keymap/use-global-keymap.d.ts +0 -19
- package/dist/tui/keymap/use-global-keymap.js +0 -82
- package/dist/tui/navigation/nav-items.d.ts +0 -3
- package/dist/tui/navigation/nav-items.js +0 -18
- package/dist/tui/screens/bridge.d.ts +0 -8
- package/dist/tui/screens/bridge.js +0 -19
- package/dist/tui/screens/decisions.d.ts +0 -5
- package/dist/tui/screens/decisions.js +0 -28
- package/dist/tui/screens/export.d.ts +0 -5
- package/dist/tui/screens/export.js +0 -16
- package/dist/tui/screens/home.d.ts +0 -5
- package/dist/tui/screens/home.js +0 -33
- package/dist/tui/screens/locked.d.ts +0 -5
- package/dist/tui/screens/locked.js +0 -9
- package/dist/tui/screens/specs.d.ts +0 -5
- package/dist/tui/screens/specs.js +0 -31
- package/dist/tui/services/client.d.ts +0 -1
- package/dist/tui/services/client.js +0 -18
- package/dist/tui/services/context-service.d.ts +0 -19
- package/dist/tui/services/context-service.js +0 -246
- package/dist/tui/shared-enums.d.ts +0 -16
- package/dist/tui/shared-enums.js +0 -19
- package/dist/tui/state/use-app-state.d.ts +0 -35
- package/dist/tui/state/use-app-state.js +0 -177
- package/dist/tui/types.d.ts +0 -77
- package/dist/tui/types.js +0 -2
- package/dist/tui-bundle.d.ts +0 -1
- package/dist/tui-bundle.js +0 -5
- package/dist/tui-entry.mjs +0 -1407
- package/dist/utils/cli-runtime.d.ts +0 -5
- package/dist/utils/cli-runtime.js +0 -22
- package/dist/utils/help-error.d.ts +0 -7
- package/dist/utils/help-error.js +0 -14
- package/dist/utils/interaction.d.ts +0 -19
- package/dist/utils/interaction.js +0 -93
- package/dist/utils/structured-log.d.ts +0 -7
- package/dist/utils/structured-log.js +0 -112
- package/dist/utils/trpc-url.d.ts +0 -4
- package/dist/utils/trpc-url.js +0 -15
package/dist/tui-entry.mjs
DELETED
|
@@ -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
|
-
};
|