sitevision-cli 1.0.0-beta.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +194 -70
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +95 -212
  7. package/dist/commands/dev.js +87 -416
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +50 -33
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +5 -31
  13. package/dist/components/AnimatedLogo.js +8 -2
  14. package/dist/components/AuthLoginScreen.js +33 -15
  15. package/dist/components/PasswordInput.js +2 -1
  16. package/dist/scaffold/inquirer-stub.js +119 -0
  17. package/dist/scaffold/register.js +15 -0
  18. package/dist/shell/AddonPicker.js +54 -0
  19. package/dist/shell/Changelog.js +44 -0
  20. package/dist/shell/CommandPalette.js +63 -0
  21. package/dist/shell/ConfigForm.js +703 -0
  22. package/dist/shell/Frame.js +138 -0
  23. package/dist/shell/Help.js +96 -0
  24. package/dist/shell/Popover.js +15 -0
  25. package/dist/shell/Settings.js +149 -0
  26. package/dist/shell/Shell.js +953 -0
  27. package/dist/shell/Tabs.js +96 -0
  28. package/dist/shell/actions.js +493 -0
  29. package/dist/utils/config.js +62 -2
  30. package/dist/utils/create-app.js +231 -0
  31. package/dist/utils/debug.js +32 -0
  32. package/dist/utils/environments.js +74 -0
  33. package/dist/utils/i18n.js +369 -0
  34. package/dist/utils/jsonc.js +2 -1
  35. package/dist/utils/keychain.js +54 -6
  36. package/dist/utils/oauth2-auth.js +146 -43
  37. package/dist/utils/password-prompt.js +37 -3
  38. package/dist/utils/process-runner.js +77 -38
  39. package/dist/utils/project-detection.js +457 -112
  40. package/dist/utils/session-cookie-auth.js +10 -6
  41. package/dist/utils/sitevision-api.js +162 -32
  42. package/dist/utils/sitevision-scripts-runner.js +50 -22
  43. package/dist/utils/tasks.js +510 -0
  44. package/dist/utils/version-check.js +3 -3
  45. package/dist/utils/webpack-runner.js +9 -5
  46. package/dist/utils/workspace.js +105 -0
  47. package/dist/utils/zip.js +18 -19
  48. package/package.json +27 -4
  49. package/readme.md +113 -141
  50. package/dist/app.d.ts +0 -6
  51. package/dist/app.js +0 -307
  52. package/dist/cli.d.ts +0 -2
  53. package/dist/commands/build.d.ts +0 -12
  54. package/dist/commands/deploy.d.ts +0 -16
  55. package/dist/commands/dev.d.ts +0 -16
  56. package/dist/commands/index.d.ts +0 -4
  57. package/dist/commands/info.d.ts +0 -2
  58. package/dist/commands/setup-signing.d.ts +0 -2
  59. package/dist/commands/sign.d.ts +0 -14
  60. package/dist/commands/types.d.ts +0 -18
  61. package/dist/commands/watch.d.ts +0 -2
  62. package/dist/components/AnimatedLogo.d.ts +0 -10
  63. package/dist/components/AuthLoginScreen.d.ts +0 -21
  64. package/dist/components/DevPropertiesForm.d.ts +0 -10
  65. package/dist/components/DevPropertiesForm.js +0 -210
  66. package/dist/components/InfoScreen.d.ts +0 -7
  67. package/dist/components/InfoScreen.js +0 -14
  68. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  69. package/dist/components/KeychainPasswordChoice.js +0 -30
  70. package/dist/components/MainMenu.d.ts +0 -7
  71. package/dist/components/MainMenu.js +0 -83
  72. package/dist/components/PasswordInput.d.ts +0 -10
  73. package/dist/components/ProcessOutput.d.ts +0 -6
  74. package/dist/components/ProcessOutput.js +0 -30
  75. package/dist/components/SetupFlow.d.ts +0 -8
  76. package/dist/components/SetupFlow.js +0 -233
  77. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  78. package/dist/components/SigningPropertiesForm.js +0 -38
  79. package/dist/components/StatusIndicator.d.ts +0 -8
  80. package/dist/components/TextInput.d.ts +0 -10
  81. package/dist/components/TextInput.js +0 -29
  82. package/dist/components/WelcomeScreen.d.ts +0 -14
  83. package/dist/types/index.d.ts +0 -284
  84. package/dist/utils/branding.d.ts +0 -28
  85. package/dist/utils/config.d.ts +0 -17
  86. package/dist/utils/jsonc.d.ts +0 -19
  87. package/dist/utils/keychain.d.ts +0 -15
  88. package/dist/utils/oauth2-auth.d.ts +0 -27
  89. package/dist/utils/password-prompt.d.ts +0 -14
  90. package/dist/utils/process-runner.d.ts +0 -22
  91. package/dist/utils/project-detection.d.ts +0 -151
  92. package/dist/utils/session-cookie-auth.d.ts +0 -34
  93. package/dist/utils/signing-password.d.ts +0 -12
  94. package/dist/utils/signing-step.d.ts +0 -15
  95. package/dist/utils/signing-step.js +0 -16
  96. package/dist/utils/sitevision-api.d.ts +0 -112
  97. package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
  98. package/dist/utils/version-check.d.ts +0 -9
  99. package/dist/utils/webpack-runner.d.ts +0 -76
  100. package/dist/utils/zip.d.ts +0 -71
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } 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 { fuzzyMatch } from './actions.js';
6
+ import { t } from '../utils/i18n.js';
7
+ export const ACCENT = 'cyan';
8
+ export const NARROW_BELOW = 100;
9
+ /** Navigator width for a terminal: a quarter of the columns, within 32..48. */
10
+ export function navWidth(columns) {
11
+ return Math.min(48, Math.max(32, Math.floor(columns / 4)));
12
+ }
13
+ export function TopBar({ context, domain, auth, version, environment, }) {
14
+ // Narrow terminals squeeze the domain first, then the context, then the
15
+ // login. `svc` and the environment badge never give way: the badge is the
16
+ // one thing that says a deploy goes to production.
17
+ return (_jsxs(Box, { paddingX: 1, height: 1, overflow: "hidden", children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { bold: true, color: ACCENT, children: "svc" }), environment && (_jsxs(Text, { children: [' ', _jsx(Text, { backgroundColor: environment.color, color: "black", bold: true, children: ` ${environment.name.toUpperCase()} ` })] }))] }), _jsx(Box, { flexShrink: 2, flexGrow: 1, marginRight: 2, minWidth: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', context] }) }), domain && (_jsx(Box, { flexShrink: 100, minWidth: 0, marginRight: 1, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: domain }) })), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: auth.ready ? 'green' : 'yellow', children: auth.ready ? '●' : '○' }), _jsxs(Text, { children: [" ", auth.label] })] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: [" v", version] }) })] }));
18
+ }
19
+ const TYPE_GLYPH = {
20
+ web: 'web',
21
+ widget: 'wgt',
22
+ rest: 'rst',
23
+ mcp: 'mcp',
24
+ };
25
+ /** Three-letter type marker; unknown manifest types show as "???". */
26
+ export function typeGlyph(manifest) {
27
+ const type = appTypeOf(manifest);
28
+ return type ? TYPE_GLYPH[type] : '???';
29
+ }
30
+ export function appStatus(project) {
31
+ const sync = getPackageJsonSyncChanges(project.root).length;
32
+ return {
33
+ deps: project.hasNodeModules,
34
+ config: Boolean(project.devProperties),
35
+ sync,
36
+ signing: project.hasSigningProperties,
37
+ scriptsWarning: undefined,
38
+ };
39
+ }
40
+ function Dots({ project }) {
41
+ const s = appStatus(project);
42
+ const dot = (ok, warn = false) => (_jsx(Text, { color: ok ? 'green' : warn ? 'yellow' : 'red', children: "\u25CF" }));
43
+ return (_jsxs(Text, { children: [dot(s.deps), dot(s.config), dot(s.sync === 0, s.sync > 0), dot(s.signing, !s.signing)] }));
44
+ }
45
+ /** The name shown for an app in the navigator, and what the filter matches. */
46
+ export function appLabel(app) {
47
+ return localizedText(app.manifest.name) || app.manifest.id;
48
+ }
49
+ /** Indices into `apps` whose label fuzzy-matches the filter. */
50
+ export function navMatches(apps, filter) {
51
+ return apps
52
+ .map((_, index) => index)
53
+ .filter(index => fuzzyMatch(filter, appLabel(apps[index])));
54
+ }
55
+ /** Next selectable index when moving by `delta`, wrapping at both ends. */
56
+ export function navMove(ring, selected, delta) {
57
+ if (ring.length === 0)
58
+ return selected;
59
+ const at = ring.indexOf(selected);
60
+ return ring[at === -1 ? 0 : (at + delta + ring.length) % ring.length];
61
+ }
62
+ export function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected = false, width, filter = '', }) {
63
+ // Row: marker(1) glyph(3) sp name sp version(6) sp dots(4) inside padding + border.
64
+ const rowWidth = width - 3;
65
+ const nameWidth = rowWidth - 17;
66
+ const running = tasks.filter(task => task.status === 'running');
67
+ // Every row is exactly one line; nothing may shrink or the rows overlap.
68
+ const rows = [];
69
+ const rowApp = [];
70
+ let lastGroup;
71
+ for (const [index, app] of apps.entries()) {
72
+ const group = groupOf(app);
73
+ if (!single && group !== lastGroup) {
74
+ rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', group] }) }, `g-${index}-${group}`));
75
+ rowApp.push(-1);
76
+ lastGroup = group;
77
+ }
78
+ rowApp.push(index);
79
+ const active = index === selected;
80
+ const busy = running.some(task => task.appRoot === app.root);
81
+ const name = appLabel(app);
82
+ rows.push(_jsxs(Box, { width: rowWidth, height: 1, flexShrink: 0, children: [_jsxs(Text, { backgroundColor: active && focused ? ACCENT : undefined, color: active && focused ? 'black' : undefined, bold: active, wrap: "truncate", children: [active ? '▎' : ' ', _jsx(Text, { dimColor: !active, children: typeGlyph(app.manifest) }), ' ', name.padEnd(nameWidth).slice(0, nameWidth), ' ', _jsx(Text, { dimColor: true, children: app.manifest.version.padStart(6).slice(0, 6) }), ' '] }), busy ? (_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) })) : (_jsx(Dots, { project: app }))] }, app.root));
83
+ }
84
+ if (rows.length === 0) {
85
+ rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: ' ' + t('no matches') }) }, "none"));
86
+ rowApp.push(-1);
87
+ }
88
+ // Window the list so the selected app stays visible; the lines outside
89
+ // are summarised as "… n more".
90
+ // Top border, header and legend, then the settings row and running tasks.
91
+ const fixed = 3 + (single ? 0 : 2) + (running.length > 0 ? running.length + 2 : 0);
92
+ const avail = Math.max(3, height - fixed);
93
+ let shown = rows;
94
+ if (rows.length > avail) {
95
+ const target = settingsSelected
96
+ ? rows.length - 1
97
+ : rowApp.indexOf(selected);
98
+ const start = Math.max(0, Math.min(target - Math.floor(avail / 2), rows.length - avail));
99
+ const end = start + avail;
100
+ shown = rows.slice(start, end);
101
+ const more = (n, arrow) => (_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: [' ', t('{arrow} {n} more', { arrow, n })] }) }, `more-${arrow}`));
102
+ if (start > 0)
103
+ shown[0] = more(start, '↑');
104
+ if (end < rows.length)
105
+ shown[shown.length - 1] = more(rows.length - end, '↓');
106
+ }
107
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderColor: focused ? ACCENT : undefined, borderDimColor: !focused, borderBottom: false, borderLeft: false, paddingX: 1, overflow: "hidden", children: [filter ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), filter, _jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { dimColor: true, children: [' ', apps.length === 1
108
+ ? t('1 match')
109
+ : t('{n} matches', { n: apps.length })] })] })) : (_jsx(Text, { bold: true, color: focused ? ACCENT : undefined, dimColor: !focused, children: single
110
+ ? t('APP')
111
+ : apps.length === 1
112
+ ? t('WORKSPACE 1 app')
113
+ : t('WORKSPACE {n} apps', { n: apps.length }) })), shown, _jsx(Text, { dimColor: true, children: ' ' + t('deps·config·sync·signing') }), !single && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: settingsSelected && focused ? ACCENT : undefined, color: settingsSelected && focused ? 'black' : undefined, bold: settingsSelected, children: [settingsSelected ? '▎' : ' ', "\u2699 ", t('Workspace settings')] }) })), running.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('TASKS') }), running.map(task => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', task.label, " ", task.appName, " ", _jsx(Text, { dimColor: true, children: elapsed(task) })] }, task.id)))] }))] }));
114
+ }
115
+ export function NavigatorStrip({ apps, selected, focused, width, filter = '', }) {
116
+ const label = (app) => ` ${typeGlyph(app.manifest)} ${appLabel(app)} `;
117
+ // Scroll the strip so the selection stays on screen: walk left from it
118
+ // until the row is full.
119
+ const budget = width - 2 - (filter ? filter.length + 5 : 0);
120
+ const from = Math.min(Math.max(0, selected), apps.length - 1);
121
+ let start = from;
122
+ let used = 0;
123
+ for (let i = from; i >= 0; i--) {
124
+ used += label(apps[i]).length;
125
+ if (used > budget)
126
+ break;
127
+ start = i;
128
+ }
129
+ return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { wrap: "truncate", children: [filter && (_jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), filter, _jsx(Text, { dimColor: true, children: " \u00B7" })] })), apps.slice(start).map((app, index) => (_jsx(Text, { backgroundColor: index + start === selected && focused ? ACCENT : undefined, color: index + start === selected && focused ? 'black' : undefined, bold: index + start === selected, children: label(app) }, app.root)))] }) }));
130
+ }
131
+ export function elapsed(task) {
132
+ const ms = (task.endedAt ?? Date.now()) - task.startedAt;
133
+ const s = Math.round(ms / 1000);
134
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
135
+ }
136
+ export function BottomBar({ hints, right }) {
137
+ return (_jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { wrap: "truncate", children: hints.map(h => (_jsxs(Text, { children: [_jsx(Text, { bold: true, color: ACCENT, children: h.key }), _jsxs(Text, { dimColor: true, children: [" ", h.label, " "] })] }, h.key + h.label))) }), right] }));
138
+ }
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Box, Text, measureElement, useInput } from 'ink';
4
+ import { t } from '../utils/i18n.js';
5
+ import { ACCENT } from './Frame.js';
6
+ import { actions } from './actions.js';
7
+ import { GROUPS } from './CommandPalette.js';
8
+ // What the pane you are in does, beyond the key list. One entry per paragraph.
9
+ const GUIDE = {
10
+ nav: {
11
+ title: 'Navigator',
12
+ text: [
13
+ 'Typing filters the app list. ↑↓ move, Enter selects the app and moves focus to the content pane. Action keys such as d or p only work there; in the navigator they search.',
14
+ 'The last row is Workspace settings: the shared root config that every app inherits.',
15
+ ],
16
+ },
17
+ workspace: {
18
+ title: 'Workspace settings',
19
+ text: [
20
+ "The root .dev_properties.json shared by every app, with the root package.json defaults underneath. Fill in site, auth and environments once; an app's own config overrides only what differs.",
21
+ 'Same keys as the Config tab: Enter edits and saves, Esc cancels, ←→ or space cycles choices.',
22
+ ],
23
+ },
24
+ overview: {
25
+ title: 'Overview',
26
+ text: [
27
+ 'Each status line names the key that fixes it: i installs dependencies, e opens Config, y syncs package.json, / sets up signing.',
28
+ 'The badge in the top bar is the active environment: green for the base, yellow for others, red for production. v cycles it.',
29
+ ],
30
+ },
31
+ config: {
32
+ title: 'Config',
33
+ text: [
34
+ 'Values come in layers and the source is shown next to each: your local .dev_properties.json wins, shared package.json values sit underneath, and in a workspace the root config underneath that. Environments add on top of the base and override only what differs.',
35
+ 'Enter edits and saves a field, Esc cancels, ←→ or space cycles choices, Ctrl+O lists the addons on the site. y copies shared values into package.json for the team.',
36
+ 'Passwords and secrets go to the OS keychain, never to a file. Leave them empty to be asked on each run.',
37
+ ],
38
+ },
39
+ versions: {
40
+ title: 'Versions',
41
+ text: [
42
+ "The versions uploaded to the addon on the active environment's site, the domain in the top bar. v switches environment.",
43
+ '↑↓ select, a activates the selected version, r refreshes. Production deploys activate on their own.',
44
+ ],
45
+ },
46
+ log: {
47
+ title: 'Log',
48
+ text: [
49
+ 'Output from build, sign, deploy, dev and watch for the selected app. Dev and watch keep running while you switch apps; K stops them.',
50
+ '↑↓ scroll, PgUp/PgDn page, f jumps to the end, x toggles line wrap.',
51
+ ],
52
+ },
53
+ };
54
+ const MOVE = [
55
+ ['Tab', 'switch pane'],
56
+ ['1–4', 'tabs'],
57
+ ['←→', 'tabs'],
58
+ ['/', 'commands'],
59
+ ['?', 'help'],
60
+ ['Esc', 'back'],
61
+ ];
62
+ function Row({ keys, label }) {
63
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: ACCENT, children: keys.padEnd(6) }), label] }));
64
+ }
65
+ export function HelpPanel({ here, where, height, onClose, }) {
66
+ const inner = useRef(null);
67
+ const [top, setTop] = useState(0);
68
+ const [total, setTotal] = useState(0);
69
+ // The text depends on the pane and the language, both fixed while this is open.
70
+ useEffect(() => {
71
+ if (inner.current)
72
+ setTotal(measureElement(inner.current).height);
73
+ }, [here, where, height]);
74
+ const visible = Math.max(1, height - 1);
75
+ const max = Math.max(0, total - visible);
76
+ useInput((input, key) => {
77
+ if (key.escape || key.return || input === '?' || input === 'q')
78
+ onClose();
79
+ else if (key.upArrow)
80
+ setTop(n => Math.max(0, n - 1));
81
+ else if (key.downArrow)
82
+ setTop(n => Math.min(max, n + 1));
83
+ else if (key.pageUp)
84
+ setTop(n => Math.max(0, n - visible));
85
+ else if (key.pageDown)
86
+ setTop(n => Math.min(max, n + visible));
87
+ });
88
+ // Only keys not already listed elsewhere, e.g. f and x on the Log tab.
89
+ const local = here.filter(hint => actions.every(action => action.key !== hint.key) &&
90
+ MOVE.every(([keys]) => keys !== hint.key));
91
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: height, overflow: "hidden", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: t(GUIDE[where].title) }), _jsxs(Text, { dimColor: true, children: [' · ', max > 0 ? t('↑↓ scroll · Esc close') : t('Esc close')] })] }), _jsx(Box, { flexDirection: "column", overflow: "hidden", height: visible, children: _jsxs(Box, { ref: inner, flexDirection: "column", flexShrink: 0, marginTop: -top, children: [GUIDE[where].text.map(line => (_jsx(Text, { wrap: "wrap", children: t(line) }, line))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: t('Keys') }) }), _jsxs(Box, { flexWrap: "wrap", columnGap: 4, children: [_jsx(Box, { flexDirection: "column", children: GROUPS.map(group => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t(group.label) }), actions.flatMap(action => action.group === group.id && action.key
92
+ ? [
93
+ _jsx(Row, { keys: action.key, label: t(action.label) }, action.id),
94
+ ]
95
+ : [])] }, group.id))) }), _jsxs(Box, { flexDirection: "column", children: [local.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('Here') }), local.map(hint => (_jsx(Row, { keys: hint.key, label: hint.label }, hint.key + hint.label)))] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t('Move around') }), MOVE.map(([keys, label]) => (_jsx(Row, { keys: keys, label: t(label) }, keys + label)))] })] })] })] }) })] }));
96
+ }
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { ACCENT } from './Frame.js';
4
+ /**
5
+ * A bordered box floating centred over the frame. Render it as the last child
6
+ * of the root so it draws on top; Ink paints in tree order.
7
+ */
8
+ export function Popover({ columns, rows, width, height, children, }) {
9
+ const w = Math.max(20, Math.min(width, columns - 4));
10
+ const h = Math.max(6, Math.min(height, rows - 2));
11
+ // Ink only overwrites cells it writes to, so blank the interior first or the
12
+ // content underneath shows through around shorter lines.
13
+ const blank = ' '.repeat(w - 2);
14
+ return (_jsxs(Box, { position: "absolute", top: Math.floor((rows - h) / 2), left: Math.floor((columns - w) / 2), width: w, height: h, flexDirection: "column", borderStyle: "round", borderColor: ACCENT, children: [_jsx(Box, { position: "absolute", flexDirection: "column", children: Array.from({ length: h - 2 }, (_, i) => (_jsx(Text, { children: blank }, i))) }), _jsx(Box, { flexDirection: "column", height: h - 2, overflow: "hidden", children: children })] }));
15
+ }
@@ -0,0 +1,149 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { configProblem, getGlobalSigning, getSettings, setGlobalSigning, setSettings, settingsFile, } from '../utils/config.js';
5
+ import { LANGUAGES, LANGUAGE_NAMES, setLanguage, t, } from '../utils/i18n.js';
6
+ import { ACCENT } from './Frame.js';
7
+ const ROWS = [
8
+ {
9
+ key: 'language',
10
+ label: 'Language',
11
+ help: 'The language svc itself speaks.',
12
+ options: LANGUAGES,
13
+ display: value => LANGUAGE_NAMES[value],
14
+ },
15
+ {
16
+ key: 'introAnimation',
17
+ label: 'Intro animation',
18
+ help: 'Play the logo animation when the shell starts.',
19
+ options: ['on', 'off'],
20
+ display: value => t(value),
21
+ },
22
+ {
23
+ key: 'updateCheck',
24
+ label: 'Update check',
25
+ help: 'Look for a newer svc on npm at start.',
26
+ options: ['on', 'off'],
27
+ display: value => t(value),
28
+ },
29
+ {
30
+ key: 'addonNameDriftWarning',
31
+ label: 'Addon name warning',
32
+ help: 'Mark the addon name with ≠ manifest when it is none of the names in manifest.json.',
33
+ options: ['on', 'off'],
34
+ display: value => t(value),
35
+ },
36
+ {
37
+ key: 'signingUsername',
38
+ label: 'Default signing user',
39
+ help: 'The developer.sitevision.se account that signs in every project without a signing user of its own.',
40
+ options: [],
41
+ display: value => value,
42
+ },
43
+ {
44
+ key: 'certificateName',
45
+ label: 'Default certificate',
46
+ help: 'Which certificate signs when the account has several. Empty = the account default.',
47
+ options: [],
48
+ display: value => value,
49
+ },
50
+ ];
51
+ /** Global preferences, saved per field like the Config tab. */
52
+ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
53
+ const [values, setValues] = useState(() => {
54
+ const s = getSettings();
55
+ const signing = getGlobalSigning();
56
+ return {
57
+ language: s.language,
58
+ introAnimation: s.introAnimation ? 'on' : 'off',
59
+ updateCheck: s.updateCheck ? 'on' : 'off',
60
+ addonNameDriftWarning: s.addonNameDriftWarning ? 'on' : 'off',
61
+ signingUsername: signing.signingUsername ?? '',
62
+ certificateName: signing.certificateName ?? '',
63
+ };
64
+ });
65
+ const [cursor, setCursor] = useState(0);
66
+ const [editing, setEditing] = useState(false);
67
+ const [draft, setDraft] = useState('');
68
+ const [note, setNote] = useState(() => configProblem()
69
+ ? t('The settings file does not parse and is ignored until it is fixed.')
70
+ : '');
71
+ const rowCount = ROWS.length + (onOpenWorkspace ? 1 : 0);
72
+ const onWorkspaceRow = cursor === ROWS.length;
73
+ const current = ROWS[Math.min(cursor, ROWS.length - 1)];
74
+ const commit = (value) => {
75
+ const next = { ...values, [current.key]: value };
76
+ setValues(next);
77
+ if (current.key === 'language')
78
+ setLanguage(value);
79
+ setSettings({
80
+ language: next.language,
81
+ introAnimation: next.introAnimation === 'on',
82
+ updateCheck: next.updateCheck === 'on',
83
+ addonNameDriftWarning: next.addonNameDriftWarning === 'on',
84
+ });
85
+ setGlobalSigning({
86
+ signingUsername: next.signingUsername.trim() || undefined,
87
+ certificateName: next.certificateName.trim() || undefined,
88
+ });
89
+ setNote(configProblem()
90
+ ? t('Not saved: the settings file does not parse.')
91
+ : t('Saved.'));
92
+ onChanged();
93
+ };
94
+ useInput((input, key) => {
95
+ if (editing) {
96
+ if (key.escape) {
97
+ setEditing(false);
98
+ }
99
+ else if (key.return) {
100
+ setEditing(false);
101
+ if (draft !== values[current.key])
102
+ commit(draft);
103
+ }
104
+ else if (current.options.length === 0) {
105
+ if (key.backspace || key.delete)
106
+ setDraft(d => d.slice(0, -1));
107
+ else if (input && !key.ctrl && !key.meta)
108
+ setDraft(d => d + input);
109
+ }
110
+ else {
111
+ const step = key.leftArrow || key.upArrow
112
+ ? -1
113
+ : key.rightArrow || key.downArrow || input === ' '
114
+ ? 1
115
+ : 0;
116
+ if (step !== 0) {
117
+ const i = current.options.indexOf(draft);
118
+ const n = current.options.length;
119
+ setDraft(current.options[(i + step + n) % n]);
120
+ }
121
+ }
122
+ return;
123
+ }
124
+ if (key.escape)
125
+ onClose();
126
+ else if (key.downArrow || key.tab)
127
+ setCursor(c => (c + 1) % rowCount);
128
+ else if (key.upArrow)
129
+ setCursor(c => (c - 1 + rowCount) % rowCount);
130
+ else if (key.return) {
131
+ if (onWorkspaceRow) {
132
+ onOpenWorkspace?.();
133
+ return;
134
+ }
135
+ setDraft(values[current.key]);
136
+ setEditing(true);
137
+ }
138
+ });
139
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: t('Settings') }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t('stored in {file}', { file: settingsFile() })] })] }), ROWS.map((row, i) => {
140
+ const focused = i === cursor && !onWorkspaceRow;
141
+ const typing = focused && editing;
142
+ const chosen = typing ? draft : values[row.key];
143
+ return (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(row.label).padEnd(24) }), row.options.length === 0 && (_jsxs(Text, { dimColor: !typing && !chosen, children: [chosen || (typing ? '' : t('not set')), typing && _jsx(Text, { inverse: true, children: " " })] })), _jsx(Text, { children: row.options.map((option, j) => (_jsxs(Text, { children: [_jsx(Text, { bold: option === chosen, color: option === chosen ? ACCENT : undefined, inverse: typing && option === chosen, dimColor: option !== chosen, children: row.display(option) }), j < row.options.length - 1 && _jsx(Text, { dimColor: true, children: " \u00B7 " })] }, option))) })] }, row.key));
144
+ }), onOpenWorkspace && (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: onWorkspaceRow ? ACCENT : undefined, bold: onWorkspaceRow, dimColor: !onWorkspaceRow, children: (onWorkspaceRow ? '▸ ' : ' ') + t('Workspace config').padEnd(24) }), _jsx(Text, { dimColor: true, children: t('shared auth and site config for every app') })] })), _jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: onWorkspaceRow ? ' ' : t(current.help) }) }), _jsx(Text, { color: "yellow", children: note }), _jsx(Text, { dimColor: true, children: editing
145
+ ? current.options.length === 0
146
+ ? t('Enter confirm · Esc cancel')
147
+ : t('←→ choose · Enter confirm · Esc cancel')
148
+ : t('↑↓ setting · Enter edit · Esc close') })] }));
149
+ }