@treeseed/cli 0.4.12 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/handlers/auth-login.js +58 -46
- package/dist/cli/handlers/auth-logout.js +23 -11
- package/dist/cli/handlers/auth-whoami.js +27 -16
- package/dist/cli/handlers/config-ui.d.ts +65 -9
- package/dist/cli/handlers/config-ui.js +561 -175
- package/dist/cli/handlers/config.js +177 -11
- package/dist/cli/handlers/dev.js +6 -1
- package/dist/cli/handlers/doctor.js +11 -5
- package/dist/cli/handlers/secret-prompts.d.ts +2 -0
- package/dist/cli/handlers/secret-prompts.js +54 -0
- package/dist/cli/handlers/secrets.d.ts +7 -0
- package/dist/cli/handlers/secrets.js +175 -0
- package/dist/cli/handlers/status.js +31 -5
- package/dist/cli/handlers/workflow.js +31 -0
- package/dist/cli/help-ui.js +1 -1
- package/dist/cli/operations-registry.js +129 -9
- package/dist/cli/registry.d.ts +6 -0
- package/dist/cli/registry.js +15 -1
- package/dist/cli/repair.js +5 -9
- package/dist/cli/ui/framework.d.ts +2 -0
- package/dist/cli/ui/framework.js +53 -22
- package/dist/cli/ui/mouse.d.ts +3 -1
- package/dist/cli/ui/mouse.js +3 -3
- package/package.json +7 -6
- package/scripts/verify-driver.mjs +34 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { Box, render, Text, useApp, useInput, usePaste, useWindowSize } from "ink";
|
|
2
3
|
import React from "react";
|
|
3
4
|
import {
|
|
4
5
|
AppFrame,
|
|
@@ -14,25 +15,71 @@ import {
|
|
|
14
15
|
SecondaryButton,
|
|
15
16
|
StatusBar,
|
|
16
17
|
TextInputField,
|
|
17
|
-
|
|
18
|
+
formatSecretMaskedValue,
|
|
18
19
|
truncateLine,
|
|
19
20
|
wrapText
|
|
20
21
|
} from "../ui/framework.js";
|
|
21
22
|
import { useTerminalMouse } from "../ui/mouse.js";
|
|
22
|
-
const
|
|
23
|
-
const CONFIG_VIEW_MODES = ["startup", "full"];
|
|
23
|
+
const FULL_CONFIG_FILTERS = ["local", "staging", "prod"];
|
|
24
24
|
function maskValue(value) {
|
|
25
25
|
if (!value) {
|
|
26
26
|
return "(unset)";
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
return "********";
|
|
30
|
-
}
|
|
31
|
-
return `${value.slice(0, 3)}...${value.slice(-3)}`;
|
|
28
|
+
return formatSecretMaskedValue(value);
|
|
32
29
|
}
|
|
33
30
|
function scopeOrder(scope) {
|
|
34
31
|
return ["local", "staging", "prod"].indexOf(scope);
|
|
35
32
|
}
|
|
33
|
+
function providerWorkflowKey(entry) {
|
|
34
|
+
const id = entry.id.toUpperCase();
|
|
35
|
+
if (id.startsWith("GH_") || id.includes("GITHUB")) {
|
|
36
|
+
return "github";
|
|
37
|
+
}
|
|
38
|
+
if (id.startsWith("CLOUDFLARE_") || id.includes("TURNSTILE") || entry.group === "cloudflare") {
|
|
39
|
+
return "cloudflare";
|
|
40
|
+
}
|
|
41
|
+
if (id.startsWith("RAILWAY_") || entry.group === "railway") {
|
|
42
|
+
return "railway";
|
|
43
|
+
}
|
|
44
|
+
if (entry.group === "local-development") {
|
|
45
|
+
return "local-development";
|
|
46
|
+
}
|
|
47
|
+
if (entry.group === "forms") {
|
|
48
|
+
return "forms";
|
|
49
|
+
}
|
|
50
|
+
if (entry.group === "smtp") {
|
|
51
|
+
return "smtp";
|
|
52
|
+
}
|
|
53
|
+
if (entry.group === "auth") {
|
|
54
|
+
return "auth-core";
|
|
55
|
+
}
|
|
56
|
+
return entry.group;
|
|
57
|
+
}
|
|
58
|
+
function providerWorkflowRank(entry) {
|
|
59
|
+
const order = ["auth-core", "github", "cloudflare", "railway", "local-development", "forms", "smtp"];
|
|
60
|
+
const index = order.indexOf(providerWorkflowKey(entry));
|
|
61
|
+
return index === -1 ? order.length : index;
|
|
62
|
+
}
|
|
63
|
+
function normalizedClusterKey(entry) {
|
|
64
|
+
const provider = providerWorkflowKey(entry);
|
|
65
|
+
const cluster = entry.cluster.trim().toLowerCase();
|
|
66
|
+
if (provider === "cloudflare") {
|
|
67
|
+
if (entry.id.includes("API_TOKEN") || entry.id.includes("ACCOUNT_ID")) {
|
|
68
|
+
return "cloudflare-account";
|
|
69
|
+
}
|
|
70
|
+
if (entry.id.includes("TURNSTILE") || cluster.includes("turnstile")) {
|
|
71
|
+
return "cloudflare-turnstile";
|
|
72
|
+
}
|
|
73
|
+
return `cloudflare-${cluster}`;
|
|
74
|
+
}
|
|
75
|
+
if (provider === "railway") {
|
|
76
|
+
if (entry.id.includes("API_TOKEN")) {
|
|
77
|
+
return "railway-access";
|
|
78
|
+
}
|
|
79
|
+
return `railway-${cluster}`;
|
|
80
|
+
}
|
|
81
|
+
return `${provider}-${cluster}`;
|
|
82
|
+
}
|
|
36
83
|
function resolveFirstNonEmptyValue(scopes, entriesByScope, entryId, field) {
|
|
37
84
|
for (const scope of scopes) {
|
|
38
85
|
const entry = entriesByScope[scope].find((candidate) => candidate.id === entryId);
|
|
@@ -43,15 +90,78 @@ function resolveFirstNonEmptyValue(scopes, entriesByScope, entryId, field) {
|
|
|
43
90
|
}
|
|
44
91
|
return "";
|
|
45
92
|
}
|
|
93
|
+
function resolveSharedEntryValue(relevantScopes, requiredScopes, entriesByScope, entryId, field, options = {}) {
|
|
94
|
+
const preferredScopes = requiredScopes.length > 0 ? requiredScopes : relevantScopes;
|
|
95
|
+
const preferredValue = resolveFirstNonEmptyValue(preferredScopes, entriesByScope, entryId, field);
|
|
96
|
+
if (preferredValue.length > 0) {
|
|
97
|
+
return preferredValue;
|
|
98
|
+
}
|
|
99
|
+
if (options.fallbackToRelevant === false) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
return resolveFirstNonEmptyValue(relevantScopes, entriesByScope, entryId, field);
|
|
103
|
+
}
|
|
104
|
+
function resolveCurrentConfigValue(context, overrides, entryId, scope = "local") {
|
|
105
|
+
const sharedOverrideKey = `shared:${entryId}`;
|
|
106
|
+
if (sharedOverrideKey in overrides) {
|
|
107
|
+
return overrides[sharedOverrideKey] ?? "";
|
|
108
|
+
}
|
|
109
|
+
const scopedOverrideKey = `${scope}:${entryId}`;
|
|
110
|
+
if (scopedOverrideKey in overrides) {
|
|
111
|
+
return overrides[scopedOverrideKey] ?? "";
|
|
112
|
+
}
|
|
113
|
+
for (const candidateScope of [scope, ...context.scopes.filter((candidate) => candidate !== scope)]) {
|
|
114
|
+
const entry = context.entriesByScope[candidateScope].find((candidate) => candidate.id === entryId);
|
|
115
|
+
if (entry?.storage === "shared") {
|
|
116
|
+
const overrideKey = `shared:${entryId}`;
|
|
117
|
+
if (overrideKey in overrides) {
|
|
118
|
+
return overrides[overrideKey] ?? "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (typeof entry?.currentValue === "string" && entry.currentValue.length > 0) {
|
|
122
|
+
return entry.currentValue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
46
127
|
function hasUsableValue(value) {
|
|
47
128
|
return typeof value === "string" && value.trim().length > 0;
|
|
48
129
|
}
|
|
130
|
+
function isConfigValueValid(entry, value) {
|
|
131
|
+
if (!hasUsableValue(value)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (!entry.validation) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
switch (entry.validation.kind) {
|
|
138
|
+
case "string":
|
|
139
|
+
case "nonempty":
|
|
140
|
+
return value.trim().length > 0 && (typeof entry.validation.minLength !== "number" || value.trim().length >= entry.validation.minLength);
|
|
141
|
+
case "boolean":
|
|
142
|
+
return /^(true|false|1|0)$/i.test(value);
|
|
143
|
+
case "number":
|
|
144
|
+
return Number.isFinite(Number(value));
|
|
145
|
+
case "url":
|
|
146
|
+
try {
|
|
147
|
+
new URL(value);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
case "email":
|
|
153
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
154
|
+
case "enum":
|
|
155
|
+
return entry.validation.values.includes(value);
|
|
156
|
+
default:
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
49
160
|
function isWizardRequiredMissing(page) {
|
|
50
|
-
if (
|
|
161
|
+
if (page.requiredScopes.length === 0) {
|
|
51
162
|
return false;
|
|
52
163
|
}
|
|
53
|
-
|
|
54
|
-
return !hasUsableValue(resolvedValue);
|
|
164
|
+
return !isConfigValueValid(page.entry, page.finalValue);
|
|
55
165
|
}
|
|
56
166
|
function startupPriority(page) {
|
|
57
167
|
if (page.required) {
|
|
@@ -68,6 +178,21 @@ function formatDisplayValue(page, value, emptyLabel) {
|
|
|
68
178
|
}
|
|
69
179
|
return page.entry.sensitivity === "secret" ? maskValue(value) : value;
|
|
70
180
|
}
|
|
181
|
+
function filterCliConfigPages(pages, query) {
|
|
182
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
183
|
+
if (!normalizedQuery) {
|
|
184
|
+
return pages;
|
|
185
|
+
}
|
|
186
|
+
return pages.filter(
|
|
187
|
+
(page) => [
|
|
188
|
+
page.entry.id,
|
|
189
|
+
page.entry.label,
|
|
190
|
+
page.entry.group,
|
|
191
|
+
page.entry.cluster,
|
|
192
|
+
page.scope
|
|
193
|
+
].some((field) => field.toLowerCase().includes(normalizedQuery))
|
|
194
|
+
);
|
|
195
|
+
}
|
|
71
196
|
function tabRects(prefix, items, selectedIndex, y, startX) {
|
|
72
197
|
let x = startX + prefix.length;
|
|
73
198
|
return items.map((item, index) => {
|
|
@@ -90,7 +215,7 @@ function buttonRects(labels, y, startX) {
|
|
|
90
215
|
});
|
|
91
216
|
}
|
|
92
217
|
function computeConfigViewportLayout(rows, columns) {
|
|
93
|
-
const layout = computeViewportLayout(rows, columns, { topBarHeight:
|
|
218
|
+
const layout = computeViewportLayout(rows, columns, { topBarHeight: 3, footerHeight: 2 });
|
|
94
219
|
const sidebarWidth = Math.max(22, Math.min(34, Math.floor(layout.columns * 0.28)));
|
|
95
220
|
const contentWidth = Math.max(34, layout.columns - sidebarWidth - 1);
|
|
96
221
|
const actionRowHeight = 1;
|
|
@@ -107,7 +232,7 @@ function computeConfigViewportLayout(rows, columns) {
|
|
|
107
232
|
};
|
|
108
233
|
}
|
|
109
234
|
function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode = "startup") {
|
|
110
|
-
const selectedScopes =
|
|
235
|
+
const selectedScopes = viewMode === "startup" ? context.scopes : context.scopes.filter((scope) => scope === selectedFilter);
|
|
111
236
|
const sharedEntries = /* @__PURE__ */ new Set();
|
|
112
237
|
const pages = [];
|
|
113
238
|
for (const scope of selectedScopes) {
|
|
@@ -119,17 +244,30 @@ function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode =
|
|
|
119
244
|
const relevantScopes = selectedScopes.filter((candidateScope) => context.entriesByScope[candidateScope].some((candidate) => candidate.id === entry.id));
|
|
120
245
|
const key2 = `shared:${entry.id}`;
|
|
121
246
|
sharedEntries.add(entry.id);
|
|
122
|
-
const
|
|
123
|
-
const
|
|
247
|
+
const requiredScopes = relevantScopes.filter((candidateScope) => context.entriesByScope[candidateScope].some((candidate) => candidate.id === entry.id && candidate.required));
|
|
248
|
+
const currentValue = resolveSharedEntryValue(relevantScopes, requiredScopes, context.entriesByScope, entry.id, "currentValue");
|
|
249
|
+
const suggestedValue = resolveSharedEntryValue(relevantScopes, requiredScopes, context.entriesByScope, entry.id, "suggestedValue", {
|
|
250
|
+
fallbackToRelevant: requiredScopes.length === 0
|
|
251
|
+
});
|
|
252
|
+
const effectiveValue = resolveSharedEntryValue(relevantScopes, requiredScopes, context.entriesByScope, entry.id, "effectiveValue", {
|
|
253
|
+
fallbackToRelevant: requiredScopes.length === 0
|
|
254
|
+
});
|
|
124
255
|
const candidatePage2 = {
|
|
256
|
+
kind: "entry",
|
|
125
257
|
key: key2,
|
|
126
258
|
entry,
|
|
127
259
|
scope,
|
|
128
260
|
scopes: relevantScopes,
|
|
129
|
-
|
|
261
|
+
requiredScopes,
|
|
262
|
+
required: requiredScopes.length > 0,
|
|
130
263
|
currentValue,
|
|
131
264
|
suggestedValue,
|
|
132
|
-
finalValue: key2
|
|
265
|
+
finalValue: resolveEntryPageFinalValue(key2, {
|
|
266
|
+
...entry,
|
|
267
|
+
currentValue,
|
|
268
|
+
suggestedValue,
|
|
269
|
+
effectiveValue
|
|
270
|
+
}, overrides)
|
|
133
271
|
};
|
|
134
272
|
pages.push({
|
|
135
273
|
...candidatePage2,
|
|
@@ -139,14 +277,16 @@ function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode =
|
|
|
139
277
|
}
|
|
140
278
|
const key = `${scope}:${entry.id}`;
|
|
141
279
|
const candidatePage = {
|
|
280
|
+
kind: "entry",
|
|
142
281
|
key,
|
|
143
282
|
entry,
|
|
144
283
|
scope,
|
|
145
284
|
scopes: [scope],
|
|
285
|
+
requiredScopes: entry.required ? [scope] : [],
|
|
146
286
|
required: entry.required,
|
|
147
287
|
currentValue: entry.currentValue,
|
|
148
288
|
suggestedValue: entry.suggestedValue,
|
|
149
|
-
finalValue: key
|
|
289
|
+
finalValue: resolveEntryPageFinalValue(key, entry, overrides)
|
|
150
290
|
};
|
|
151
291
|
pages.push({
|
|
152
292
|
...candidatePage,
|
|
@@ -158,18 +298,25 @@ function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode =
|
|
|
158
298
|
if (startupPriority(left) !== startupPriority(right)) {
|
|
159
299
|
return startupPriority(left) - startupPriority(right);
|
|
160
300
|
}
|
|
161
|
-
if (left.entry.
|
|
162
|
-
|
|
301
|
+
if (left.entry.startupProfile !== right.entry.startupProfile) {
|
|
302
|
+
const order = { core: 0, optional: 1, advanced: 2 };
|
|
303
|
+
return order[left.entry.startupProfile] - order[right.entry.startupProfile];
|
|
163
304
|
}
|
|
164
|
-
if (left.entry
|
|
165
|
-
return
|
|
305
|
+
if (providerWorkflowRank(left.entry) !== providerWorkflowRank(right.entry)) {
|
|
306
|
+
return providerWorkflowRank(left.entry) - providerWorkflowRank(right.entry);
|
|
307
|
+
}
|
|
308
|
+
if (normalizedClusterKey(left.entry) !== normalizedClusterKey(right.entry)) {
|
|
309
|
+
return normalizedClusterKey(left.entry).localeCompare(normalizedClusterKey(right.entry));
|
|
166
310
|
}
|
|
167
|
-
if (left.entry.
|
|
168
|
-
return left.entry.
|
|
311
|
+
if (left.entry.storage !== right.entry.storage) {
|
|
312
|
+
return left.entry.storage === "shared" ? -1 : 1;
|
|
169
313
|
}
|
|
170
314
|
if (left.scope !== right.scope) {
|
|
171
315
|
return scopeOrder(left.scope) - scopeOrder(right.scope);
|
|
172
316
|
}
|
|
317
|
+
if (left.entry.purposes.length !== right.entry.purposes.length) {
|
|
318
|
+
return right.entry.purposes.length - left.entry.purposes.length;
|
|
319
|
+
}
|
|
173
320
|
return left.entry.label.localeCompare(right.entry.label);
|
|
174
321
|
});
|
|
175
322
|
return viewMode === "startup" ? orderedPages.filter((page) => page.wizardRequiredMissing) : orderedPages;
|
|
@@ -179,17 +326,17 @@ function buildStartupDetailLines(step, draftValue) {
|
|
|
179
326
|
return ["No startup configuration is required for the selected environment set."];
|
|
180
327
|
}
|
|
181
328
|
return [
|
|
182
|
-
|
|
183
|
-
step.
|
|
184
|
-
step.
|
|
185
|
-
|
|
186
|
-
"",
|
|
187
|
-
step.entry.description || "Treeseed needs this value to complete setup.",
|
|
329
|
+
`${step.entry.label} (${step.entry.id})`,
|
|
330
|
+
`Applies to: ${step.scopes.join(", ")}`,
|
|
331
|
+
`Required in: ${step.requiredScopes.join(", ")}`,
|
|
332
|
+
`Storage: ${step.entry.storage}`,
|
|
188
333
|
"",
|
|
189
|
-
`How to get it: ${step.entry.howToGet || "Use the suggested/default value if it matches your setup."}`,
|
|
190
334
|
`Current value: ${formatDisplayValue(step, step.currentValue, "(unset)")}`,
|
|
191
335
|
`Suggested value: ${formatDisplayValue(step, step.suggestedValue, "(none)")}`,
|
|
192
|
-
`Pending value: ${formatDisplayValue(step, draftValue, "(unset)")}
|
|
336
|
+
`Pending value: ${formatDisplayValue(step, draftValue, "(unset)")}`,
|
|
337
|
+
"",
|
|
338
|
+
step.entry.description || "Treeseed needs this value to complete setup.",
|
|
339
|
+
`How to get it: ${step.entry.howToGet || "Use the suggested/default value if it matches your setup."}`
|
|
193
340
|
];
|
|
194
341
|
}
|
|
195
342
|
function buildFullDetailLines(page, draftValue) {
|
|
@@ -197,23 +344,17 @@ function buildFullDetailLines(page, draftValue) {
|
|
|
197
344
|
return ["No configuration entries match the selected environment filter."];
|
|
198
345
|
}
|
|
199
346
|
return [
|
|
200
|
-
page.entry.label
|
|
201
|
-
page.entry.id,
|
|
347
|
+
`${page.entry.label} (${page.entry.id})`,
|
|
202
348
|
`Scope: ${page.scopes.join(", ")}`,
|
|
203
349
|
`Storage: ${page.entry.storage} | ${page.required ? "required" : "optional"}`,
|
|
204
350
|
`Group: ${page.entry.group}`,
|
|
205
|
-
`Used for: ${page.entry.purposes.join(", ") || "(none)"}`,
|
|
206
|
-
`Targets: ${page.entry.targets.join(", ") || "(none)"}`,
|
|
207
|
-
"",
|
|
208
|
-
"About",
|
|
209
|
-
page.entry.description || "(no description)",
|
|
210
|
-
"",
|
|
211
|
-
"How to get it",
|
|
212
|
-
page.entry.howToGet || "(no extra setup guidance)",
|
|
213
351
|
"",
|
|
214
352
|
`Current: ${formatDisplayValue(page, page.currentValue, "(unset)")}`,
|
|
215
353
|
`Suggested: ${formatDisplayValue(page, page.suggestedValue, "(none)")}`,
|
|
216
|
-
`Pending: ${formatDisplayValue(page, draftValue, "(unset)")}
|
|
354
|
+
`Pending: ${formatDisplayValue(page, draftValue, "(unset)")}`,
|
|
355
|
+
"",
|
|
356
|
+
page.entry.description || "(no description)",
|
|
357
|
+
`Get it: ${page.entry.howToGet || "(no extra setup guidance)"}`
|
|
217
358
|
];
|
|
218
359
|
}
|
|
219
360
|
function detailViewportLines(lines, width, height, offset) {
|
|
@@ -233,6 +374,12 @@ function nextDraftValue(page, drafts) {
|
|
|
233
374
|
}
|
|
234
375
|
return page.key in drafts ? drafts[page.key] : page.finalValue;
|
|
235
376
|
}
|
|
377
|
+
function resolveEntryPageFinalValue(pageKey, entry, overrides) {
|
|
378
|
+
if (pageKey in overrides) {
|
|
379
|
+
return overrides[pageKey];
|
|
380
|
+
}
|
|
381
|
+
return entry.effectiveValue || entry.suggestedValue || entry.currentValue || "";
|
|
382
|
+
}
|
|
236
383
|
function insertAt(value, insert, cursor) {
|
|
237
384
|
return `${value.slice(0, cursor)}${insert}${value.slice(cursor)}`;
|
|
238
385
|
}
|
|
@@ -255,10 +402,47 @@ function deleteForward(value, cursor) {
|
|
|
255
402
|
};
|
|
256
403
|
}
|
|
257
404
|
function cycleFocus(current, viewMode) {
|
|
258
|
-
const areas = viewMode === "startup" ? ["
|
|
405
|
+
const areas = viewMode === "startup" ? ["content", "actions"] : ["environment", "filter", "sidebar", "content", "actions"];
|
|
259
406
|
const index = areas.indexOf(current);
|
|
260
407
|
return areas[(index + 1) % areas.length] ?? "content";
|
|
261
408
|
}
|
|
409
|
+
function normalizeConfigInputChunk(input) {
|
|
410
|
+
if (!input) {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
return input.replace(/\u001b\[200~/gu, "").replace(/\u001b\[201~/gu, "").replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "").replace(/\n+$/gu, "");
|
|
414
|
+
}
|
|
415
|
+
function applyConfigInputInsertion(state, input) {
|
|
416
|
+
const normalized = normalizeConfigInputChunk(input);
|
|
417
|
+
if (!normalized) {
|
|
418
|
+
return state;
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
value: insertAt(state.value, normalized, state.cursor),
|
|
422
|
+
cursor: state.cursor + normalized.length
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function runClipboardCommand(command, args) {
|
|
426
|
+
const result = spawnSync(command, args, {
|
|
427
|
+
stdio: "pipe",
|
|
428
|
+
encoding: "utf8",
|
|
429
|
+
timeout: 1500
|
|
430
|
+
});
|
|
431
|
+
if (result.status !== 0) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const text = String(result.stdout ?? "").replace(/\r\n/gu, "\n");
|
|
435
|
+
return text.length > 0 ? text : null;
|
|
436
|
+
}
|
|
437
|
+
function readLinuxClipboardText() {
|
|
438
|
+
if (process.platform !== "linux") {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
return runClipboardCommand("wl-paste", ["--no-newline"]) ?? runClipboardCommand("xclip", ["-selection", "clipboard", "-o"]) ?? runClipboardCommand("xsel", ["--clipboard", "--output"]);
|
|
442
|
+
}
|
|
443
|
+
function isCtrlVPaste(input, key) {
|
|
444
|
+
return key.ctrl && input === "v" || input === "";
|
|
445
|
+
}
|
|
262
446
|
async function runCliConfigEditor(context, options = {}) {
|
|
263
447
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
264
448
|
return null;
|
|
@@ -275,6 +459,8 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
275
459
|
resolveSession(result);
|
|
276
460
|
};
|
|
277
461
|
function App() {
|
|
462
|
+
const sidebarFilterHeight = 4;
|
|
463
|
+
const [currentContext, setCurrentContext] = React.useState(context);
|
|
278
464
|
const [filterIndex, setFilterIndex] = React.useState(0);
|
|
279
465
|
const [viewMode, setViewMode] = React.useState(options.initialViewMode ?? "startup");
|
|
280
466
|
const [pageIndex, setPageIndex] = React.useState(0);
|
|
@@ -283,32 +469,59 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
283
469
|
const [drafts, setDrafts] = React.useState({});
|
|
284
470
|
const [cursorPositions, setCursorPositions] = React.useState({});
|
|
285
471
|
const [overrides, setOverrides] = React.useState({});
|
|
286
|
-
const [
|
|
472
|
+
const [filterQuery, setFilterQuery] = React.useState("");
|
|
473
|
+
const [filterCursor, setFilterCursor] = React.useState(0);
|
|
474
|
+
const [focusArea, setFocusArea] = React.useState(options.initialViewMode === "full" ? "filter" : "content");
|
|
287
475
|
const [actionIndex, setActionIndex] = React.useState(0);
|
|
288
|
-
const [
|
|
476
|
+
const [saving, setSaving] = React.useState(false);
|
|
477
|
+
const [statusMessage, setStatusMessage] = React.useState(
|
|
478
|
+
options.initialStatusMessage ?? (options.initialViewMode === "full" ? "Full editor ready. Filter the variable list or edit the selected value." : "Startup wizard ready. Update each required value in order.")
|
|
479
|
+
);
|
|
289
480
|
const { exit } = useApp();
|
|
290
481
|
const windowSize = useWindowSize();
|
|
291
482
|
const layout = computeConfigViewportLayout(windowSize?.rows ?? 24, windowSize?.columns ?? 100);
|
|
292
|
-
const selectedFilter =
|
|
293
|
-
const
|
|
483
|
+
const selectedFilter = FULL_CONFIG_FILTERS[filterIndex] ?? "local";
|
|
484
|
+
const allPages = buildCliConfigPages(currentContext, selectedFilter, overrides, viewMode);
|
|
485
|
+
const pages = viewMode === "full" ? filterCliConfigPages(allPages, filterQuery) : allPages;
|
|
294
486
|
const safePageIndex = pages.length === 0 ? 0 : Math.min(pageIndex, pages.length - 1);
|
|
295
487
|
const selectedPage = pages[safePageIndex] ?? null;
|
|
296
488
|
const draftValue = nextDraftValue(selectedPage, drafts);
|
|
297
489
|
const cursorPosition = selectedPage ? Math.max(0, Math.min(cursorPositions[selectedPage.key] ?? draftValue.length, draftValue.length)) : 0;
|
|
490
|
+
const focusAreaRef = React.useRef(focusArea);
|
|
491
|
+
const viewModeRef = React.useRef(viewMode);
|
|
492
|
+
const selectedPageRef = React.useRef(selectedPage);
|
|
493
|
+
const draftValueRef = React.useRef(draftValue);
|
|
494
|
+
const cursorPositionRef = React.useRef(cursorPosition);
|
|
495
|
+
const filterQueryRef = React.useRef(filterQuery);
|
|
496
|
+
const filterCursorRef = React.useRef(filterCursor);
|
|
497
|
+
focusAreaRef.current = focusArea;
|
|
498
|
+
viewModeRef.current = viewMode;
|
|
499
|
+
selectedPageRef.current = selectedPage;
|
|
500
|
+
draftValueRef.current = draftValue;
|
|
501
|
+
cursorPositionRef.current = cursorPosition;
|
|
502
|
+
filterQueryRef.current = filterQuery;
|
|
503
|
+
filterCursorRef.current = filterCursor;
|
|
298
504
|
const startupStep = selectedPage ? { ...selectedPage, index: safePageIndex, total: pages.length } : null;
|
|
299
505
|
const detailSourceLines = viewMode === "startup" ? buildStartupDetailLines(startupStep, draftValue) : buildFullDetailLines(selectedPage, draftValue);
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
const
|
|
506
|
+
const detailWidth = viewMode === "full" ? layout.contentWidth : layout.columns;
|
|
507
|
+
const detailPanel = detailViewportLines(detailSourceLines, detailWidth, layout.detailHeight, detailOffset);
|
|
508
|
+
const configReadiness = {
|
|
509
|
+
github: { configured: hasUsableValue(resolveCurrentConfigValue(currentContext, overrides, "GH_TOKEN", "local")) },
|
|
510
|
+
cloudflare: { configured: hasUsableValue(resolveCurrentConfigValue(currentContext, overrides, "CLOUDFLARE_API_TOKEN", "local")) },
|
|
511
|
+
railway: { configured: hasUsableValue(resolveCurrentConfigValue(currentContext, overrides, "RAILWAY_API_TOKEN", "local")) },
|
|
512
|
+
localDevelopment: currentContext.configReadinessByScope.local?.localDevelopment ?? { configured: true }
|
|
513
|
+
};
|
|
514
|
+
const sidebarHeight = Math.max(4, layout.bodyHeight - sidebarFilterHeight);
|
|
515
|
+
const sidebarViewportSize = Math.max(1, sidebarHeight - 4);
|
|
303
516
|
const safeSidebarOffset = clampOffset(ensureVisible(safePageIndex, sidebarOffset, sidebarViewportSize), pages.length, sidebarViewportSize);
|
|
304
517
|
const visibleSidebar = pages.slice(safeSidebarOffset, safeSidebarOffset + sidebarViewportSize);
|
|
305
|
-
const startupActions = selectedPage ? ["Back", ...hasUsableValue(selectedPage?.suggestedValue ?? "") ? ["Use Suggested + Next"] : [], "Update + Next"
|
|
306
|
-
const fullActions = ["
|
|
518
|
+
const startupActions = selectedPage ? ["Back", ...hasUsableValue(selectedPage?.suggestedValue ?? "") ? ["Use Suggested + Next"] : [], "Update + Next"] : [];
|
|
519
|
+
const fullActions = ["Save Value", "Clear", "Finish"];
|
|
307
520
|
const actions = viewMode === "startup" ? startupActions : fullActions;
|
|
308
|
-
const envRects = tabRects("Env ",
|
|
309
|
-
const modeRects = tabRects("View ", CONFIG_VIEW_MODES, CONFIG_VIEW_MODES.indexOf(viewMode), 2, Math.floor(layout.columns / 2));
|
|
521
|
+
const envRects = viewMode === "full" ? tabRects("Env ", FULL_CONFIG_FILTERS, filterIndex, 2, 1) : [];
|
|
310
522
|
const clickRegions = [];
|
|
311
523
|
const sidebarRect = { x: 0, y: layout.topBarHeight, width: layout.sidebarWidth, height: layout.bodyHeight };
|
|
524
|
+
const filterRect = { x: 0, y: layout.topBarHeight, width: layout.sidebarWidth, height: sidebarFilterHeight };
|
|
312
525
|
const detailRect = {
|
|
313
526
|
x: viewMode === "full" ? layout.sidebarWidth + 1 : 0,
|
|
314
527
|
y: layout.topBarHeight,
|
|
@@ -332,32 +545,120 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
332
545
|
}, [detailPanel.offset, detailOffset]);
|
|
333
546
|
React.useEffect(() => {
|
|
334
547
|
setActionIndex(0);
|
|
335
|
-
}, [viewMode]);
|
|
548
|
+
}, [viewMode, selectedPage?.key]);
|
|
336
549
|
React.useEffect(() => {
|
|
337
550
|
setDetailOffset(0);
|
|
338
551
|
}, [selectedPage?.key]);
|
|
552
|
+
React.useEffect(() => {
|
|
553
|
+
if (viewMode === "startup" && selectedPage) {
|
|
554
|
+
setFocusArea("content");
|
|
555
|
+
}
|
|
556
|
+
}, [selectedPage?.key, viewMode, selectedPage]);
|
|
557
|
+
React.useEffect(() => {
|
|
558
|
+
setPageIndex(0);
|
|
559
|
+
setSidebarOffset(0);
|
|
560
|
+
}, [filterQuery, selectedFilter]);
|
|
339
561
|
React.useEffect(() => {
|
|
340
562
|
if (selectedPage && !(selectedPage.key in cursorPositions)) {
|
|
341
563
|
setCursorPositions((current) => ({ ...current, [selectedPage.key]: draftValue.length }));
|
|
342
564
|
}
|
|
343
565
|
}, [cursorPositions, draftValue.length, selectedPage]);
|
|
344
566
|
React.useEffect(() => {
|
|
345
|
-
setFocusArea(viewMode === "full" ? "
|
|
567
|
+
setFocusArea(viewMode === "full" ? "filter" : "content");
|
|
346
568
|
}, [viewMode]);
|
|
347
|
-
|
|
348
|
-
if (
|
|
569
|
+
React.useEffect(() => {
|
|
570
|
+
if (viewMode === "startup" && pages.length === 0) {
|
|
571
|
+
setViewMode("full");
|
|
572
|
+
setFocusArea("filter");
|
|
573
|
+
setPageIndex(0);
|
|
574
|
+
setFilterIndex(0);
|
|
575
|
+
setFilterQuery("");
|
|
576
|
+
setFilterCursor(0);
|
|
577
|
+
setStatusMessage("Startup configuration is complete. Switched to the full editor.");
|
|
578
|
+
}
|
|
579
|
+
}, [pages.length, viewMode]);
|
|
580
|
+
const finishWithOverrides = React.useCallback((nextOverrides) => {
|
|
581
|
+
finish({
|
|
582
|
+
overrides: nextOverrides,
|
|
583
|
+
viewMode
|
|
584
|
+
});
|
|
585
|
+
}, [viewMode]);
|
|
586
|
+
const advanceStartupFlow = React.useCallback((nextOverrides, currentPageKey) => {
|
|
587
|
+
const nextPages = buildCliConfigPages(currentContext, selectedFilter, nextOverrides, "startup");
|
|
588
|
+
const currentIndex = nextPages.findIndex((page) => page.key === currentPageKey);
|
|
589
|
+
const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
|
|
590
|
+
if (nextIndex >= nextPages.length) {
|
|
591
|
+
setOverrides(nextOverrides);
|
|
592
|
+
setViewMode("full");
|
|
593
|
+
setFocusArea("filter");
|
|
594
|
+
setPageIndex(0);
|
|
595
|
+
setFilterIndex(0);
|
|
596
|
+
setFilterQuery("");
|
|
597
|
+
setFilterCursor(0);
|
|
598
|
+
setDetailOffset(0);
|
|
599
|
+
setStatusMessage("Startup configuration is complete. Switched to the full editor.");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
setFocusArea("content");
|
|
603
|
+
setPageIndex(nextIndex);
|
|
604
|
+
setDetailOffset(0);
|
|
605
|
+
}, [currentContext, selectedFilter]);
|
|
606
|
+
const commitCurrentDraft = React.useCallback(async (value) => {
|
|
607
|
+
if (!selectedPage || !options.onCommit) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
setSaving(true);
|
|
611
|
+
setStatusMessage(`Saving ${selectedPage.entry.id}...`);
|
|
612
|
+
try {
|
|
613
|
+
const refreshedContext = await options.onCommit({
|
|
614
|
+
scope: selectedPage.scope,
|
|
615
|
+
entryId: selectedPage.entry.id,
|
|
616
|
+
value
|
|
617
|
+
});
|
|
618
|
+
setCurrentContext(refreshedContext);
|
|
619
|
+
setDrafts((current) => ({ ...current, [selectedPage.key]: value }));
|
|
620
|
+
setCursorPositions((current) => ({ ...current, [selectedPage.key]: value.length }));
|
|
621
|
+
setOverrides((current) => {
|
|
622
|
+
const next = { ...current };
|
|
623
|
+
delete next[selectedPage.key];
|
|
624
|
+
return next;
|
|
625
|
+
});
|
|
626
|
+
return true;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
setStatusMessage(error instanceof Error ? error.message : `Unable to save ${selectedPage.entry.id}.`);
|
|
629
|
+
return false;
|
|
630
|
+
} finally {
|
|
631
|
+
setSaving(false);
|
|
632
|
+
}
|
|
633
|
+
}, [options, selectedPage]);
|
|
634
|
+
const saveCurrentDraft = React.useCallback(async (advance) => {
|
|
635
|
+
if (!selectedPage || saving) {
|
|
349
636
|
return;
|
|
350
637
|
}
|
|
351
638
|
const value = nextDraftValue(selectedPage, drafts);
|
|
352
|
-
|
|
639
|
+
const committed = await commitCurrentDraft(value);
|
|
640
|
+
if (!committed) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const nextOverrides = { ...overrides, [selectedPage.key]: value };
|
|
644
|
+
setOverrides(nextOverrides);
|
|
645
|
+
setFocusArea("content");
|
|
353
646
|
setStatusMessage(`Updated ${selectedPage.entry.id}.`);
|
|
354
647
|
if (advance) {
|
|
648
|
+
if (viewMode === "startup") {
|
|
649
|
+
advanceStartupFlow(nextOverrides, selectedPage.key);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
355
652
|
setPageIndex((current) => Math.min(Math.max(0, pages.length - 1), current + 1));
|
|
356
653
|
setDetailOffset(0);
|
|
357
654
|
}
|
|
358
|
-
}, [drafts, pages.length, selectedPage]);
|
|
359
|
-
const activateAction = React.useCallback((label) => {
|
|
655
|
+
}, [advanceStartupFlow, commitCurrentDraft, drafts, overrides, pages.length, saving, selectedPage, viewMode]);
|
|
656
|
+
const activateAction = React.useCallback(async (label) => {
|
|
657
|
+
if (saving) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
360
660
|
if (label === "Back") {
|
|
661
|
+
setFocusArea("content");
|
|
361
662
|
setPageIndex((current) => Math.max(0, current - 1));
|
|
362
663
|
setDetailOffset(0);
|
|
363
664
|
return;
|
|
@@ -366,14 +667,28 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
366
667
|
const suggested = selectedPage.suggestedValue || selectedPage.finalValue;
|
|
367
668
|
setDrafts((current) => ({ ...current, [selectedPage.key]: suggested }));
|
|
368
669
|
setCursorPositions((current) => ({ ...current, [selectedPage.key]: suggested.length }));
|
|
369
|
-
|
|
370
|
-
|
|
670
|
+
const committed = await commitCurrentDraft(suggested);
|
|
671
|
+
if (!committed) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const nextOverrides = { ...overrides, [selectedPage.key]: suggested };
|
|
675
|
+
setOverrides(nextOverrides);
|
|
676
|
+
setFocusArea("content");
|
|
371
677
|
setStatusMessage(`Accepted suggested value for ${selectedPage.entry.id}.`);
|
|
678
|
+
if (viewMode === "startup") {
|
|
679
|
+
advanceStartupFlow(nextOverrides, selectedPage.key);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
setPageIndex((current) => Math.min(Math.max(0, pages.length - 1), current + 1));
|
|
372
683
|
setDetailOffset(0);
|
|
373
684
|
return;
|
|
374
685
|
}
|
|
375
686
|
if (label === "Update + Next") {
|
|
376
|
-
saveCurrentDraft(true);
|
|
687
|
+
void saveCurrentDraft(true);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (label === "Save Value") {
|
|
691
|
+
void saveCurrentDraft(false);
|
|
377
692
|
return;
|
|
378
693
|
}
|
|
379
694
|
if (label === "Clear" && selectedPage) {
|
|
@@ -383,35 +698,19 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
383
698
|
setStatusMessage(`Cleared ${selectedPage.entry.id}.`);
|
|
384
699
|
return;
|
|
385
700
|
}
|
|
386
|
-
if (label === "
|
|
387
|
-
|
|
388
|
-
setFocusArea("sidebar");
|
|
389
|
-
setStatusMessage("Switched to the advanced editor.");
|
|
390
|
-
return;
|
|
701
|
+
if (label === "Finish") {
|
|
702
|
+
finishWithOverrides(overrides);
|
|
391
703
|
}
|
|
392
|
-
|
|
393
|
-
setViewMode("startup");
|
|
394
|
-
setFocusArea("content");
|
|
395
|
-
setStatusMessage("Switched back to the startup wizard.");
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
if (label === "Open Full Editor") {
|
|
399
|
-
setViewMode("full");
|
|
400
|
-
setFocusArea("sidebar");
|
|
401
|
-
setStatusMessage("Opened the advanced editor.");
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (label === "Save and Continue" || label === "Finish") {
|
|
405
|
-
finish({
|
|
406
|
-
overrides,
|
|
407
|
-
viewMode
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
}, [overrides, pages.length, saveCurrentDraft, selectedPage, viewMode]);
|
|
704
|
+
}, [advanceStartupFlow, commitCurrentDraft, finishWithOverrides, overrides, saveCurrentDraft, saving, selectedPage, viewMode]);
|
|
411
705
|
const scrollRegions = [
|
|
412
706
|
...viewMode === "full" ? [{
|
|
413
707
|
id: "config-sidebar",
|
|
414
|
-
rect:
|
|
708
|
+
rect: {
|
|
709
|
+
x: sidebarRect.x,
|
|
710
|
+
y: sidebarRect.y + sidebarFilterHeight,
|
|
711
|
+
width: sidebarRect.width,
|
|
712
|
+
height: sidebarHeight
|
|
713
|
+
},
|
|
415
714
|
state: {
|
|
416
715
|
offset: safeSidebarOffset,
|
|
417
716
|
viewportSize: sidebarViewportSize,
|
|
@@ -445,7 +744,7 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
445
744
|
return;
|
|
446
745
|
}
|
|
447
746
|
findClickableRegion(clickRegions, event.x, event.y)?.onClick();
|
|
448
|
-
});
|
|
747
|
+
}, { enabled: options.mouseEnabled === true });
|
|
449
748
|
useInput((input, key) => {
|
|
450
749
|
if (key.ctrl && input === "c") {
|
|
451
750
|
exit();
|
|
@@ -456,36 +755,74 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
456
755
|
setFocusArea((current) => cycleFocus(current, viewMode));
|
|
457
756
|
return;
|
|
458
757
|
}
|
|
459
|
-
if (input === "
|
|
460
|
-
setViewMode((current) => current === "startup" ? "full" : "startup");
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (input === "s") {
|
|
758
|
+
if (input === "s" && focusArea !== "content" && focusArea !== "filter") {
|
|
464
759
|
if (viewMode === "startup" && selectedPage) {
|
|
465
760
|
setStatusMessage("Complete each required startup step before saving. Use Update + Next to continue.");
|
|
466
761
|
return;
|
|
467
762
|
}
|
|
468
|
-
|
|
469
|
-
overrides,
|
|
470
|
-
viewMode
|
|
471
|
-
});
|
|
763
|
+
finishWithOverrides(overrides);
|
|
472
764
|
return;
|
|
473
765
|
}
|
|
474
766
|
if (focusArea === "environment") {
|
|
767
|
+
if (viewMode === "startup") {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
475
770
|
if (key.leftArrow) {
|
|
476
771
|
setFilterIndex((current) => Math.max(0, current - 1));
|
|
477
772
|
setPageIndex(0);
|
|
478
773
|
return;
|
|
479
774
|
}
|
|
480
775
|
if (key.rightArrow) {
|
|
481
|
-
setFilterIndex((current) => Math.min(
|
|
776
|
+
setFilterIndex((current) => Math.min(FULL_CONFIG_FILTERS.length - 1, current + 1));
|
|
482
777
|
setPageIndex(0);
|
|
483
778
|
return;
|
|
484
779
|
}
|
|
485
780
|
}
|
|
486
|
-
if (focusArea === "
|
|
487
|
-
if (
|
|
488
|
-
|
|
781
|
+
if (focusArea === "filter" && viewMode === "full") {
|
|
782
|
+
if (isCtrlVPaste(input, key)) {
|
|
783
|
+
const clipboardText = readLinuxClipboardText();
|
|
784
|
+
if (!clipboardText) {
|
|
785
|
+
setStatusMessage("Ctrl+V clipboard paste is unavailable. Use right-click/menu paste or install wl-paste, xclip, or xsel.");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const next = applyConfigInputInsertion({ value: filterQuery, cursor: filterCursor }, clipboardText);
|
|
789
|
+
setFilterQuery(next.value);
|
|
790
|
+
setFilterCursor(next.cursor);
|
|
791
|
+
setStatusMessage("Filtered the variable list from the clipboard.");
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (key.home) {
|
|
795
|
+
setFilterCursor(0);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (key.end) {
|
|
799
|
+
setFilterCursor(filterQuery.length);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (key.leftArrow) {
|
|
803
|
+
setFilterCursor((current) => Math.max(0, current - 1));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (key.rightArrow) {
|
|
807
|
+
setFilterCursor((current) => Math.min(filterQuery.length, current + 1));
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (key.backspace) {
|
|
811
|
+
const next = deleteBackward(filterQuery, filterCursor);
|
|
812
|
+
setFilterQuery(next.value);
|
|
813
|
+
setFilterCursor(next.cursor);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (key.delete) {
|
|
817
|
+
const next = deleteForward(filterQuery, filterCursor);
|
|
818
|
+
setFilterQuery(next.value);
|
|
819
|
+
setFilterCursor(next.cursor);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!key.ctrl && !key.meta && input && !key.return && !key.upArrow && !key.downArrow && !key.pageDown && !key.pageUp) {
|
|
823
|
+
const next = applyConfigInputInsertion({ value: filterQuery, cursor: filterCursor }, input);
|
|
824
|
+
setFilterQuery(next.value);
|
|
825
|
+
setFilterCursor(next.cursor);
|
|
489
826
|
return;
|
|
490
827
|
}
|
|
491
828
|
}
|
|
@@ -508,6 +845,26 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
508
845
|
}
|
|
509
846
|
}
|
|
510
847
|
if (focusArea === "content") {
|
|
848
|
+
if (selectedPage && isCtrlVPaste(input, key)) {
|
|
849
|
+
const clipboardText = readLinuxClipboardText();
|
|
850
|
+
if (!clipboardText) {
|
|
851
|
+
setStatusMessage("Ctrl+V clipboard paste is unavailable. Use right-click/menu paste or install wl-paste, xclip, or xsel.");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const next = applyConfigInputInsertion({ value: draftValue, cursor: cursorPosition }, clipboardText);
|
|
855
|
+
setDrafts((current) => ({ ...current, [selectedPage.key]: next.value }));
|
|
856
|
+
setCursorPositions((current) => ({ ...current, [selectedPage.key]: next.cursor }));
|
|
857
|
+
setStatusMessage(`Pasted text into ${selectedPage.entry.id}.`);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (selectedPage && key.home) {
|
|
861
|
+
setCursorPositions((current) => ({ ...current, [selectedPage.key]: 0 }));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (selectedPage && key.end) {
|
|
865
|
+
setCursorPositions((current) => ({ ...current, [selectedPage.key]: draftValue.length }));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
511
868
|
if (selectedPage && key.leftArrow) {
|
|
512
869
|
setCursorPositions((current) => ({ ...current, [selectedPage.key]: Math.max(0, cursorPosition - 1) }));
|
|
513
870
|
return;
|
|
@@ -545,13 +902,13 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
545
902
|
return;
|
|
546
903
|
}
|
|
547
904
|
if (selectedPage && key.return && viewMode === "startup") {
|
|
548
|
-
saveCurrentDraft(true);
|
|
905
|
+
void saveCurrentDraft(true);
|
|
549
906
|
return;
|
|
550
907
|
}
|
|
551
908
|
if (!key.ctrl && !key.meta && input && selectedPage && !key.upArrow && !key.downArrow && !key.pageDown && !key.pageUp) {
|
|
552
|
-
const
|
|
553
|
-
setDrafts((current) => ({ ...current, [selectedPage.key]:
|
|
554
|
-
setCursorPositions((current) => ({ ...current, [selectedPage.key]:
|
|
909
|
+
const next = applyConfigInputInsertion({ value: draftValue, cursor: cursorPosition }, input);
|
|
910
|
+
setDrafts((current) => ({ ...current, [selectedPage.key]: next.value }));
|
|
911
|
+
setCursorPositions((current) => ({ ...current, [selectedPage.key]: next.cursor }));
|
|
555
912
|
return;
|
|
556
913
|
}
|
|
557
914
|
}
|
|
@@ -565,30 +922,39 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
565
922
|
return;
|
|
566
923
|
}
|
|
567
924
|
if (key.return) {
|
|
568
|
-
activateAction(actions[actionIndex] ?? actions[0] ?? "
|
|
925
|
+
void activateAction(actions[actionIndex] ?? actions[0] ?? "Finish");
|
|
569
926
|
}
|
|
570
927
|
}
|
|
571
928
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
929
|
+
usePaste((text) => {
|
|
930
|
+
if (focusAreaRef.current === "filter" && viewModeRef.current === "full") {
|
|
931
|
+
const next2 = applyConfigInputInsertion({ value: filterQueryRef.current, cursor: filterCursorRef.current }, text);
|
|
932
|
+
setFilterQuery(next2.value);
|
|
933
|
+
setFilterCursor(next2.cursor);
|
|
934
|
+
setStatusMessage("Filtered the variable list.");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const currentPage = selectedPageRef.current;
|
|
938
|
+
if (focusAreaRef.current !== "content" || !currentPage) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const next = applyConfigInputInsertion({ value: draftValueRef.current, cursor: cursorPositionRef.current }, text);
|
|
942
|
+
setDrafts((current) => ({ ...current, [currentPage.key]: next.value }));
|
|
943
|
+
setCursorPositions((current) => ({ ...current, [currentPage.key]: next.cursor }));
|
|
944
|
+
setStatusMessage(`Pasted text into ${currentPage.entry.id}.`);
|
|
945
|
+
}, { isActive: true });
|
|
946
|
+
if (viewMode === "full") {
|
|
947
|
+
for (const [index, item] of envRects.entries()) {
|
|
948
|
+
clickRegions.push({
|
|
949
|
+
id: `env-${item.item}`,
|
|
950
|
+
rect: item.rect,
|
|
951
|
+
onClick: () => {
|
|
952
|
+
setFilterIndex(index);
|
|
953
|
+
setPageIndex(0);
|
|
954
|
+
setFocusArea("environment");
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
592
958
|
}
|
|
593
959
|
clickRegions.push({
|
|
594
960
|
id: "detail-focus",
|
|
@@ -598,6 +964,13 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
598
964
|
}
|
|
599
965
|
});
|
|
600
966
|
if (viewMode === "full") {
|
|
967
|
+
clickRegions.push({
|
|
968
|
+
id: "filter-focus",
|
|
969
|
+
rect: filterRect,
|
|
970
|
+
onClick: () => {
|
|
971
|
+
setFocusArea("filter");
|
|
972
|
+
}
|
|
973
|
+
});
|
|
601
974
|
for (let index = 0; index < visibleSidebar.length; index += 1) {
|
|
602
975
|
const page = visibleSidebar[index];
|
|
603
976
|
if (!page) {
|
|
@@ -605,7 +978,7 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
605
978
|
}
|
|
606
979
|
clickRegions.push({
|
|
607
980
|
id: `sidebar-${page.key}`,
|
|
608
|
-
rect: { x: 1, y: layout.topBarHeight + 2 + index, width: layout.sidebarWidth - 2, height: 1 },
|
|
981
|
+
rect: { x: 1, y: layout.topBarHeight + sidebarFilterHeight + 2 + index, width: layout.sidebarWidth - 2, height: 1 },
|
|
609
982
|
onClick: () => {
|
|
610
983
|
setPageIndex(safeSidebarOffset + index);
|
|
611
984
|
setFocusArea("sidebar");
|
|
@@ -621,37 +994,30 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
621
994
|
onClick: () => {
|
|
622
995
|
setFocusArea("actions");
|
|
623
996
|
setActionIndex(index);
|
|
624
|
-
activateAction(item.label);
|
|
997
|
+
void activateAction(item.label);
|
|
625
998
|
}
|
|
626
999
|
});
|
|
627
1000
|
}
|
|
1001
|
+
const titleLine = truncateLine(
|
|
1002
|
+
`Treeseed Config ${currentContext.project.name} (${currentContext.project.slug}) GH cfg:${configReadiness.github.configured ? "ok" : "miss"} CF cfg:${configReadiness.cloudflare.configured ? "ok" : "miss"} RW cfg:${configReadiness.railway.configured ? "ok" : "miss"}`,
|
|
1003
|
+
layout.columns
|
|
1004
|
+
);
|
|
1005
|
+
const statusTail = viewMode === "full" ? `Env ${selectedFilter}` : "";
|
|
1006
|
+
const toolsLine = truncateLine(
|
|
1007
|
+
`gh:${options.toolAvailability?.githubCli?.available ? "ok" : "miss"} wr:${options.toolAvailability?.wranglerCli?.available ? "ok" : "miss"} rw:${options.toolAvailability?.railwayCli?.available ? "ok" : "miss"} act:${options.toolAvailability?.ghActExtension?.available ? "ok" : "miss"} dk:${options.toolAvailability?.dockerDaemon?.available ? "ok" : "miss"} sec:${options.secretSession?.status?.unlocked ? "on" : "off"}${statusTail ? ` ${statusTail}` : ""}`,
|
|
1008
|
+
layout.columns
|
|
1009
|
+
);
|
|
628
1010
|
const topBar = React.createElement(
|
|
629
1011
|
React.Fragment,
|
|
630
1012
|
null,
|
|
631
|
-
React.createElement(Text, { color: "cyan", bold: true },
|
|
632
|
-
React.createElement(
|
|
633
|
-
|
|
634
|
-
{ width: layout.columns },
|
|
635
|
-
React.createElement(TopTabs, {
|
|
636
|
-
width: Math.floor(layout.columns / 2) - 1,
|
|
637
|
-
title: "Env",
|
|
638
|
-
items: CONFIG_FILTERS.map((filter) => ({ id: filter, label: filter })),
|
|
639
|
-
activeId: selectedFilter,
|
|
640
|
-
focused: focusArea === "environment"
|
|
641
|
-
}),
|
|
642
|
-
React.createElement(TopTabs, {
|
|
643
|
-
width: Math.ceil(layout.columns / 2),
|
|
644
|
-
title: "View",
|
|
645
|
-
items: CONFIG_VIEW_MODES.map((mode) => ({ id: mode, label: mode === "startup" ? "wizard" : "full" })),
|
|
646
|
-
activeId: viewMode,
|
|
647
|
-
focused: focusArea === "mode"
|
|
648
|
-
})
|
|
649
|
-
)
|
|
1013
|
+
React.createElement(Text, { color: "cyan", bold: true }, titleLine),
|
|
1014
|
+
React.createElement(Text, { color: "gray" }, toolsLine),
|
|
1015
|
+
viewMode === "full" ? React.createElement(Text, { color: focusArea === "environment" ? "cyan" : "gray" }, truncateLine(`Env ${FULL_CONFIG_FILTERS.map((filter) => filter === selectedFilter ? `[${filter}]` : filter).join(" ")}`, layout.columns)) : React.createElement(Text, { color: "gray" }, truncateLine(`Wizard mode across ${currentContext.scopes.join(", ")}.`, layout.columns))
|
|
650
1016
|
);
|
|
651
1017
|
const footer = React.createElement(StatusBar, {
|
|
652
1018
|
width: layout.columns,
|
|
653
1019
|
accent: focusArea === "content",
|
|
654
|
-
primary: viewMode === "full" ?
|
|
1020
|
+
primary: viewMode === "full" ? `Tab cycles env, filter, list, editor, and actions. Type in Filter to narrow variables. Sidebar arrows${options.mouseEnabled === true ? " or wheel" : ""} change selection.` : `Type or paste to edit. Left/Right move the cursor, Home/End jump, Enter updates and advances.`,
|
|
655
1021
|
secondary: statusMessage
|
|
656
1022
|
});
|
|
657
1023
|
if (viewMode === "startup") {
|
|
@@ -661,7 +1027,7 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
661
1027
|
React.createElement(ScrollPanel, {
|
|
662
1028
|
width: layout.columns,
|
|
663
1029
|
height: layout.detailHeight,
|
|
664
|
-
title: startupStep ? `
|
|
1030
|
+
title: startupStep ? `Required Setup ${Math.max(0, startupStep.total - startupStep.index - 1)} left` : "Required Setup",
|
|
665
1031
|
lines: detailPanel.lines,
|
|
666
1032
|
focused: focusArea === "content",
|
|
667
1033
|
tone: "accent",
|
|
@@ -674,12 +1040,13 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
674
1040
|
React.createElement(TextInputField, {
|
|
675
1041
|
width: layout.columns,
|
|
676
1042
|
height: layout.inputHeight,
|
|
677
|
-
label:
|
|
1043
|
+
label: "Value",
|
|
678
1044
|
focused: focusArea === "content",
|
|
679
1045
|
value: draftValue,
|
|
680
1046
|
cursorPosition,
|
|
681
1047
|
secret: selectedPage.entry.sensitivity === "secret",
|
|
682
|
-
placeholder:
|
|
1048
|
+
placeholder: "",
|
|
1049
|
+
helperText: ""
|
|
683
1050
|
}),
|
|
684
1051
|
React.createElement(
|
|
685
1052
|
Box,
|
|
@@ -697,31 +1064,45 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
697
1064
|
) : React.createElement(EmptyState, {
|
|
698
1065
|
width: layout.columns,
|
|
699
1066
|
height: layout.bodyHeight,
|
|
700
|
-
title: "
|
|
701
|
-
message: "
|
|
1067
|
+
title: "Required Setup Complete",
|
|
1068
|
+
message: "The required setup flow is complete."
|
|
702
1069
|
});
|
|
703
1070
|
return React.createElement(AppFrame, { layout, topBar, body: body2, footer });
|
|
704
1071
|
}
|
|
705
1072
|
const body = React.createElement(
|
|
706
1073
|
Box,
|
|
707
1074
|
{ width: layout.columns, height: layout.bodyHeight, overflow: "hidden" },
|
|
708
|
-
React.createElement(
|
|
709
|
-
|
|
710
|
-
height: layout.bodyHeight,
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1075
|
+
React.createElement(
|
|
1076
|
+
Box,
|
|
1077
|
+
{ flexDirection: "column", width: layout.sidebarWidth, height: layout.bodyHeight, overflow: "hidden" },
|
|
1078
|
+
React.createElement(TextInputField, {
|
|
1079
|
+
width: layout.sidebarWidth,
|
|
1080
|
+
height: sidebarFilterHeight,
|
|
1081
|
+
label: "Filter",
|
|
1082
|
+
focused: focusArea === "filter",
|
|
1083
|
+
value: filterQuery,
|
|
1084
|
+
cursorPosition: filterCursor,
|
|
1085
|
+
placeholder: "id, label, group, cluster",
|
|
1086
|
+
helperText: "Type to narrow by id, label, group, or cluster."
|
|
1087
|
+
}),
|
|
1088
|
+
React.createElement(SidebarList, {
|
|
1089
|
+
width: layout.sidebarWidth,
|
|
1090
|
+
height: sidebarHeight,
|
|
1091
|
+
title: filterQuery ? `Variables (${pages.length})` : "Variables",
|
|
1092
|
+
focused: focusArea === "sidebar",
|
|
1093
|
+
scrollState: {
|
|
1094
|
+
offset: safeSidebarOffset,
|
|
1095
|
+
viewportSize: sidebarViewportSize,
|
|
1096
|
+
totalSize: pages.length
|
|
1097
|
+
},
|
|
1098
|
+
items: visibleSidebar.map((page, index) => ({
|
|
1099
|
+
id: page.key,
|
|
1100
|
+
label: page.entry.id,
|
|
1101
|
+
active: safeSidebarOffset + index === safePageIndex,
|
|
1102
|
+
tone: page.required ? "required" : "normal"
|
|
1103
|
+
}))
|
|
1104
|
+
})
|
|
1105
|
+
),
|
|
725
1106
|
React.createElement(Text, null, " "),
|
|
726
1107
|
React.createElement(
|
|
727
1108
|
Box,
|
|
@@ -745,12 +1126,13 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
745
1126
|
React.createElement(TextInputField, {
|
|
746
1127
|
width: layout.contentWidth,
|
|
747
1128
|
height: layout.inputHeight,
|
|
748
|
-
label: "
|
|
1129
|
+
label: "Value",
|
|
749
1130
|
focused: focusArea === "content",
|
|
750
1131
|
value: draftValue,
|
|
751
1132
|
cursorPosition,
|
|
752
1133
|
secret: selectedPage.entry.sensitivity === "secret",
|
|
753
|
-
placeholder:
|
|
1134
|
+
placeholder: "",
|
|
1135
|
+
helperText: ""
|
|
754
1136
|
}),
|
|
755
1137
|
React.createElement(
|
|
756
1138
|
Box,
|
|
@@ -779,7 +1161,11 @@ async function runCliConfigEditor(context, options = {}) {
|
|
|
779
1161
|
});
|
|
780
1162
|
}
|
|
781
1163
|
export {
|
|
1164
|
+
applyConfigInputInsertion,
|
|
782
1165
|
buildCliConfigPages,
|
|
783
1166
|
computeConfigViewportLayout,
|
|
1167
|
+
filterCliConfigPages,
|
|
1168
|
+
normalizeConfigInputChunk,
|
|
1169
|
+
readLinuxClipboardText,
|
|
784
1170
|
runCliConfigEditor
|
|
785
1171
|
};
|