sitevision-cli 1.0.0-beta.2 → 1.0.0-beta.20

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