sitevision-cli 1.0.0-beta.3 → 1.0.0-beta.30

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 (72) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +59 -8
  4. package/dist/cli.js +125 -40
  5. package/dist/commands/build.js +1 -1
  6. package/dist/commands/deploy.d.ts +2 -2
  7. package/dist/commands/deploy.js +137 -28
  8. package/dist/commands/dev.d.ts +8 -10
  9. package/dist/commands/dev.js +70 -371
  10. package/dist/commands/watch.js +5 -23
  11. package/dist/components/AnimatedLogo.js +8 -2
  12. package/dist/components/AuthLoginScreen.d.ts +21 -0
  13. package/dist/components/AuthLoginScreen.js +90 -0
  14. package/dist/components/DevPropertiesForm.d.ts +2 -1
  15. package/dist/components/DevPropertiesForm.js +198 -33
  16. package/dist/components/MainMenu.js +5 -0
  17. package/dist/components/PasswordInput.js +2 -1
  18. package/dist/components/SetupFlow.d.ts +2 -1
  19. package/dist/components/SetupFlow.js +99 -10
  20. package/dist/shell/AddonPicker.d.ts +14 -0
  21. package/dist/shell/AddonPicker.js +54 -0
  22. package/dist/shell/Changelog.d.ts +10 -0
  23. package/dist/shell/Changelog.js +44 -0
  24. package/dist/shell/CommandPalette.d.ts +12 -0
  25. package/dist/shell/CommandPalette.js +63 -0
  26. package/dist/shell/ConfigForm.d.ts +36 -0
  27. package/dist/shell/ConfigForm.js +558 -0
  28. package/dist/shell/Frame.d.ts +59 -0
  29. package/dist/shell/Frame.js +135 -0
  30. package/dist/shell/Help.d.ts +6 -0
  31. package/dist/shell/Help.js +31 -0
  32. package/dist/shell/Popover.d.ts +12 -0
  33. package/dist/shell/Popover.js +15 -0
  34. package/dist/shell/Settings.d.ts +6 -0
  35. package/dist/shell/Settings.js +96 -0
  36. package/dist/shell/Shell.d.ts +10 -0
  37. package/dist/shell/Shell.js +620 -0
  38. package/dist/shell/Tabs.d.ts +36 -0
  39. package/dist/shell/Tabs.js +90 -0
  40. package/dist/shell/actions.d.ts +48 -0
  41. package/dist/shell/actions.js +0 -0
  42. package/dist/types/index.d.ts +37 -3
  43. package/dist/utils/config.d.ts +10 -0
  44. package/dist/utils/config.js +14 -0
  45. package/dist/utils/environments.d.ts +20 -0
  46. package/dist/utils/environments.js +74 -0
  47. package/dist/utils/i18n.d.ts +12 -0
  48. package/dist/utils/i18n.js +300 -0
  49. package/dist/utils/jsonc.d.ts +19 -0
  50. package/dist/utils/jsonc.js +74 -0
  51. package/dist/utils/keychain.d.ts +9 -0
  52. package/dist/utils/keychain.js +54 -0
  53. package/dist/utils/oauth2-auth.d.ts +64 -0
  54. package/dist/utils/oauth2-auth.js +242 -0
  55. package/dist/utils/password-prompt.d.ts +5 -0
  56. package/dist/utils/password-prompt.js +28 -0
  57. package/dist/utils/process-runner.d.ts +13 -4
  58. package/dist/utils/process-runner.js +63 -37
  59. package/dist/utils/project-detection.d.ts +90 -4
  60. package/dist/utils/project-detection.js +389 -63
  61. package/dist/utils/session-cookie-auth.d.ts +35 -0
  62. package/dist/utils/session-cookie-auth.js +99 -0
  63. package/dist/utils/sitevision-api.d.ts +72 -5
  64. package/dist/utils/sitevision-api.js +220 -38
  65. package/dist/utils/sitevision-scripts-runner.d.ts +2 -1
  66. package/dist/utils/sitevision-scripts-runner.js +18 -7
  67. package/dist/utils/tasks.d.ts +50 -0
  68. package/dist/utils/tasks.js +439 -0
  69. package/dist/utils/workspace.d.ts +17 -0
  70. package/dist/utils/workspace.js +67 -0
  71. package/package.json +5 -2
  72. package/readme.md +103 -121
@@ -0,0 +1,620 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import path from 'node:path';
3
+ import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
4
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
5
+ import Spinner from 'ink-spinner';
6
+ import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, syncDevPropertiesToPackageJson, } from '../utils/project-detection.js';
7
+ import { appGroup, configIncomplete, needsOnboarding, } from '../utils/workspace.js';
8
+ import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
9
+ import { useTasks, runningTasks, startActivate, getTasks, } from '../utils/tasks.js';
10
+ import { PasswordInput } from '../components/PasswordInput.js';
11
+ import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
12
+ import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
13
+ import { TabBar, TABS, Overview, Versions, Log, } from './Tabs.js';
14
+ import { CommandPalette } from './CommandPalette.js';
15
+ import { ConfigForm } from './ConfigForm.js';
16
+ import { AddonPicker } from './AddonPicker.js';
17
+ import { SettingsScreen } from './Settings.js';
18
+ import { HelpPanel } from './Help.js';
19
+ import { Popover } from './Popover.js';
20
+ import { ChangelogPanel } from './Changelog.js';
21
+ import { baseEnvironment, environmentColor, environmentNames, environmentProject, isProductionEnvironment, resolveEnvironment, } from '../utils/environments.js';
22
+ import { t } from '../utils/i18n.js';
23
+ import { actionForKey, authState, resolveDeployConfig, } from './actions.js';
24
+ function useSize() {
25
+ const { stdout } = useStdout();
26
+ const read = () => ({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
27
+ const [size, setSize] = useState(read);
28
+ useEffect(() => {
29
+ const onResize = () => setSize(read());
30
+ stdout.on('resize', onResize);
31
+ return () => {
32
+ stdout.off('resize', onResize);
33
+ };
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [stdout]);
36
+ return size;
37
+ }
38
+ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, }) {
39
+ const { exit } = useApp();
40
+ const { columns, rows } = useSize();
41
+ const tasks = useTasks();
42
+ const [apps, setApps] = useState(initialApps);
43
+ // A workspace with nothing to deploy against opens on its settings form.
44
+ const onboard = useMemo(() => Boolean(workspaceRoot) && needsOnboarding(workspaceRoot, initialApps), [workspaceRoot, initialApps]);
45
+ const [selected, setSelected] = useState(onboard ? initialApps.length : 0);
46
+ const [tab, setTab] = useState('overview');
47
+ const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
48
+ const [overlay, setOverlay] = useState(updatedFrom ? { kind: 'changelog', since: updatedFrom } : null);
49
+ const [filter, setFilter] = useState('');
50
+ const [versions, setVersions] = useState({});
51
+ const [versionRow, setVersionRow] = useState(0);
52
+ const [logScroll, setLogScroll] = useState(0);
53
+ const [logWrap, setLogWrap] = useState(false);
54
+ const [notice, setNotice] = useState(null);
55
+ const [, tick] = useReducer((n) => n + 1, 0);
56
+ // In workspace mode the row after the last app is "Workspace settings".
57
+ const settings = Boolean(workspaceRoot) && selected === apps.length;
58
+ const rawProject = apps[Math.min(selected, apps.length - 1)];
59
+ // Active environment, remembered per workspace (or app) in .svcconfig.
60
+ const configRoot = workspaceRoot ?? rawProject.root;
61
+ const [envChoice, setEnvChoice] = useState(() => readSvcConfig(configRoot).environment ?? '');
62
+ const envNames = environmentNames(rawProject.devProperties);
63
+ const envList = envNames.join(',');
64
+ const env = envNames.includes(envChoice)
65
+ ? envChoice
66
+ : baseEnvironment(rawProject.devProperties);
67
+ const project = useMemo(() => environmentProject(rawProject, env), [rawProject, env]);
68
+ const isProduction = isProductionEnvironment(env, rawProject.devProperties);
69
+ const versionsKey = `${project.root}|${env}`;
70
+ const single = !workspaceRoot;
71
+ const workspaceTarget = useMemo(() => workspaceRoot
72
+ ? {
73
+ root: workspaceRoot,
74
+ base: readWorkspaceDevProperties(workspaceRoot),
75
+ devProperties: resolveEnvironment(readWorkspaceDevProperties(workspaceRoot), env),
76
+ environment: env,
77
+ workspace: true,
78
+ }
79
+ : undefined,
80
+ // Re-read after any reload so saved values show up.
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ [workspaceRoot, apps, env]);
83
+ const narrow = minimal || columns < NARROW_BELOW;
84
+ const sidebar = navWidth(columns);
85
+ // The navigator shows the fuzzy matches; `selected` stays an index into
86
+ // `apps` (with `apps.length` meaning the workspace settings row).
87
+ const matches = useMemo(() => navMatches(apps, filter), [apps, filter]);
88
+ const ring = single ? matches : [...matches, apps.length];
89
+ const running = runningTasks();
90
+ // Re-render once a second while something runs so elapsed times move.
91
+ useEffect(() => {
92
+ if (running.length === 0)
93
+ return;
94
+ const timer = setInterval(tick, 1000);
95
+ return () => clearInterval(timer);
96
+ }, [running.length]);
97
+ const reload = useCallback(() => {
98
+ setApps(current => current.map(app => {
99
+ try {
100
+ return detectProject(app.root) ?? app;
101
+ }
102
+ catch {
103
+ return app;
104
+ }
105
+ }));
106
+ }, []);
107
+ // A finished npm install changes what detection sees (node_modules,
108
+ // sitevision-scripts); re-detect so the status strip flips.
109
+ const installsDone = tasks.filter(task => task.kind === 'install' && task.status !== 'running').length;
110
+ useEffect(() => {
111
+ if (installsDone > 0)
112
+ reload();
113
+ }, [installsDone, reload]);
114
+ const notify = useCallback((text, level = 'info') => {
115
+ setNotice({ text, level });
116
+ }, []);
117
+ const quit = useCallback(() => {
118
+ for (const task of getTasks())
119
+ if (task.status === 'running')
120
+ task.stop();
121
+ exit();
122
+ }, [exit]);
123
+ const context = useMemo(() => ({
124
+ project,
125
+ reload,
126
+ notify,
127
+ quit,
128
+ setTab(next) {
129
+ setTab(next);
130
+ setFocus('content');
131
+ },
132
+ openSettings() {
133
+ setOverlay({ kind: 'settings' });
134
+ },
135
+ openChangelog() {
136
+ setOverlay({ kind: 'changelog' });
137
+ },
138
+ environment: env,
139
+ isProduction,
140
+ cycleEnvironment() {
141
+ const next = envNames[(envNames.indexOf(env) + 1) % envNames.length];
142
+ setEnvChoice(next);
143
+ writeSvcConfig(configRoot, { environment: next });
144
+ notify(t('switched to {env}', { env: next }));
145
+ },
146
+ async addEnvironment() {
147
+ const name = await new Promise(resolve => {
148
+ setOverlay({
149
+ kind: 'prompt',
150
+ label: t('Environment name (e.g. test, prod)'),
151
+ resolve,
152
+ });
153
+ });
154
+ const clean = name
155
+ ?.trim()
156
+ .toLowerCase()
157
+ .replaceAll(/[^\d\-a-z]/g, '');
158
+ if (!clean || clean === baseEnvironment(rawProject.devProperties))
159
+ return;
160
+ const targetRoot = workspaceRoot ?? rawProject.root;
161
+ const base = (workspaceRoot
162
+ ? readWorkspaceDevProperties(workspaceRoot)
163
+ : rawProject.devProperties);
164
+ if (!base)
165
+ return;
166
+ writeDevProperties(targetRoot, { ...base, environments: { ...base.environments, [clean]: {} } }, { complete: !workspaceRoot });
167
+ reload();
168
+ setEnvChoice(clean);
169
+ writeSvcConfig(configRoot, { environment: clean });
170
+ notify(t('environment {env} added', { env: clean }), 'ok');
171
+ },
172
+ openWorkspaceSettings: workspaceRoot
173
+ ? () => {
174
+ setSelected(apps.length);
175
+ setFocus('content');
176
+ }
177
+ : undefined,
178
+ askPassword: (label, rememberLabel) => new Promise(resolve => {
179
+ setOverlay({ kind: 'password', label, rememberLabel, resolve });
180
+ }),
181
+ login: method => new Promise(resolve => {
182
+ setOverlay({
183
+ kind: 'login',
184
+ method,
185
+ devProperties: project.devProperties,
186
+ resolve,
187
+ });
188
+ }),
189
+ confirm: message => new Promise(resolve => {
190
+ setOverlay({ kind: 'confirm', message, resolve });
191
+ }),
192
+ openHelp() {
193
+ setOverlay({ kind: 'help' });
194
+ },
195
+ }),
196
+ // eslint-disable-next-line react-hooks/exhaustive-deps
197
+ [project, reload, notify, quit, workspaceRoot, apps.length, env, envList]);
198
+ const run = useCallback((action) => {
199
+ setOverlay(null);
200
+ if (action.enabled && !action.enabled(project)) {
201
+ notify(`${t(action.label)}: ${action.detail?.(project) ?? t('not available')}`, 'warn');
202
+ return;
203
+ }
204
+ action.run(context).catch((error) => {
205
+ notify(error instanceof Error ? error.message : String(error), 'error');
206
+ });
207
+ }, [context, project, notify]);
208
+ const fetchVersions = useCallback(async (fresh = false) => {
209
+ const key = versionsKey;
210
+ const config = await resolveDeployConfig(context, fresh);
211
+ if (!config)
212
+ return;
213
+ setVersions(v => ({
214
+ ...v,
215
+ [key]: { ...v[key], loading: true, error: undefined },
216
+ }));
217
+ const result = await listExecutables(config);
218
+ setVersions(v => ({
219
+ ...v,
220
+ [key]: {
221
+ loading: false,
222
+ executables: result.executables ?? v[key]?.executables,
223
+ error: result.error,
224
+ fetchedAt: result.success ? Date.now() : v[key]?.fetchedAt,
225
+ },
226
+ }));
227
+ setVersionRow(0);
228
+ }, [versionsKey, context]);
229
+ const activateSelected = useCallback(async () => {
230
+ const executable = versions[versionsKey]?.executables?.[versionRow];
231
+ if (!executable)
232
+ return;
233
+ if (executable.active) {
234
+ notify(t('{v} is already active', { v: executable.appVersion }));
235
+ return;
236
+ }
237
+ const config = await resolveDeployConfig(context);
238
+ if (!config)
239
+ return;
240
+ const task = startActivate(project, config, executable.id, executable.appVersion);
241
+ const wait = () => new Promise(resolve => {
242
+ const check = () => task.status === 'running' ? setTimeout(check, 200) : resolve();
243
+ check();
244
+ });
245
+ await wait();
246
+ notify(task.error ?? t('{v} activated', { v: executable.appVersion }), task.error ? 'error' : 'ok');
247
+ await fetchVersions();
248
+ }, [versions, project, versionRow, context, notify, fetchVersions]);
249
+ // A popover owns the keyboard; the form stays mounted underneath it.
250
+ const formActive = (tab === 'config' || settings) && focus === 'content' && !overlay;
251
+ const [editing, setEditing] = useState(false);
252
+ const pickAddon = useCallback(async () => new Promise(resolve => {
253
+ setOverlay({ kind: 'picker', resolve });
254
+ }), []);
255
+ const loadAddons = useCallback(async () => {
256
+ const config = await resolveDeployConfig(context);
257
+ return config ? listAddons(config) : { error: t('No credentials.') };
258
+ }, [context]);
259
+ const appTasks = tasks.filter(task => task.appRoot === project.root);
260
+ const logTask = appTasks.find(task => task.status === 'running') ?? appTasks.at(-1);
261
+ useInput((raw, key) => {
262
+ // Terminals speaking the kitty keyboard protocol report shift+s as
263
+ // "s" plus a shift flag; fold that back into the uppercase letter so
264
+ // P and K behave the same everywhere.
265
+ const input = key.shift && raw.length === 1 && /[a-z]/.test(raw)
266
+ ? raw.toUpperCase()
267
+ : raw;
268
+ if (key.escape) {
269
+ if (focus === 'nav' && filter) {
270
+ setFilter('');
271
+ return;
272
+ }
273
+ // Esc backs out of the workspace settings pane, not just its focus.
274
+ if (settings)
275
+ setSelected(0);
276
+ setFocus(single ? 'content' : 'nav');
277
+ return;
278
+ }
279
+ if (input === '/') {
280
+ setOverlay({ kind: 'palette' });
281
+ return;
282
+ }
283
+ if (input === '?') {
284
+ setOverlay({ kind: 'help' });
285
+ return;
286
+ }
287
+ // The config form uses Tab/Shift+Tab to move between fields.
288
+ if (key.tab && !formActive) {
289
+ setFilter('');
290
+ setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
291
+ return;
292
+ }
293
+ // Navigator: typing searches, so no action key fires until Enter has
294
+ // moved the focus into the content pane.
295
+ if (focus === 'nav') {
296
+ const move = (delta) => {
297
+ setSelected(navMove(ring, selected, delta));
298
+ };
299
+ const search = (next) => {
300
+ setFilter(next);
301
+ const found = navMatches(apps, next);
302
+ if (found.length > 0 && !found.includes(selected))
303
+ setSelected(found[0]);
304
+ };
305
+ if (key.upArrow)
306
+ move(-1);
307
+ else if (key.downArrow)
308
+ move(1);
309
+ else if (key.return) {
310
+ if (matches.length > 0 || settings) {
311
+ setFilter('');
312
+ setFocus('content');
313
+ }
314
+ }
315
+ else if (key.backspace || key.delete)
316
+ search(filter.slice(0, -1));
317
+ else if (input === 'q' && !filter)
318
+ quit();
319
+ else if (input?.length === 1 && input >= ' ' && !key.ctrl && !key.meta)
320
+ search(filter + input);
321
+ return;
322
+ }
323
+ if (input === 'a' && tab !== 'versions') {
324
+ setTab('versions');
325
+ setFocus('content');
326
+ if (!Object.hasOwn(versions, versionsKey))
327
+ void fetchVersions(false);
328
+ return;
329
+ }
330
+ const digit = Number.parseInt(input, 10);
331
+ if (digit >= 1 && digit <= TABS.length) {
332
+ setTab(TABS[digit - 1].id);
333
+ setFocus('content');
334
+ return;
335
+ }
336
+ if (key.leftArrow || key.rightArrow) {
337
+ const i = TABS.findIndex(entry => entry.id === tab);
338
+ setTab(TABS[(i + (key.rightArrow ? 1 : TABS.length - 1)) % TABS.length].id);
339
+ return;
340
+ }
341
+ if (settings && focus === 'content') {
342
+ // Settings pane: the form owns everything but q, y and Tab/Esc above.
343
+ if (input === 'q')
344
+ quit();
345
+ else if (input === 'y') {
346
+ try {
347
+ if (syncDevPropertiesToPackageJson(workspaceRoot)) {
348
+ reload();
349
+ notify(t('package.json updated'), 'ok');
350
+ }
351
+ }
352
+ catch (error) {
353
+ notify(error instanceof Error ? error.message : String(error), 'error');
354
+ }
355
+ }
356
+ return;
357
+ }
358
+ if (tab === 'versions') {
359
+ const count = versions[versionsKey]?.executables?.length ?? 0;
360
+ if (key.upArrow)
361
+ setVersionRow(r => Math.max(0, r - 1));
362
+ if (key.downArrow)
363
+ setVersionRow(r => Math.min(Math.max(0, count - 1), r + 1));
364
+ if (input === 'r')
365
+ void fetchVersions(false);
366
+ else if (input === 'a' && count > 0)
367
+ void activateSelected();
368
+ }
369
+ else if (tab === 'log') {
370
+ const max = Math.max(0, (logTask?.lines.length ?? 0) - 1);
371
+ if (key.upArrow)
372
+ setLogScroll(s => Math.min(max, s + 1));
373
+ if (key.downArrow)
374
+ setLogScroll(s => Math.max(0, s - 1));
375
+ if (key.pageUp)
376
+ setLogScroll(s => Math.min(max, s + 10));
377
+ if (key.pageDown)
378
+ setLogScroll(s => Math.max(0, s - 10));
379
+ if (input === 'f')
380
+ setLogScroll(0);
381
+ else if (input === 'x')
382
+ setLogWrap(w => !w);
383
+ }
384
+ const action = actionForKey(input);
385
+ if (action)
386
+ run(action);
387
+ }, { isActive: overlay === null && !editing });
388
+ // Frame geometry: one row for Ink's trailing newline, top bar, bottom bar.
389
+ // The sidebar layout needs 10 rows to stack its own fixed rows without
390
+ // overflowing; the compact one has no sidebar and fits in 6.
391
+ const frameRows = Math.max(narrow ? 6 : 10, rows - 1);
392
+ const mainHeight = frameRows - 2;
393
+ const contentHeight = mainHeight - 1 - (narrow ? 1 : 0);
394
+ const groupOf = (app) => workspaceRoot ? appGroup(workspaceRoot, app.root) : '.';
395
+ const appName = appLabel(project);
396
+ const tabName = t(TABS.find(entry => entry.id === tab).label).toLowerCase();
397
+ const contextLabel = settings
398
+ ? `${t('workspace')} ▸ ${t('settings')}`
399
+ : workspaceRoot
400
+ ? `${t('workspace')} ▸ ${path.relative(workspaceRoot, project.root)} ▸ ${tabName}`
401
+ : `${appName} ▸ ${tabName}`;
402
+ const h = (pairs) => pairs.map(([key, label]) => ({ key, label: t(label) }));
403
+ const settingsHints = editing
404
+ ? h([
405
+ ['Enter', 'save'],
406
+ ['Esc', 'cancel'],
407
+ ])
408
+ : formActive
409
+ ? h([
410
+ ['↑↓', 'field'],
411
+ ['Enter', 'edit'],
412
+ ['y', 'sync'],
413
+ ['Esc', 'back'],
414
+ ['q', 'quit'],
415
+ ])
416
+ : h([
417
+ ['Enter', 'edit settings'],
418
+ ['↑↓', 'apps'],
419
+ ['q', 'quit'],
420
+ ]);
421
+ const navHints = filter
422
+ ? h([
423
+ ['↑↓', 'move'],
424
+ ['Enter', 'select'],
425
+ ['Esc', 'clear'],
426
+ ['/', 'commands'],
427
+ ])
428
+ : h([
429
+ ['a–z', 'search'],
430
+ ['↑↓', 'move'],
431
+ ['Enter', 'select'],
432
+ ['/', 'commands'],
433
+ ['q', 'quit'],
434
+ ]);
435
+ const hereHints = focus === 'nav' && !settings
436
+ ? navHints
437
+ : settings
438
+ ? settingsHints
439
+ : tab === 'versions'
440
+ ? h([
441
+ ['a', 'activate'],
442
+ ['r', 'refresh'],
443
+ ['p', 'deploy'],
444
+ ['P', 'force'],
445
+ ['/', 'commands'],
446
+ ['q', 'quit'],
447
+ ])
448
+ : tab === 'log'
449
+ ? h([
450
+ ['f', 'follow'],
451
+ ['x', 'wrap'],
452
+ ['K', 'stop'],
453
+ ['p', 'deploy'],
454
+ ['P', 'force'],
455
+ ['/', 'commands'],
456
+ ['q', 'quit'],
457
+ ])
458
+ : tab === 'config'
459
+ ? editing
460
+ ? h([
461
+ ['Enter', 'save'],
462
+ ['Esc', 'cancel'],
463
+ ])
464
+ : formActive
465
+ ? h([
466
+ ['↑↓', 'field'],
467
+ ['Enter', 'edit'],
468
+ ['^O', 'pick addon'],
469
+ ['y', 'sync'],
470
+ ['/', 'commands'],
471
+ ['q', 'quit'],
472
+ ])
473
+ : h([
474
+ ['Tab', 'edit'],
475
+ ['y', 'sync'],
476
+ ['l', 'login'],
477
+ ['/', 'commands'],
478
+ ['q', 'quit'],
479
+ ])
480
+ : h([
481
+ ['d', 'dev'],
482
+ ['w', 'watch'],
483
+ ['b', 'build'],
484
+ ['s', 'sign'],
485
+ ['p', 'deploy'],
486
+ ['P', 'force'],
487
+ ['v', 'env'],
488
+ ['K', 'stop'],
489
+ ['a', 'versions'],
490
+ ['e', 'config'],
491
+ ['i', 'install'],
492
+ ['l', 'login'],
493
+ [',', 'settings'],
494
+ ['/', 'commands'],
495
+ ['q', 'quit'],
496
+ ]);
497
+ const hints = overlay
498
+ ? h([['Esc', 'cancel']])
499
+ : editing
500
+ ? hereHints
501
+ : [...hereHints, ...h([['?', 'help']])];
502
+ 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') }));
503
+ const closeOverlay = () => setOverlay(null);
504
+ const popoverWidth = Math.min(96, columns - 8);
505
+ const popoverHeight = Math.min(30, frameRows - 4);
506
+ const popover = overlay &&
507
+ renderOverlay(overlay, {
508
+ project,
509
+ here: hereHints,
510
+ closeOverlay,
511
+ run,
512
+ notify,
513
+ loadAddons,
514
+ height: popoverHeight - 2,
515
+ rerender: tick,
516
+ openWorkspace: workspaceRoot
517
+ ? () => {
518
+ setOverlay(null);
519
+ setSelected(apps.length);
520
+ setFocus('content');
521
+ }
522
+ : undefined,
523
+ });
524
+ const content = settings && workspaceTarget ? (_jsx(ConfigForm, { project: workspaceTarget, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: async () => null, onSaved: () => {
525
+ reload();
526
+ notify(t('workspace config saved'), 'ok');
527
+ }, onEditingChange: setEditing }, "workspace")) : (_jsxs(_Fragment, { children: [tab === 'overview' && (_jsx(Overview, { project: project, tasks: tasks, height: contentHeight })), tab === 'config' && (_jsx(ConfigForm, { project: {
528
+ root: project.root,
529
+ devProperties: project.devProperties,
530
+ base: rawProject.devProperties,
531
+ environment: env,
532
+ workspaceRoot,
533
+ }, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: pickAddon, onSaved: () => {
534
+ reload();
535
+ notify(t('config saved'), 'ok');
536
+ }, 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 }))] }));
537
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
538
+ name: env,
539
+ color: environmentColor(env, rawProject.devProperties),
540
+ }, 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)
541
+ ? t(' · new workspace: fill in once, every app inherits · Esc skips')
542
+ : 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 }))] }));
543
+ }
544
+ function renderOverlay(overlay, env) {
545
+ const { project, here, closeOverlay, run, notify, loadAddons, height, rerender, openWorkspace, } = env;
546
+ switch (overlay.kind) {
547
+ case 'palette':
548
+ return (_jsx(CommandPalette, { project: project, onRun: run, onClose: closeOverlay, height: height }));
549
+ case 'password':
550
+ return (_jsx(PasswordInput, { label: overlay.label, showRememberOption: Boolean(overlay.rememberLabel), rememberLabel: overlay.rememberLabel ? `${overlay.rememberLabel}: ` : undefined, onSubmit: (password, remember) => {
551
+ closeOverlay();
552
+ overlay.resolve({ password, remember });
553
+ }, onCancel: () => {
554
+ closeOverlay();
555
+ overlay.resolve(null);
556
+ } }, overlay.label));
557
+ case 'login':
558
+ return (_jsx(AuthLoginScreen, { method: overlay.method, devProperties: overlay.devProperties, onComplete: credential => {
559
+ closeOverlay();
560
+ overlay.resolve(credential);
561
+ }, onError: message => {
562
+ closeOverlay();
563
+ notify(message, 'error');
564
+ overlay.resolve(null);
565
+ }, onCancel: () => {
566
+ closeOverlay();
567
+ overlay.resolve(null);
568
+ } }));
569
+ case 'confirm':
570
+ return (_jsx(Confirm, { message: overlay.message, onAnswer: answer => {
571
+ closeOverlay();
572
+ overlay.resolve(answer);
573
+ } }));
574
+ case 'prompt':
575
+ return (_jsx(TextPrompt, { label: overlay.label, onSubmit: value => {
576
+ closeOverlay();
577
+ overlay.resolve(value);
578
+ }, onCancel: () => {
579
+ closeOverlay();
580
+ overlay.resolve(null);
581
+ } }));
582
+ case 'help':
583
+ return _jsx(HelpPanel, { here: here, height: height, onClose: closeOverlay });
584
+ case 'changelog':
585
+ return (_jsx(ChangelogPanel, { since: overlay.since, height: height, onClose: closeOverlay }));
586
+ case 'settings':
587
+ return (_jsx(SettingsScreen, { onChanged: rerender, onClose: closeOverlay, onOpenWorkspace: openWorkspace }));
588
+ case 'picker':
589
+ return (_jsx(AddonPicker, { domain: project.devProperties?.domain ?? '', appType: appTypeOf(project.manifest), initialQuery: localizedText(project.manifest.name), load: loadAddons, height: height, onSelect: name => {
590
+ closeOverlay();
591
+ overlay.resolve(name);
592
+ }, onClose: () => {
593
+ closeOverlay();
594
+ overlay.resolve(null);
595
+ } }));
596
+ }
597
+ }
598
+ function Confirm({ message, onAnswer, }) {
599
+ useInput(input => {
600
+ if (input === 'y' || input === 'Y')
601
+ onAnswer(true);
602
+ else if (input === 'n' || input === 'N')
603
+ onAnswer(false);
604
+ });
605
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: message }), _jsx(Text, { dimColor: true, children: t('y confirm · n cancel') })] }));
606
+ }
607
+ function TextPrompt({ label, onSubmit, onCancel, }) {
608
+ const [value, setValue] = useState('');
609
+ useInput((input, key) => {
610
+ if (key.escape)
611
+ onCancel();
612
+ else if (key.return)
613
+ onSubmit(value);
614
+ else if (key.backspace || key.delete)
615
+ setValue(v => v.slice(0, -1));
616
+ else if (input && !key.ctrl && !key.meta)
617
+ setValue(v => v + input);
618
+ });
619
+ 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: " " })] }), _jsx(Text, { dimColor: true, children: t('Press Enter to submit, Esc to cancel') })] }));
620
+ }
@@ -0,0 +1,36 @@
1
+ import type { ProjectInfo } from '../types/index.js';
2
+ import { type Task } from '../utils/tasks.js';
3
+ import { type Executable } from '../utils/sitevision-api.js';
4
+ import { type Tab } from './actions.js';
5
+ export declare const TABS: {
6
+ id: Tab;
7
+ label: string;
8
+ short: string;
9
+ }[];
10
+ export declare function TabBar({ tab, narrow, focused, }: {
11
+ tab: Tab;
12
+ narrow: boolean;
13
+ focused: boolean;
14
+ }): import("react").JSX.Element;
15
+ export declare function Overview({ project, tasks, height, }: {
16
+ project: ProjectInfo;
17
+ tasks: Task[];
18
+ height: number;
19
+ }): import("react").JSX.Element;
20
+ export interface VersionsState {
21
+ loading: boolean;
22
+ executables?: Executable[];
23
+ error?: string;
24
+ fetchedAt?: number;
25
+ }
26
+ export declare function Versions({ project, state, selected, }: {
27
+ project: ProjectInfo;
28
+ state?: VersionsState;
29
+ selected: number;
30
+ }): import("react").JSX.Element;
31
+ export declare function Log({ task, height, scroll, wrap, }: {
32
+ task?: Task;
33
+ height: number;
34
+ scroll: number;
35
+ wrap: boolean;
36
+ }): import("react").JSX.Element;