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

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 +36 -0
  26. package/dist/shell/ConfigForm.js +558 -0
  27. package/dist/shell/Frame.d.ts +59 -0
  28. package/dist/shell/Frame.js +134 -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 +586 -0
  33. package/dist/shell/Tabs.d.ts +36 -0
  34. package/dist/shell/Tabs.js +90 -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 +279 -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 +105 -6
  53. package/dist/utils/project-detection.js +411 -54
  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 +102 -121
@@ -0,0 +1,586 @@
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 { 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, { ...base, environments: { ...base.environments, [clean]: {} } }, { complete: !workspaceRoot });
161
+ reload();
162
+ setEnvChoice(clean);
163
+ writeSvcConfig(configRoot, { environment: clean });
164
+ notify(t('environment {env} added', { env: clean }), 'ok');
165
+ },
166
+ openWorkspaceSettings: workspaceRoot
167
+ ? () => {
168
+ setSelected(apps.length);
169
+ setFocus('content');
170
+ }
171
+ : undefined,
172
+ askPassword: (label, rememberLabel) => new Promise(resolve => {
173
+ setOverlay({ kind: 'password', label, rememberLabel, resolve });
174
+ }),
175
+ login: method => new Promise(resolve => {
176
+ setOverlay({
177
+ kind: 'login',
178
+ method,
179
+ devProperties: project.devProperties,
180
+ resolve,
181
+ });
182
+ }),
183
+ confirm: message => new Promise(resolve => {
184
+ setOverlay({ kind: 'confirm', message, resolve });
185
+ }),
186
+ }),
187
+ // eslint-disable-next-line react-hooks/exhaustive-deps
188
+ [project, reload, notify, quit, workspaceRoot, apps.length, env, envList]);
189
+ const run = useCallback((action) => {
190
+ setOverlay(null);
191
+ if (action.enabled && !action.enabled(project)) {
192
+ notify(`${t(action.label)}: ${action.detail?.(project) ?? t('not available')}`, 'warn');
193
+ return;
194
+ }
195
+ action.run(context).catch((error) => {
196
+ notify(error instanceof Error ? error.message : String(error), 'error');
197
+ });
198
+ }, [context, project, notify]);
199
+ const fetchVersions = useCallback(async (fresh = false) => {
200
+ const key = versionsKey;
201
+ const config = await resolveDeployConfig(context, fresh);
202
+ if (!config)
203
+ return;
204
+ setVersions(v => ({
205
+ ...v,
206
+ [key]: { ...v[key], loading: true, error: undefined },
207
+ }));
208
+ const result = await listExecutables(config);
209
+ setVersions(v => ({
210
+ ...v,
211
+ [key]: {
212
+ loading: false,
213
+ executables: result.executables ?? v[key]?.executables,
214
+ error: result.error,
215
+ fetchedAt: result.success ? Date.now() : v[key]?.fetchedAt,
216
+ },
217
+ }));
218
+ setVersionRow(0);
219
+ }, [versionsKey, context]);
220
+ const activateSelected = useCallback(async () => {
221
+ const executable = versions[versionsKey]?.executables?.[versionRow];
222
+ if (!executable)
223
+ return;
224
+ if (executable.active) {
225
+ notify(t('{v} is already active', { v: executable.appVersion }));
226
+ return;
227
+ }
228
+ const config = await resolveDeployConfig(context);
229
+ if (!config)
230
+ return;
231
+ const task = startActivate(project, config, executable.id, executable.appVersion);
232
+ const wait = () => new Promise(resolve => {
233
+ const check = () => task.status === 'running' ? setTimeout(check, 200) : resolve();
234
+ check();
235
+ });
236
+ await wait();
237
+ notify(task.error ?? t('{v} activated', { v: executable.appVersion }), task.error ? 'error' : 'ok');
238
+ await fetchVersions();
239
+ }, [versions, project, versionRow, context, notify, fetchVersions]);
240
+ const formActive = (tab === 'config' || settings) && focus === 'content';
241
+ const [editing, setEditing] = useState(false);
242
+ const pickAddon = useCallback(async () => new Promise(resolve => {
243
+ setOverlay({ kind: 'picker', resolve });
244
+ }), []);
245
+ const loadAddons = useCallback(async () => {
246
+ const config = await resolveDeployConfig(context);
247
+ return config ? listAddons(config) : { error: t('No credentials.') };
248
+ }, [context]);
249
+ const appTasks = tasks.filter(task => task.appRoot === project.root);
250
+ const logTask = appTasks.find(task => task.status === 'running') ?? appTasks.at(-1);
251
+ useInput((raw, key) => {
252
+ // Terminals speaking the kitty keyboard protocol report shift+s as
253
+ // "s" plus a shift flag; fold that back into the uppercase letter so
254
+ // P, K, R behave the same everywhere.
255
+ const input = key.shift && raw.length === 1 && /[a-z]/.test(raw)
256
+ ? raw.toUpperCase()
257
+ : raw;
258
+ if (key.escape) {
259
+ if (focus === 'nav' && filter) {
260
+ setFilter('');
261
+ return;
262
+ }
263
+ // Esc backs out of the workspace settings pane, not just its focus.
264
+ if (settings)
265
+ setSelected(0);
266
+ setFocus(single ? 'content' : 'nav');
267
+ return;
268
+ }
269
+ if (input === '/') {
270
+ setOverlay({ kind: 'palette' });
271
+ return;
272
+ }
273
+ if (key.tab) {
274
+ setFilter('');
275
+ setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
276
+ return;
277
+ }
278
+ // Navigator: typing searches, so no action key fires until Enter has
279
+ // moved the focus into the content pane.
280
+ if (focus === 'nav') {
281
+ const move = (delta) => {
282
+ setSelected(navMove(ring, selected, delta));
283
+ };
284
+ const search = (next) => {
285
+ setFilter(next);
286
+ const found = navMatches(apps, next);
287
+ if (found.length > 0 && !found.includes(selected))
288
+ setSelected(found[0]);
289
+ };
290
+ if (key.upArrow)
291
+ move(-1);
292
+ else if (key.downArrow)
293
+ move(1);
294
+ else if (key.return) {
295
+ if (matches.length > 0 || settings) {
296
+ setFilter('');
297
+ setFocus('content');
298
+ }
299
+ }
300
+ else if (key.backspace || key.delete)
301
+ search(filter.slice(0, -1));
302
+ else if (input === 'q' && !filter)
303
+ quit();
304
+ else if (input?.length === 1 && input >= ' ' && !key.ctrl && !key.meta)
305
+ search(filter + input);
306
+ return;
307
+ }
308
+ if (input === 'a' && tab !== 'versions') {
309
+ setTab('versions');
310
+ setFocus('content');
311
+ if (!Object.hasOwn(versions, versionsKey))
312
+ void fetchVersions(false);
313
+ return;
314
+ }
315
+ const digit = Number.parseInt(input, 10);
316
+ if (digit >= 1 && digit <= TABS.length) {
317
+ setTab(TABS[digit - 1].id);
318
+ setFocus('content');
319
+ return;
320
+ }
321
+ if (key.leftArrow || key.rightArrow) {
322
+ const i = TABS.findIndex(entry => entry.id === tab);
323
+ setTab(TABS[(i + (key.rightArrow ? 1 : TABS.length - 1)) % TABS.length].id);
324
+ return;
325
+ }
326
+ if (settings && focus === 'content') {
327
+ // Settings pane: the form owns everything but q, y and Tab/Esc above.
328
+ if (input === 'q')
329
+ quit();
330
+ else if (input === 'y') {
331
+ try {
332
+ if (syncDevPropertiesToPackageJson(workspaceRoot)) {
333
+ reload();
334
+ notify(t('package.json updated'), 'ok');
335
+ }
336
+ }
337
+ catch (error) {
338
+ notify(error instanceof Error ? error.message : String(error), 'error');
339
+ }
340
+ }
341
+ return;
342
+ }
343
+ if (tab === 'versions') {
344
+ const count = versions[versionsKey]?.executables?.length ?? 0;
345
+ if (key.upArrow)
346
+ setVersionRow(r => Math.max(0, r - 1));
347
+ if (key.downArrow)
348
+ setVersionRow(r => Math.min(Math.max(0, count - 1), r + 1));
349
+ if (input === 'R')
350
+ void fetchVersions(false);
351
+ else if (input === 'a' && count > 0)
352
+ void activateSelected();
353
+ }
354
+ else if (tab === 'log') {
355
+ const max = Math.max(0, (logTask?.lines.length ?? 0) - 1);
356
+ if (key.upArrow)
357
+ setLogScroll(s => Math.min(max, s + 1));
358
+ if (key.downArrow)
359
+ setLogScroll(s => Math.max(0, s - 1));
360
+ if (key.pageUp)
361
+ setLogScroll(s => Math.min(max, s + 10));
362
+ if (key.pageDown)
363
+ setLogScroll(s => Math.max(0, s - 10));
364
+ if (input === 'f')
365
+ setLogScroll(0);
366
+ else if (input === 'x')
367
+ setLogWrap(w => !w);
368
+ }
369
+ const action = actionForKey(input);
370
+ if (action)
371
+ run(action);
372
+ }, { isActive: overlay === null && !editing });
373
+ // Frame geometry: one row for Ink's trailing newline, top bar, bottom bar.
374
+ // The sidebar layout needs 10 rows to stack its own fixed rows without
375
+ // overflowing; the compact one has no sidebar and fits in 6.
376
+ const frameRows = Math.max(narrow ? 6 : 10, rows - 1);
377
+ const mainHeight = frameRows - 2;
378
+ const contentHeight = mainHeight - 1 - (narrow ? 1 : 0);
379
+ const groupOf = (app) => workspaceRoot ? appGroup(workspaceRoot, app.root) : '.';
380
+ const appName = appLabel(project);
381
+ const tabName = t(TABS.find(entry => entry.id === tab).label).toLowerCase();
382
+ const contextLabel = settings
383
+ ? `${t('workspace')} ▸ ${t('settings')}`
384
+ : workspaceRoot
385
+ ? `${t('workspace')} ▸ ${path.relative(workspaceRoot, project.root)} ▸ ${tabName}`
386
+ : `${appName} ▸ ${tabName}`;
387
+ const h = (pairs) => pairs.map(([key, label]) => ({ key, label: t(label) }));
388
+ const settingsHints = editing
389
+ ? h([
390
+ ['Enter', 'save'],
391
+ ['Esc', 'cancel'],
392
+ ])
393
+ : formActive
394
+ ? h([
395
+ ['↑↓', 'field'],
396
+ ['Enter', 'edit'],
397
+ ['y', 'sync'],
398
+ ['Esc', 'back'],
399
+ ['q', 'quit'],
400
+ ])
401
+ : h([
402
+ ['Enter', 'edit settings'],
403
+ ['↑↓', 'apps'],
404
+ ['q', 'quit'],
405
+ ]);
406
+ const navHints = filter
407
+ ? h([
408
+ ['↑↓', 'move'],
409
+ ['Enter', 'select'],
410
+ ['Esc', 'clear'],
411
+ ['/', 'commands'],
412
+ ])
413
+ : h([
414
+ ['a–z', 'search'],
415
+ ['↑↓', 'move'],
416
+ ['Enter', 'select'],
417
+ ['/', 'commands'],
418
+ ['q', 'quit'],
419
+ ]);
420
+ const hints = overlay
421
+ ? h([['Esc', 'cancel']])
422
+ : focus === 'nav' && !settings
423
+ ? navHints
424
+ : settings
425
+ ? settingsHints
426
+ : tab === 'versions'
427
+ ? h([
428
+ ['a', 'activate'],
429
+ ['R', 'refresh'],
430
+ ['p', 'deploy'],
431
+ ['/', 'commands'],
432
+ ['q', 'quit'],
433
+ ])
434
+ : tab === 'log'
435
+ ? h([
436
+ ['f', 'follow'],
437
+ ['x', 'wrap'],
438
+ ['K', 'stop'],
439
+ ['p', 'deploy'],
440
+ ['/', 'commands'],
441
+ ['q', 'quit'],
442
+ ])
443
+ : tab === 'config'
444
+ ? editing
445
+ ? h([
446
+ ['Enter', 'save'],
447
+ ['Esc', 'cancel'],
448
+ ])
449
+ : formActive
450
+ ? h([
451
+ ['↑↓', 'field'],
452
+ ['Enter', 'edit'],
453
+ ['^O', 'pick addon'],
454
+ ['y', 'sync'],
455
+ ['/', 'commands'],
456
+ ['q', 'quit'],
457
+ ])
458
+ : h([
459
+ ['Tab', 'edit'],
460
+ ['y', 'sync'],
461
+ ['l', 'login'],
462
+ ['/', 'commands'],
463
+ ['q', 'quit'],
464
+ ])
465
+ : h([
466
+ ['d', 'dev'],
467
+ ['w', 'watch'],
468
+ ['b', 'build'],
469
+ ['s', 'sign'],
470
+ ['p', 'deploy'],
471
+ ['a', 'activate'],
472
+ ['E', 'env'],
473
+ ['i', 'install'],
474
+ ['/', 'commands'],
475
+ ['q', 'quit'],
476
+ ]);
477
+ 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') }));
478
+ const closeOverlay = () => setOverlay(null);
479
+ const content = overlay ? (renderOverlay(overlay, {
480
+ project,
481
+ closeOverlay,
482
+ run,
483
+ notify,
484
+ loadAddons,
485
+ height: contentHeight,
486
+ rerender: tick,
487
+ openWorkspace: workspaceRoot
488
+ ? () => {
489
+ setOverlay(null);
490
+ setSelected(apps.length);
491
+ setFocus('content');
492
+ }
493
+ : undefined,
494
+ })) : settings && workspaceTarget ? (_jsx(ConfigForm, { project: workspaceTarget, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: async () => null, onSaved: () => {
495
+ reload();
496
+ notify(t('workspace config saved'), 'ok');
497
+ }, onEditingChange: setEditing }, "workspace")) : (_jsxs(_Fragment, { children: [tab === 'overview' && (_jsx(Overview, { project: project, tasks: tasks, height: contentHeight })), tab === 'config' && (_jsx(ConfigForm, { project: {
498
+ root: project.root,
499
+ devProperties: project.devProperties,
500
+ base: rawProject.devProperties,
501
+ environment: env,
502
+ workspaceRoot,
503
+ }, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: pickAddon, onSaved: () => {
504
+ reload();
505
+ notify(t('config saved'), 'ok');
506
+ }, 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 }))] }));
507
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
508
+ name: env,
509
+ color: environmentColor(env, rawProject.devProperties),
510
+ }, 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)
511
+ ? t(' · new workspace: fill in once, every app inherits · Esc skips')
512
+ : 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 })] }));
513
+ }
514
+ function renderOverlay(overlay, env) {
515
+ const { project, closeOverlay, run, notify, loadAddons, height, rerender, openWorkspace, } = env;
516
+ switch (overlay.kind) {
517
+ case 'palette':
518
+ return (_jsx(CommandPalette, { project: project, onRun: run, onClose: closeOverlay, height: height }));
519
+ case 'password':
520
+ return (_jsx(PasswordInput, { label: overlay.label, showRememberOption: Boolean(overlay.rememberLabel), rememberLabel: overlay.rememberLabel ? `${overlay.rememberLabel}: ` : undefined, onSubmit: (password, remember) => {
521
+ closeOverlay();
522
+ overlay.resolve({ password, remember });
523
+ }, onCancel: () => {
524
+ closeOverlay();
525
+ overlay.resolve(null);
526
+ } }, overlay.label));
527
+ case 'login':
528
+ return (_jsx(AuthLoginScreen, { method: overlay.method, devProperties: overlay.devProperties, onComplete: credential => {
529
+ closeOverlay();
530
+ overlay.resolve(credential);
531
+ }, onError: message => {
532
+ closeOverlay();
533
+ notify(message, 'error');
534
+ overlay.resolve(null);
535
+ }, onCancel: () => {
536
+ closeOverlay();
537
+ overlay.resolve(null);
538
+ } }));
539
+ case 'confirm':
540
+ return (_jsx(Confirm, { message: overlay.message, onAnswer: answer => {
541
+ closeOverlay();
542
+ overlay.resolve(answer);
543
+ } }));
544
+ case 'prompt':
545
+ return (_jsx(TextPrompt, { label: overlay.label, onSubmit: value => {
546
+ closeOverlay();
547
+ overlay.resolve(value);
548
+ }, onCancel: () => {
549
+ closeOverlay();
550
+ overlay.resolve(null);
551
+ } }));
552
+ case 'settings':
553
+ return (_jsx(SettingsScreen, { onChanged: rerender, onClose: closeOverlay, onOpenWorkspace: openWorkspace }));
554
+ case 'picker':
555
+ return (_jsx(AddonPicker, { domain: project.devProperties?.domain ?? '', appType: appTypeOf(project.manifest), initialQuery: localizedText(project.manifest.name), load: loadAddons, height: height, onSelect: name => {
556
+ closeOverlay();
557
+ overlay.resolve(name);
558
+ }, onClose: () => {
559
+ closeOverlay();
560
+ overlay.resolve(null);
561
+ } }));
562
+ }
563
+ }
564
+ function Confirm({ message, onAnswer, }) {
565
+ useInput(input => {
566
+ if (input === 'y' || input === 'Y')
567
+ onAnswer(true);
568
+ else if (input === 'n' || input === 'N')
569
+ onAnswer(false);
570
+ });
571
+ 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') })] }));
572
+ }
573
+ function TextPrompt({ label, onSubmit, onCancel, }) {
574
+ const [value, setValue] = useState('');
575
+ useInput((input, key) => {
576
+ if (key.escape)
577
+ onCancel();
578
+ else if (key.return)
579
+ onSubmit(value);
580
+ else if (key.backspace || key.delete)
581
+ setValue(v => v.slice(0, -1));
582
+ else if (input && !key.ctrl && !key.meta)
583
+ setValue(v => v + input);
584
+ });
585
+ 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') })] }));
586
+ }
@@ -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;