sitevision-cli 1.0.0-beta.8 → 1.0.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +194 -70
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +95 -212
  7. package/dist/commands/dev.js +87 -416
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +50 -33
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +5 -31
  13. package/dist/components/AnimatedLogo.js +8 -2
  14. package/dist/components/AuthLoginScreen.js +33 -15
  15. package/dist/components/PasswordInput.js +2 -1
  16. package/dist/scaffold/inquirer-stub.js +119 -0
  17. package/dist/scaffold/register.js +15 -0
  18. package/dist/shell/AddonPicker.js +54 -0
  19. package/dist/shell/Changelog.js +44 -0
  20. package/dist/shell/CommandPalette.js +63 -0
  21. package/dist/shell/ConfigForm.js +703 -0
  22. package/dist/shell/Frame.js +138 -0
  23. package/dist/shell/Help.js +96 -0
  24. package/dist/shell/Popover.js +15 -0
  25. package/dist/shell/Settings.js +149 -0
  26. package/dist/shell/Shell.js +953 -0
  27. package/dist/shell/Tabs.js +96 -0
  28. package/dist/shell/actions.js +493 -0
  29. package/dist/utils/config.js +62 -2
  30. package/dist/utils/create-app.js +231 -0
  31. package/dist/utils/debug.js +32 -0
  32. package/dist/utils/environments.js +74 -0
  33. package/dist/utils/i18n.js +369 -0
  34. package/dist/utils/jsonc.js +2 -1
  35. package/dist/utils/keychain.js +54 -6
  36. package/dist/utils/oauth2-auth.js +146 -43
  37. package/dist/utils/password-prompt.js +37 -3
  38. package/dist/utils/process-runner.js +77 -38
  39. package/dist/utils/project-detection.js +457 -112
  40. package/dist/utils/session-cookie-auth.js +10 -6
  41. package/dist/utils/sitevision-api.js +162 -32
  42. package/dist/utils/sitevision-scripts-runner.js +50 -22
  43. package/dist/utils/tasks.js +510 -0
  44. package/dist/utils/version-check.js +3 -3
  45. package/dist/utils/webpack-runner.js +9 -5
  46. package/dist/utils/workspace.js +105 -0
  47. package/dist/utils/zip.js +18 -19
  48. package/package.json +27 -4
  49. package/readme.md +113 -141
  50. package/dist/app.d.ts +0 -6
  51. package/dist/app.js +0 -307
  52. package/dist/cli.d.ts +0 -2
  53. package/dist/commands/build.d.ts +0 -12
  54. package/dist/commands/deploy.d.ts +0 -16
  55. package/dist/commands/dev.d.ts +0 -16
  56. package/dist/commands/index.d.ts +0 -4
  57. package/dist/commands/info.d.ts +0 -2
  58. package/dist/commands/setup-signing.d.ts +0 -2
  59. package/dist/commands/sign.d.ts +0 -14
  60. package/dist/commands/types.d.ts +0 -18
  61. package/dist/commands/watch.d.ts +0 -2
  62. package/dist/components/AnimatedLogo.d.ts +0 -10
  63. package/dist/components/AuthLoginScreen.d.ts +0 -21
  64. package/dist/components/DevPropertiesForm.d.ts +0 -10
  65. package/dist/components/DevPropertiesForm.js +0 -210
  66. package/dist/components/InfoScreen.d.ts +0 -7
  67. package/dist/components/InfoScreen.js +0 -14
  68. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  69. package/dist/components/KeychainPasswordChoice.js +0 -30
  70. package/dist/components/MainMenu.d.ts +0 -7
  71. package/dist/components/MainMenu.js +0 -83
  72. package/dist/components/PasswordInput.d.ts +0 -10
  73. package/dist/components/ProcessOutput.d.ts +0 -6
  74. package/dist/components/ProcessOutput.js +0 -30
  75. package/dist/components/SetupFlow.d.ts +0 -8
  76. package/dist/components/SetupFlow.js +0 -233
  77. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  78. package/dist/components/SigningPropertiesForm.js +0 -38
  79. package/dist/components/StatusIndicator.d.ts +0 -8
  80. package/dist/components/TextInput.d.ts +0 -10
  81. package/dist/components/TextInput.js +0 -29
  82. package/dist/components/WelcomeScreen.d.ts +0 -14
  83. package/dist/types/index.d.ts +0 -284
  84. package/dist/utils/branding.d.ts +0 -28
  85. package/dist/utils/config.d.ts +0 -17
  86. package/dist/utils/jsonc.d.ts +0 -19
  87. package/dist/utils/keychain.d.ts +0 -15
  88. package/dist/utils/oauth2-auth.d.ts +0 -27
  89. package/dist/utils/password-prompt.d.ts +0 -14
  90. package/dist/utils/process-runner.d.ts +0 -22
  91. package/dist/utils/project-detection.d.ts +0 -151
  92. package/dist/utils/session-cookie-auth.d.ts +0 -34
  93. package/dist/utils/signing-password.d.ts +0 -12
  94. package/dist/utils/signing-step.d.ts +0 -15
  95. package/dist/utils/signing-step.js +0 -16
  96. package/dist/utils/sitevision-api.d.ts +0 -112
  97. package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
  98. package/dist/utils/version-check.d.ts +0 -9
  99. package/dist/utils/webpack-runner.d.ts +0 -76
  100. package/dist/utils/zip.d.ts +0 -71
@@ -0,0 +1,953 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
5
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
6
+ import Spinner from 'ink-spinner';
7
+ import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, syncDevPropertiesToPackageJson, } from '../utils/project-detection.js';
8
+ import { appGroup, configIncomplete, discoverApps, needsOnboarding, } from '../utils/workspace.js';
9
+ import { createAppInTerminal, isEmptyOrMissing, seedNewApp, startCreateApp, appNameAdvice, appNameProblem, } from '../utils/create-app.js';
10
+ import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
11
+ import { useTasks, runningTasks, startActivate, getTasks, scaffoldRunning, } from '../utils/tasks.js';
12
+ import { PasswordInput } from '../components/PasswordInput.js';
13
+ import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
14
+ import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
15
+ import { TabBar, TABS, Overview, Versions, Log, } from './Tabs.js';
16
+ import { CommandPalette } from './CommandPalette.js';
17
+ import { ConfigForm } from './ConfigForm.js';
18
+ import { AddonPicker } from './AddonPicker.js';
19
+ import { SettingsScreen } from './Settings.js';
20
+ import { HelpPanel } from './Help.js';
21
+ import { Popover } from './Popover.js';
22
+ import { ChangelogPanel } from './Changelog.js';
23
+ import { baseEnvironment, environmentColor, environmentNames, environmentProject, isProductionEnvironment, resolveEnvironment, } from '../utils/environments.js';
24
+ import { t } from '../utils/i18n.js';
25
+ import { onKeychainSaveFailed } from '../utils/keychain.js';
26
+ import { actionForKey, actions, authState, resolveDeployConfig, } from './actions.js';
27
+ import { debug } from '../utils/debug.js';
28
+ // Opening a view is not an action worth a debug line.
29
+ const NAVIGATION = new Set([
30
+ 'versions',
31
+ 'help',
32
+ 'changelog',
33
+ 'settings',
34
+ 'config',
35
+ 'workspace-settings',
36
+ ]);
37
+ function useSize() {
38
+ const { stdout } = useStdout();
39
+ const read = () => ({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
40
+ const [size, setSize] = useState(read);
41
+ useEffect(() => {
42
+ const onResize = () => setSize(read());
43
+ stdout.on('resize', onResize);
44
+ return () => {
45
+ stdout.off('resize', onResize);
46
+ };
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [stdout]);
49
+ return size;
50
+ }
51
+ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, handover, skipped, }) {
52
+ const { exit } = useApp();
53
+ const { columns, rows } = useSize();
54
+ const tasks = useTasks();
55
+ const [apps, setApps] = useState(initialApps);
56
+ // A workspace with nothing to deploy against opens on its settings form.
57
+ const onboard = useMemo(() => Boolean(workspaceRoot) && needsOnboarding(workspaceRoot, initialApps), [workspaceRoot, initialApps]);
58
+ const [selected, setSelected] = useState(onboard ? initialApps.length : 0);
59
+ const [tab, setTab] = useState('overview');
60
+ const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
61
+ const [overlays, setOverlays] = useState(updatedFrom ? [{ kind: 'changelog', since: updatedFrom }] : []);
62
+ const setOverlay = useCallback((next) => {
63
+ setOverlays(stack => overlayStack(stack, next));
64
+ }, []);
65
+ const [editing, setEditing] = useState(false);
66
+ // A confirm that turns up mid-edit stays hidden until the field is done.
67
+ const top = overlays.at(-1) ?? null;
68
+ const overlay = top?.kind === 'confirm' && editing ? null : top;
69
+ const [filter, setFilter] = useState('');
70
+ const [versions, setVersions] = useState({});
71
+ const [versionRow, setVersionRow] = useState(0);
72
+ const [logScroll, setLogScroll] = useState(0);
73
+ const [logWrap, setLogWrap] = useState(false);
74
+ const [offerAddon, setOfferAddon] = useState();
75
+ const [notice, setNotice] = useState(skipped?.length
76
+ ? {
77
+ text: skipped.length === 1
78
+ ? t('Skipped: {reason}', { reason: skipped[0] })
79
+ : t('Skipped {n} apps. First: {reason}', {
80
+ n: skipped.length,
81
+ reason: skipped[0],
82
+ }),
83
+ level: 'warn',
84
+ }
85
+ : null);
86
+ const [, tick] = useReducer((n) => n + 1, 0);
87
+ // In workspace mode the row after the last app is "Workspace settings".
88
+ const settings = Boolean(workspaceRoot) && selected === apps.length;
89
+ const rawProject = apps[Math.min(selected, apps.length - 1)];
90
+ // Active environment, remembered per workspace (or app) in .svcconfig.
91
+ const configRoot = workspaceRoot ?? rawProject.root;
92
+ const [envChoice, setEnvChoice] = useState(() => readSvcConfig(configRoot).environment ?? '');
93
+ const envNames = environmentNames(rawProject.devProperties);
94
+ const envList = envNames.join(',');
95
+ const env = envNames.includes(envChoice)
96
+ ? envChoice
97
+ : baseEnvironment(rawProject.devProperties);
98
+ const project = useMemo(() => environmentProject(rawProject, env), [rawProject, env]);
99
+ const isProduction = isProductionEnvironment(env, rawProject.devProperties);
100
+ const versionsKey = `${project.root}|${env}`;
101
+ const single = !workspaceRoot;
102
+ const workspaceTarget = useMemo(() => workspaceRoot
103
+ ? {
104
+ root: workspaceRoot,
105
+ base: readWorkspaceDevProperties(workspaceRoot),
106
+ devProperties: resolveEnvironment(readWorkspaceDevProperties(workspaceRoot), env),
107
+ environment: env,
108
+ workspace: true,
109
+ }
110
+ : undefined,
111
+ // Re-read after any reload so saved values show up.
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ [workspaceRoot, apps, env]);
114
+ const narrow = minimal || columns < NARROW_BELOW;
115
+ const sidebar = navWidth(columns);
116
+ // The navigator shows the fuzzy matches; `selected` stays an index into
117
+ // `apps` (with `apps.length` meaning the workspace settings row).
118
+ const matches = useMemo(() => navMatches(apps, filter), [apps, filter]);
119
+ const ring = single ? matches : [...matches, apps.length];
120
+ const running = runningTasks();
121
+ // Re-render once a second while something runs so elapsed times move.
122
+ useEffect(() => {
123
+ if (running.length === 0)
124
+ return;
125
+ const timer = setInterval(tick, 1000);
126
+ return () => clearInterval(timer);
127
+ }, [running.length]);
128
+ const reload = useCallback(() => {
129
+ setApps(current => current.map(app => {
130
+ try {
131
+ return detectProject(app.root) ?? app;
132
+ }
133
+ catch {
134
+ return app;
135
+ }
136
+ }));
137
+ }, []);
138
+ // A finished npm install changes what detection sees (node_modules,
139
+ // sitevision-scripts); re-detect so the status strip flips.
140
+ const installsDone = tasks.filter(task => task.kind === 'install' && task.status !== 'running').length;
141
+ useEffect(() => {
142
+ if (installsDone > 0)
143
+ reload();
144
+ }, [installsDone, reload]);
145
+ const notify = useCallback((text, level = 'info') => {
146
+ debug('notice', `${level} ${text}`);
147
+ setNotice({ text, level });
148
+ }, []);
149
+ // "Save to keychain" that did not save would otherwise just ask again.
150
+ useEffect(() => {
151
+ onKeychainSaveFailed(() => {
152
+ notify(t('Could not save to the OS keychain; you will be asked again next time.'), 'warn');
153
+ });
154
+ }, [notify]);
155
+ const quit = useCallback(() => {
156
+ const leave = () => {
157
+ for (const task of getTasks())
158
+ if (task.status === 'running')
159
+ task.stop();
160
+ exit();
161
+ };
162
+ // Between the scaffolder's questions nothing is open, so q is live:
163
+ // quitting then would leave a half-made app behind without a word.
164
+ if (scaffoldRunning()) {
165
+ setOverlay({
166
+ kind: 'confirm',
167
+ message: t('A new app is still being created. Stop it and quit?'),
168
+ resolve(yes) {
169
+ if (yes)
170
+ leave();
171
+ },
172
+ });
173
+ }
174
+ else {
175
+ leave();
176
+ }
177
+ }, [exit, setOverlay]);
178
+ const context = useMemo(() => ({
179
+ project,
180
+ reload,
181
+ notify,
182
+ quit,
183
+ setTab(next) {
184
+ setTab(next);
185
+ setFocus('content');
186
+ },
187
+ openSettings() {
188
+ setOverlay({ kind: 'settings' });
189
+ },
190
+ openChangelog() {
191
+ setOverlay({ kind: 'changelog' });
192
+ },
193
+ environment: env,
194
+ isProduction,
195
+ cycleEnvironment() {
196
+ const next = envNames[(envNames.indexOf(env) + 1) % envNames.length];
197
+ setEnvChoice(next);
198
+ writeSvcConfig(configRoot, { environment: next });
199
+ notify(t('switched to {env}', { env: next }));
200
+ },
201
+ async addEnvironment() {
202
+ const name = await new Promise(resolve => {
203
+ setOverlay({
204
+ kind: 'prompt',
205
+ label: t('Environment name (e.g. test, prod)'),
206
+ resolve,
207
+ });
208
+ });
209
+ const clean = name
210
+ ?.trim()
211
+ .toLowerCase()
212
+ .replaceAll(/[^\d\-a-z]/g, '');
213
+ if (!clean || clean === baseEnvironment(rawProject.devProperties))
214
+ return;
215
+ const targetRoot = workspaceRoot ?? rawProject.root;
216
+ const base = (workspaceRoot
217
+ ? readWorkspaceDevProperties(workspaceRoot)
218
+ : rawProject.devProperties);
219
+ if (!base)
220
+ return;
221
+ writeDevProperties(targetRoot, { ...base, environments: { ...base.environments, [clean]: {} } }, { complete: !workspaceRoot });
222
+ reload();
223
+ setEnvChoice(clean);
224
+ writeSvcConfig(configRoot, { environment: clean });
225
+ notify(t('environment {env} added', { env: clean }), 'ok');
226
+ },
227
+ async createApp() {
228
+ // Only what this flow opened is closed when the scaffolder ends.
229
+ const asked = new Set();
230
+ const openOwn = (next) => {
231
+ asked.add(next);
232
+ setOverlay(next);
233
+ };
234
+ if (scaffoldRunning()) {
235
+ notify(t('An app is already being created.'), 'warn');
236
+ return;
237
+ }
238
+ const promptText = async (label, initial, validate, error, advise) => new Promise(resolve => {
239
+ openOwn({
240
+ kind: 'prompt',
241
+ label,
242
+ initial,
243
+ validate,
244
+ advise,
245
+ error,
246
+ resolve,
247
+ });
248
+ });
249
+ const base = workspaceRoot ?? path.dirname(rawProject.root);
250
+ const suggested = workspaceRoot
251
+ ? path.relative(workspaceRoot, path.dirname(rawProject.root)) || '.'
252
+ : base;
253
+ // Asked again, with the reason, until the name is free in that folder.
254
+ let name = '';
255
+ let parentDir = '';
256
+ let appDir = '';
257
+ let taken;
258
+ for (;;) {
259
+ // eslint-disable-next-line no-await-in-loop
260
+ const typed = await promptText(t('Name of the new app'), name, appNameProblem, taken, appNameAdvice);
261
+ if (typed === null)
262
+ return;
263
+ name = typed.trim();
264
+ // Folders that already hold apps, then a way to type another one.
265
+ const folders = [
266
+ ...new Set([
267
+ suggested,
268
+ ...apps.map(app => workspaceRoot
269
+ ? path.relative(base, path.dirname(app.root)) || '.'
270
+ : path.dirname(app.root)),
271
+ ]),
272
+ ].toSorted((a, b) => a.localeCompare(b));
273
+ // eslint-disable-next-line no-await-in-loop
274
+ const folderPick = await new Promise(resolve => {
275
+ openOwn({
276
+ kind: 'choice',
277
+ label: t('Create it in folder'),
278
+ choices: [...folders, t('Other folder…')],
279
+ multi: false,
280
+ initial: [folders.indexOf(suggested)],
281
+ resolve,
282
+ });
283
+ });
284
+ if (!folderPick)
285
+ return;
286
+ const folder = folders[folderPick[0]] ??
287
+ // eslint-disable-next-line no-await-in-loop
288
+ (await promptText(t('Create it in folder'), suggested));
289
+ if (folder === null)
290
+ return;
291
+ parentDir = path.resolve(base, folder.trim() || '.');
292
+ appDir = path.join(parentDir, name);
293
+ if (isEmptyOrMissing(appDir))
294
+ break;
295
+ taken = t('{dir} already exists. Pick another name, or keep it and choose a different folder next.', { dir: path.relative(base, appDir) });
296
+ }
297
+ // Both were checked above, so a run that leaves no app may remove them.
298
+ const madeParent = !fs.existsSync(parentDir);
299
+ const discard = () => {
300
+ fs.rmSync(appDir, { recursive: true, force: true });
301
+ if (madeParent && isEmptyOrMissing(parentDir))
302
+ fs.rmSync(parentDir, { recursive: true, force: true });
303
+ };
304
+ fs.mkdirSync(parentDir, { recursive: true });
305
+ const known = (workspaceRoot
306
+ ? readWorkspaceDevProperties(workspaceRoot)
307
+ : rawProject.devProperties);
308
+ const ask = async (question) => {
309
+ // svc keeps passwords in the keychain, never in the app's file.
310
+ if (question.name === 'password')
311
+ return { skip: true };
312
+ const shared = known?.[question.name];
313
+ if (!question.error &&
314
+ ['domain', 'siteName', 'username', 'useHTTPForDevDeploy'].includes(question.name) &&
315
+ shared !== undefined &&
316
+ shared !== '')
317
+ return { value: shared };
318
+ const label = question.error
319
+ ? `${question.message} (${question.error})`
320
+ : question.message;
321
+ if (question.type === 'confirm') {
322
+ const value = await new Promise(resolve => {
323
+ openOwn({ kind: 'confirm', message: label, resolve });
324
+ });
325
+ return { value };
326
+ }
327
+ if (question.type === 'password') {
328
+ const secret = await new Promise(resolve => {
329
+ openOwn({ kind: 'password', label, resolve });
330
+ });
331
+ return secret && { value: secret.password };
332
+ }
333
+ if (question.choices.length > 0) {
334
+ const multi = question.type === 'checkbox';
335
+ const picked = await new Promise(resolve => {
336
+ openOwn({
337
+ kind: 'choice',
338
+ label,
339
+ choices: question.choices,
340
+ multi,
341
+ initial: [question.default ?? []].flat(),
342
+ resolve,
343
+ });
344
+ });
345
+ return picked && { value: multi ? picked : picked[0] };
346
+ }
347
+ const initial = question.default ?? (question.name === 'addonName' ? name : '');
348
+ const value = await promptText(label, typeof initial === 'string' ? initial : JSON.stringify(initial));
349
+ return value === null ? null : { value };
350
+ };
351
+ notify(t('creating {app}: installing, questions follow', { app: name }));
352
+ setTab('log');
353
+ const { done } = startCreateApp({ name, parentDir, ask });
354
+ let outcome = await done;
355
+ setOverlays(stack => withoutOverlays(stack, asked));
356
+ if (outcome === 'unmanaged') {
357
+ if (!handover) {
358
+ notify(t('The scaffolder could not be run from here'), 'error');
359
+ return;
360
+ }
361
+ fs.rmSync(appDir, { recursive: true, force: true });
362
+ handover(async () => {
363
+ if (await createAppInTerminal(name, parentDir))
364
+ seedNewApp(appDir, workspaceRoot);
365
+ else
366
+ discard();
367
+ });
368
+ return;
369
+ }
370
+ if (outcome !== 'created') {
371
+ discard();
372
+ if (outcome === 'failed')
373
+ notify(t('creating {app} failed, see the log', { app: name }), 'error');
374
+ return;
375
+ }
376
+ try {
377
+ seedNewApp(appDir, workspaceRoot);
378
+ }
379
+ catch (error) {
380
+ notify(error instanceof Error ? error.message : String(error), 'warn');
381
+ outcome = 'failed';
382
+ }
383
+ if (!workspaceRoot ||
384
+ path.relative(workspaceRoot, appDir).startsWith('..')) {
385
+ notify(t('{app} created in {dir}', { app: name, dir: appDir }), 'ok');
386
+ return;
387
+ }
388
+ const left = [];
389
+ const found = discoverApps(workspaceRoot, left);
390
+ if (left[0])
391
+ notify(t('Skipped: {reason}', { reason: left[0] }), 'warn');
392
+ setApps(found);
393
+ setFilter('');
394
+ setSelected(Math.max(0, found.findIndex(app => app.root === appDir)));
395
+ if (outcome === 'created')
396
+ notify(t('{app} created', { app: name }), 'ok');
397
+ setOfferAddon(appDir);
398
+ },
399
+ openWorkspaceSettings: workspaceRoot
400
+ ? () => {
401
+ setSelected(apps.length);
402
+ setFocus('content');
403
+ }
404
+ : undefined,
405
+ askPassword: (label, rememberLabel) => new Promise(resolve => {
406
+ setOverlay({ kind: 'password', label, rememberLabel, resolve });
407
+ }),
408
+ login: method => new Promise(resolve => {
409
+ setOverlay({
410
+ kind: 'login',
411
+ method,
412
+ devProperties: project.devProperties,
413
+ resolve,
414
+ });
415
+ }),
416
+ confirm: message => new Promise(resolve => {
417
+ setOverlay({ kind: 'confirm', message, resolve });
418
+ }),
419
+ openHelp() {
420
+ setOverlay({ kind: 'help' });
421
+ },
422
+ }),
423
+ // eslint-disable-next-line react-hooks/exhaustive-deps
424
+ [project, reload, notify, quit, workspaceRoot, apps.length, env, envList]);
425
+ const run = useCallback((action) => {
426
+ setOverlay(null);
427
+ if (action.enabled && !action.enabled(project)) {
428
+ notify(`${t(action.label)}: ${action.detail?.(project) ?? t('not available')}`, 'warn');
429
+ return;
430
+ }
431
+ if (!NAVIGATION.has(action.id))
432
+ debug('action', action.id);
433
+ action.run(context).catch((error) => {
434
+ notify(error instanceof Error ? error.message : String(error), 'error');
435
+ });
436
+ }, [context, project, notify, setOverlay]);
437
+ const fetchVersions = useCallback(async (fresh = false) => {
438
+ const key = versionsKey;
439
+ const config = await resolveDeployConfig(context, fresh);
440
+ if (!config)
441
+ return;
442
+ setVersions(v => ({
443
+ ...v,
444
+ [key]: { ...v[key], loading: true, error: undefined },
445
+ }));
446
+ const result = await listExecutables(config);
447
+ setVersions(v => ({
448
+ ...v,
449
+ [key]: {
450
+ loading: false,
451
+ executables: result.executables ?? v[key]?.executables,
452
+ error: result.error,
453
+ fetchedAt: result.success ? Date.now() : v[key]?.fetchedAt,
454
+ },
455
+ }));
456
+ setVersionRow(0);
457
+ }, [versionsKey, context]);
458
+ const activateSelected = useCallback(async () => {
459
+ const executable = versions[versionsKey]?.executables?.[versionRow];
460
+ if (!executable)
461
+ return;
462
+ if (executable.active) {
463
+ notify(t('{v} is already active', { v: executable.appVersion }));
464
+ return;
465
+ }
466
+ const config = await resolveDeployConfig(context);
467
+ if (!config)
468
+ return;
469
+ const task = startActivate(project, config, executable.id, executable.appVersion);
470
+ const wait = () => new Promise(resolve => {
471
+ const check = () => task.status === 'running' ? setTimeout(check, 200) : resolve();
472
+ check();
473
+ });
474
+ await wait();
475
+ notify(task.error ?? t('{v} activated', { v: executable.appVersion }), task.error ? 'error' : 'ok');
476
+ await fetchVersions();
477
+ }, [
478
+ versions,
479
+ versionsKey,
480
+ project,
481
+ versionRow,
482
+ context,
483
+ notify,
484
+ fetchVersions,
485
+ ]);
486
+ // A popover owns the keyboard; the form stays mounted underneath it.
487
+ const formActive = (tab === 'config' || settings) && focus === 'content' && !overlay;
488
+ const pickAddon = useCallback(async () => new Promise(resolve => {
489
+ setOverlay({ kind: 'picker', resolve });
490
+ }), [setOverlay]);
491
+ // A freshly created app: once it is the selected project, offer its addon.
492
+ useEffect(() => {
493
+ if (!offerAddon || project.root !== offerAddon || overlay)
494
+ return;
495
+ setOfferAddon(undefined);
496
+ const addon = project.devProperties?.addonName;
497
+ const action = actions.find(entry => entry.id === 'create-addon');
498
+ if (!addon || !action || !project.devProperties?.domain)
499
+ return;
500
+ void context
501
+ .confirm(t('Create the addon "{addon}" on {domain} now?', {
502
+ addon,
503
+ domain: project.devProperties.domain,
504
+ }))
505
+ .then(yes => {
506
+ if (yes)
507
+ run(action);
508
+ });
509
+ }, [offerAddon, project, overlay, context, run]);
510
+ const loadAddons = useCallback(async () => {
511
+ const config = await resolveDeployConfig(context, false, { addon: false });
512
+ return config ? listAddons(config) : { error: t('No credentials.') };
513
+ }, [context]);
514
+ const appTasks = tasks.filter(task => task.appRoot === project.root);
515
+ // A new app is not in the list while it is being created, so its log shows
516
+ // wherever you are; a failed one stays up until another task runs.
517
+ const creating = tasks.findLast(task => task.kind === 'create');
518
+ const logTask = appTasks.find(task => task.status === 'running') ??
519
+ (creating?.status === 'running' ||
520
+ (creating?.status === 'error' && creating === tasks.at(-1))
521
+ ? creating
522
+ : appTasks.at(-1));
523
+ useInput((raw, key) => {
524
+ // Terminals speaking the kitty keyboard protocol report shift+s as
525
+ // "s" plus a shift flag; fold that back into the uppercase letter so
526
+ // P and K behave the same everywhere.
527
+ const input = key.shift && raw.length === 1 && /[a-z]/.test(raw)
528
+ ? raw.toUpperCase()
529
+ : raw;
530
+ if (key.escape) {
531
+ if (focus === 'nav' && filter) {
532
+ setFilter('');
533
+ return;
534
+ }
535
+ // Esc backs out of the workspace settings pane, not just its focus.
536
+ if (settings)
537
+ setSelected(0);
538
+ setFocus(single ? 'content' : 'nav');
539
+ return;
540
+ }
541
+ // Ctrl+D is not d: only plain keys are shortcuts.
542
+ if (key.ctrl || key.meta)
543
+ return;
544
+ if (input === '/') {
545
+ setOverlay({ kind: 'palette' });
546
+ return;
547
+ }
548
+ if (input === '?') {
549
+ setOverlay({ kind: 'help' });
550
+ return;
551
+ }
552
+ // Reachable from the navigator's search and the workspace form too.
553
+ if (input === ',') {
554
+ setOverlay({ kind: 'settings' });
555
+ return;
556
+ }
557
+ // The config form uses Tab/Shift+Tab to move between fields.
558
+ if (key.tab && !formActive) {
559
+ setFilter('');
560
+ setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
561
+ return;
562
+ }
563
+ // Navigator: typing searches, so no action key fires until Enter has
564
+ // moved the focus into the content pane.
565
+ if (focus === 'nav') {
566
+ const move = (delta) => {
567
+ setSelected(navMove(ring, selected, delta));
568
+ };
569
+ const search = (next) => {
570
+ setFilter(next);
571
+ const found = navMatches(apps, next);
572
+ if (found.length > 0 && !found.includes(selected))
573
+ setSelected(found[0]);
574
+ };
575
+ if (key.upArrow)
576
+ move(-1);
577
+ else if (key.downArrow)
578
+ move(1);
579
+ else if (key.return) {
580
+ if (matches.length > 0 || settings) {
581
+ setFilter('');
582
+ setFocus('content');
583
+ }
584
+ }
585
+ else if (key.backspace || key.delete)
586
+ search(filter.slice(0, -1));
587
+ else if (input === 'q' && !filter)
588
+ quit();
589
+ else if (input?.length === 1 && input >= ' ' && !key.ctrl && !key.meta)
590
+ search(filter + input);
591
+ return;
592
+ }
593
+ if (settings && focus === 'content') {
594
+ // Settings pane: the form owns everything but q, y and Tab/Esc above.
595
+ if (input === 'q')
596
+ quit();
597
+ else if (input === 'y') {
598
+ try {
599
+ if (syncDevPropertiesToPackageJson(workspaceRoot)) {
600
+ reload();
601
+ notify(t('package.json updated'), 'ok');
602
+ }
603
+ }
604
+ catch (error) {
605
+ notify(error instanceof Error ? error.message : String(error), 'error');
606
+ }
607
+ }
608
+ return;
609
+ }
610
+ if (input === 'a' && tab !== 'versions') {
611
+ setTab('versions');
612
+ setFocus('content');
613
+ if (!Object.hasOwn(versions, versionsKey))
614
+ void fetchVersions(false);
615
+ return;
616
+ }
617
+ const digit = Number.parseInt(input, 10);
618
+ if (digit >= 1 && digit <= TABS.length) {
619
+ setTab(TABS[digit - 1].id);
620
+ setFocus('content');
621
+ return;
622
+ }
623
+ if (key.leftArrow || key.rightArrow) {
624
+ const i = TABS.findIndex(entry => entry.id === tab);
625
+ setTab(TABS[(i + (key.rightArrow ? 1 : TABS.length - 1)) % TABS.length].id);
626
+ return;
627
+ }
628
+ if (tab === 'versions') {
629
+ const count = versions[versionsKey]?.executables?.length ?? 0;
630
+ if (key.upArrow)
631
+ setVersionRow(r => Math.max(0, r - 1));
632
+ if (key.downArrow)
633
+ setVersionRow(r => Math.min(Math.max(0, count - 1), r + 1));
634
+ if (input === 'r')
635
+ void fetchVersions(false);
636
+ else if (input === 'a' && count > 0)
637
+ void activateSelected();
638
+ }
639
+ else if (tab === 'log') {
640
+ const max = Math.max(0, (logTask?.lines.length ?? 0) - 1);
641
+ if (key.upArrow)
642
+ setLogScroll(s => Math.min(max, s + 1));
643
+ if (key.downArrow)
644
+ setLogScroll(s => Math.max(0, s - 1));
645
+ if (key.pageUp)
646
+ setLogScroll(s => Math.min(max, s + 10));
647
+ if (key.pageDown)
648
+ setLogScroll(s => Math.max(0, s - 10));
649
+ if (input === 'f')
650
+ setLogScroll(0);
651
+ else if (input === 'x')
652
+ setLogWrap(w => !w);
653
+ }
654
+ const action = actionForKey(input);
655
+ if (action)
656
+ run(action);
657
+ }, { isActive: overlay === null && !editing });
658
+ // Frame geometry: one row for Ink's trailing newline, top bar, bottom bar.
659
+ // The sidebar layout needs 10 rows to stack its own fixed rows without
660
+ // overflowing; the compact one has no sidebar and fits in 6.
661
+ const frameRows = Math.max(narrow ? 6 : 10, rows - 1);
662
+ const mainHeight = frameRows - 2;
663
+ const contentHeight = mainHeight - 1 - (narrow ? 1 : 0);
664
+ const groupOf = (app) => workspaceRoot ? appGroup(workspaceRoot, app.root) : '.';
665
+ const appName = appLabel(project);
666
+ const tabName = t(TABS.find(entry => entry.id === tab).label).toLowerCase();
667
+ const contextLabel = settings
668
+ ? `${t('workspace')} ▸ ${t('settings')}`
669
+ : workspaceRoot
670
+ ? `${t('workspace')} ▸ ${path.relative(workspaceRoot, project.root)} ▸ ${tabName}`
671
+ : `${appName} ▸ ${tabName}`;
672
+ const h = (pairs) => pairs.map(([key, label]) => ({ key, label: t(label) }));
673
+ const settingsHints = editing
674
+ ? h([
675
+ ['Enter', 'save'],
676
+ ['Esc', 'cancel'],
677
+ ])
678
+ : formActive
679
+ ? h([
680
+ ['↑↓', 'field'],
681
+ ['Enter', 'edit'],
682
+ ['y', 'sync'],
683
+ ['Esc', 'back'],
684
+ ['q', 'quit'],
685
+ ])
686
+ : h([
687
+ ['Enter', 'edit settings'],
688
+ ['↑↓', 'apps'],
689
+ ['q', 'quit'],
690
+ ]);
691
+ const navHints = filter
692
+ ? h([
693
+ ['↑↓', 'move'],
694
+ ['Enter', 'select'],
695
+ ['Esc', 'clear'],
696
+ ['/', 'commands'],
697
+ ])
698
+ : h([
699
+ ['a–z', 'search'],
700
+ ['↑↓', 'move'],
701
+ ['Enter', 'select'],
702
+ ['/', 'commands'],
703
+ ['q', 'quit'],
704
+ ]);
705
+ const where = focus === 'nav' && !settings ? 'nav' : settings ? 'workspace' : tab;
706
+ const hereHints = focus === 'nav' && !settings
707
+ ? navHints
708
+ : settings
709
+ ? settingsHints
710
+ : tab === 'versions'
711
+ ? h([
712
+ ['a', 'activate'],
713
+ ['r', 'refresh'],
714
+ ['p', 'deploy'],
715
+ ['P', 'force'],
716
+ ['/', 'commands'],
717
+ ['q', 'quit'],
718
+ ])
719
+ : tab === 'log'
720
+ ? h([
721
+ ['f', 'follow'],
722
+ ['x', 'wrap'],
723
+ ['K', 'stop'],
724
+ ['p', 'deploy'],
725
+ ['P', 'force'],
726
+ ['/', 'commands'],
727
+ ['q', 'quit'],
728
+ ])
729
+ : tab === 'config'
730
+ ? editing
731
+ ? h([
732
+ ['Enter', 'save'],
733
+ ['Esc', 'cancel'],
734
+ ])
735
+ : formActive
736
+ ? h([
737
+ ['↑↓', 'field'],
738
+ ['Enter', 'edit'],
739
+ ['^O', 'pick addon'],
740
+ ['y', 'sync'],
741
+ ['/', 'commands'],
742
+ ['q', 'quit'],
743
+ ])
744
+ : h([
745
+ ['Tab', 'edit'],
746
+ ['y', 'sync'],
747
+ ['l', 'login'],
748
+ ['/', 'commands'],
749
+ ['q', 'quit'],
750
+ ])
751
+ : h([
752
+ ['d', 'dev'],
753
+ ['w', 'watch'],
754
+ ['b', 'build'],
755
+ ['s', 'sign'],
756
+ ['p', 'deploy'],
757
+ ['P', 'force'],
758
+ ['v', 'env'],
759
+ ['K', 'stop'],
760
+ ['a', 'versions'],
761
+ ['e', 'config'],
762
+ ['i', 'install'],
763
+ ['l', 'login'],
764
+ [',', 'settings'],
765
+ ['/', 'commands'],
766
+ ['q', 'quit'],
767
+ ]);
768
+ const hints = overlay
769
+ ? h([['Esc', 'cancel']])
770
+ : editing
771
+ ? hereHints
772
+ : [...hereHints, ...h([['?', 'help']])];
773
+ const right = running.length > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', running[0].label, " ", running[0].appName, running.length > 1 && (_jsxs(Text, { dimColor: true, children: [" \u00B7 ", t('{n} tasks', { n: running.length })] }))] })) : notice ? (_jsx(Text, { color: { info: undefined, ok: 'green', warn: 'yellow', error: 'red' }[notice.level], wrap: "truncate", children: notice.text })) : (_jsx(Text, { dimColor: true, children: t('idle') }));
774
+ const closeOverlay = () => setOverlay(null);
775
+ const popoverWidth = Math.min(96, columns - 8);
776
+ const popoverHeight = Math.min(30, frameRows - 4);
777
+ const popover = overlay &&
778
+ renderOverlay(overlay, {
779
+ project,
780
+ here: hereHints,
781
+ where,
782
+ closeOverlay,
783
+ run,
784
+ notify,
785
+ loadAddons,
786
+ height: popoverHeight - 2,
787
+ rerender: tick,
788
+ openWorkspace: workspaceRoot
789
+ ? () => {
790
+ setOverlay(null);
791
+ setSelected(apps.length);
792
+ setFocus('content');
793
+ }
794
+ : undefined,
795
+ });
796
+ const content = settings && workspaceTarget ? (_jsx(ConfigForm, { project: workspaceTarget, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: async () => null, onSaved: () => {
797
+ reload();
798
+ notify(t('workspace config saved'), 'ok');
799
+ }, onEditingChange: setEditing }, "workspace")) : (_jsxs(_Fragment, { children: [tab === 'overview' && (_jsx(Overview, { project: project, tasks: tasks, height: contentHeight })), tab === 'config' && (_jsx(ConfigForm, { project: {
800
+ root: project.root,
801
+ devProperties: project.devProperties,
802
+ base: rawProject.devProperties,
803
+ environment: env,
804
+ workspaceRoot,
805
+ manifest: project.manifest,
806
+ manifestPath: project.paths.manifest,
807
+ }, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: pickAddon, onSaved: () => {
808
+ reload();
809
+ notify(t('config saved'), 'ok');
810
+ }, onEditingChange: setEditing }, `${project.root}|${env}`)), tab === 'versions' && (_jsx(Versions, { project: project, state: versions[versionsKey], selected: versionRow })), tab === 'log' && (_jsx(Log, { task: logTask, height: contentHeight, scroll: logScroll, wrap: logWrap }))] }));
811
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
812
+ name: env,
813
+ color: environmentColor(env, rawProject.devProperties),
814
+ }, version: version }), _jsxs(Box, { flexGrow: 1, height: mainHeight, children: [!narrow && (_jsx(Navigator, { apps: matches.map(i => apps[i]), groupOf: groupOf, selected: matches.indexOf(selected), focused: focus === 'nav', tasks: tasks, height: mainHeight, single: single, settingsSelected: settings, width: sidebar, filter: filter })), _jsxs(Box, { flexDirection: "column", width: narrow ? columns : columns - sidebar, overflow: "hidden", borderStyle: "single", borderLeft: false, borderRight: false, borderBottom: false, borderColor: focus === 'content' ? ACCENT : undefined, borderDimColor: focus !== 'content', children: [narrow && !single && (_jsx(NavigatorStrip, { apps: matches.map(i => apps[i]), selected: matches.indexOf(selected), focused: focus === 'nav', width: columns, filter: filter })), settings ? (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: ACCENT, children: t('Workspace settings') }), _jsx(Text, { dimColor: true, children: onboard && configIncomplete(workspaceTarget?.base)
815
+ ? t(' · new workspace: fill in once, every app inherits · Esc skips')
816
+ : t(' · shared .dev_properties.json at the root') })] })) : (_jsx(TabBar, { tab: tab, narrow: narrow, focused: focus === 'content' })), _jsx(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden", alignItems: "flex-start", children: content })] })] }), _jsx(BottomBar, { hints: hints, right: right }), popover && (_jsx(Popover, { columns: columns, rows: frameRows, width: popoverWidth, height: popoverHeight, children: popover }))] }));
817
+ }
818
+ function renderOverlay(overlay, env) {
819
+ const { project, here, where, closeOverlay, run, notify, loadAddons, height, rerender, openWorkspace, } = env;
820
+ switch (overlay.kind) {
821
+ case 'palette':
822
+ return (_jsx(CommandPalette, { project: project, onRun: run, onClose: closeOverlay, height: height }));
823
+ case 'password':
824
+ return (_jsx(PasswordInput, { label: overlay.label, showRememberOption: Boolean(overlay.rememberLabel), rememberLabel: overlay.rememberLabel ? `${overlay.rememberLabel}: ` : undefined, onSubmit: (password, remember) => {
825
+ closeOverlay();
826
+ overlay.resolve({ password, remember });
827
+ }, onCancel: () => {
828
+ closeOverlay();
829
+ overlay.resolve(null);
830
+ } }, overlay.label));
831
+ case 'login':
832
+ return (_jsx(AuthLoginScreen, { method: overlay.method, devProperties: overlay.devProperties, onComplete: credential => {
833
+ closeOverlay();
834
+ overlay.resolve(credential);
835
+ }, onError: message => {
836
+ closeOverlay();
837
+ notify(message, 'error');
838
+ overlay.resolve(null);
839
+ }, onCancel: () => {
840
+ closeOverlay();
841
+ overlay.resolve(null);
842
+ } }));
843
+ case 'confirm':
844
+ return (_jsx(Confirm, { message: overlay.message, onAnswer: answer => {
845
+ closeOverlay();
846
+ overlay.resolve(answer);
847
+ } }));
848
+ case 'prompt':
849
+ return (_jsx(TextPrompt, { label: overlay.label, initial: overlay.initial, validate: overlay.validate, advise: overlay.advise, error: overlay.error, onSubmit: value => {
850
+ closeOverlay();
851
+ overlay.resolve(value);
852
+ }, onCancel: () => {
853
+ closeOverlay();
854
+ overlay.resolve(null);
855
+ } }, overlay.label));
856
+ case 'choice':
857
+ return (_jsx(ChoicePrompt, { label: overlay.label, choices: overlay.choices, multi: overlay.multi, initial: overlay.initial, onSubmit: picked => {
858
+ closeOverlay();
859
+ overlay.resolve(picked);
860
+ }, onCancel: () => {
861
+ closeOverlay();
862
+ overlay.resolve(null);
863
+ } }, overlay.label));
864
+ case 'help':
865
+ return (_jsx(HelpPanel, { here: here, where: where, height: height, onClose: closeOverlay }));
866
+ case 'changelog':
867
+ return (_jsx(ChangelogPanel, { since: overlay.since, height: height, onClose: closeOverlay }));
868
+ case 'settings':
869
+ return (_jsx(SettingsScreen, { onChanged: rerender, onClose: closeOverlay, onOpenWorkspace: openWorkspace }));
870
+ case 'picker':
871
+ return (_jsx(AddonPicker, { domain: project.devProperties?.domain ?? '', appType: appTypeOf(project.manifest), initialQuery: localizedText(project.manifest.name), load: loadAddons, height: height, onSelect: name => {
872
+ closeOverlay();
873
+ overlay.resolve(name);
874
+ }, onClose: () => {
875
+ closeOverlay();
876
+ overlay.resolve(null);
877
+ } }));
878
+ }
879
+ }
880
+ /**
881
+ * The open overlays, last one shown. `null` closes the shown one. A new overlay
882
+ * covers the current one instead of replacing it, so whoever awaits the covered
883
+ * one still gets an answer. A confirm goes underneath: background tasks raise
884
+ * them, and on top it would take the keys being typed into a prompt.
885
+ */
886
+ export function overlayStack(stack, next) {
887
+ if (next === null)
888
+ return stack.slice(0, -1);
889
+ return next.kind === 'confirm' ? [next, ...stack] : [...stack, next];
890
+ }
891
+ export function withoutOverlays(stack, gone) {
892
+ return stack.filter(overlay => !gone.has(overlay));
893
+ }
894
+ export function Confirm({ message, onAnswer, }) {
895
+ useInput((input, key) => {
896
+ if (input === 'y' || input === 'Y')
897
+ onAnswer(true);
898
+ else if (input === 'n' || input === 'N' || key.escape)
899
+ onAnswer(false);
900
+ });
901
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: message }), _jsx(Text, { dimColor: true, children: t('y confirm · n cancel') })] }));
902
+ }
903
+ function ChoicePrompt({ label, choices, multi, initial, onSubmit, onCancel, }) {
904
+ const [cursor, setCursor] = useState(multi ? 0 : (initial[0] ?? 0));
905
+ const [checked, setChecked] = useState(() => new Set(multi ? initial : []));
906
+ useInput((input, key) => {
907
+ if (key.escape)
908
+ onCancel();
909
+ else if (key.return)
910
+ onSubmit(multi ? [...checked].toSorted((a, b) => a - b) : [cursor]);
911
+ else if (key.upArrow)
912
+ setCursor(c => (c - 1 + choices.length) % choices.length);
913
+ else if (key.downArrow)
914
+ setCursor(c => (c + 1) % choices.length);
915
+ else if (multi && input === ' ') {
916
+ setChecked(current => {
917
+ const next = new Set(current);
918
+ if (!next.delete(cursor))
919
+ next.add(cursor);
920
+ return next;
921
+ });
922
+ }
923
+ });
924
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: label }), choices.map((choice, i) => (_jsxs(Text, { color: i === cursor ? ACCENT : undefined, bold: i === cursor, children: [i === cursor ? '▸ ' : ' ', multi ? (checked.has(i) ? '◉ ' : '◯ ') : '', choice] }, choice))), _jsx(Text, { dimColor: true, children: multi
925
+ ? t('↑↓ move · Space toggle · Enter submit · Esc cancel')
926
+ : t('↑↓ move · Enter select · Esc cancel') })] }));
927
+ }
928
+ function TextPrompt({ label, initial = '', validate, advise, error, onSubmit, onCancel, }) {
929
+ const [value, setValue] = useState(initial);
930
+ const [problem, setProblem] = useState(error);
931
+ // The value an advice was shown for; submitting it again keeps it.
932
+ const [advised, setAdvised] = useState();
933
+ const advice = advised === value ? advise?.(value) : undefined;
934
+ useInput((input, key) => {
935
+ if (key.escape)
936
+ onCancel();
937
+ else if (key.return) {
938
+ const invalid = validate?.(value);
939
+ setProblem(invalid);
940
+ if (invalid)
941
+ return;
942
+ if (advise?.(value) && advised !== value)
943
+ setAdvised(value);
944
+ else
945
+ onSubmit(value);
946
+ }
947
+ else if (key.backspace || key.delete)
948
+ setValue(v => v.slice(0, -1));
949
+ else if (input && !key.ctrl && !key.meta)
950
+ setValue(v => v + input);
951
+ });
952
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: label }), _jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), value, _jsx(Text, { inverse: true, children: " " })] }), problem && _jsxs(Text, { color: "red", children: ["\u2717 ", problem] }), !problem && advice && _jsx(Text, { color: "yellow", children: advice }), _jsx(Text, { dimColor: true, children: t('Press Enter to submit, Esc to cancel') })] }));
953
+ }