sitevision-cli 1.0.0-beta.13 → 1.0.0-beta.15

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