sitevision-cli 1.0.0-beta.32 → 1.0.0-beta.33

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 (108) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +56 -35
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +85 -238
  7. package/dist/commands/dev.js +34 -37
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +43 -28
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +0 -8
  13. package/dist/components/AuthLoginScreen.js +27 -12
  14. package/dist/shell/ConfigForm.js +90 -29
  15. package/dist/shell/Frame.js +4 -1
  16. package/dist/shell/Help.js +2 -1
  17. package/dist/shell/Settings.js +59 -6
  18. package/dist/shell/Shell.js +124 -41
  19. package/dist/shell/Tabs.js +8 -2
  20. package/dist/shell/actions.js +0 -0
  21. package/dist/utils/config.js +46 -3
  22. package/dist/utils/create-app.js +9 -2
  23. package/dist/utils/i18n.js +36 -13
  24. package/dist/utils/jsonc.js +2 -1
  25. package/dist/utils/keychain.js +44 -4
  26. package/dist/utils/oauth2-auth.js +35 -7
  27. package/dist/utils/password-prompt.js +23 -3
  28. package/dist/utils/process-runner.js +4 -2
  29. package/dist/utils/project-detection.js +84 -33
  30. package/dist/utils/session-cookie-auth.js +5 -4
  31. package/dist/utils/sitevision-api.js +21 -14
  32. package/dist/utils/sitevision-scripts-runner.js +32 -15
  33. package/dist/utils/tasks.js +130 -64
  34. package/dist/utils/version-check.js +3 -3
  35. package/dist/utils/webpack-runner.js +6 -5
  36. package/dist/utils/workspace.js +41 -3
  37. package/dist/utils/zip.js +18 -19
  38. package/package.json +24 -3
  39. package/readme.md +11 -4
  40. package/dist/app.d.ts +0 -6
  41. package/dist/app.js +0 -321
  42. package/dist/cli.d.ts +0 -2
  43. package/dist/commands/build.d.ts +0 -12
  44. package/dist/commands/deploy.d.ts +0 -17
  45. package/dist/commands/dev.d.ts +0 -14
  46. package/dist/commands/index.d.ts +0 -4
  47. package/dist/commands/info.d.ts +0 -2
  48. package/dist/commands/setup-signing.d.ts +0 -2
  49. package/dist/commands/sign.d.ts +0 -14
  50. package/dist/commands/types.d.ts +0 -18
  51. package/dist/commands/watch.d.ts +0 -2
  52. package/dist/components/AnimatedLogo.d.ts +0 -10
  53. package/dist/components/AuthLoginScreen.d.ts +0 -21
  54. package/dist/components/DevPropertiesForm.d.ts +0 -11
  55. package/dist/components/DevPropertiesForm.js +0 -246
  56. package/dist/components/InfoScreen.d.ts +0 -7
  57. package/dist/components/InfoScreen.js +0 -14
  58. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  59. package/dist/components/KeychainPasswordChoice.js +0 -30
  60. package/dist/components/MainMenu.d.ts +0 -7
  61. package/dist/components/MainMenu.js +0 -88
  62. package/dist/components/PasswordInput.d.ts +0 -10
  63. package/dist/components/ProcessOutput.d.ts +0 -6
  64. package/dist/components/ProcessOutput.js +0 -30
  65. package/dist/components/SetupFlow.d.ts +0 -8
  66. package/dist/components/SetupFlow.js +0 -233
  67. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  68. package/dist/components/SigningPropertiesForm.js +0 -38
  69. package/dist/components/StatusIndicator.d.ts +0 -8
  70. package/dist/components/TextInput.d.ts +0 -10
  71. package/dist/components/TextInput.js +0 -29
  72. package/dist/components/WelcomeScreen.d.ts +0 -14
  73. package/dist/scaffold/inquirer-stub.d.ts +0 -6
  74. package/dist/scaffold/register.d.ts +0 -1
  75. package/dist/shell/AddonPicker.d.ts +0 -14
  76. package/dist/shell/Changelog.d.ts +0 -10
  77. package/dist/shell/CommandPalette.d.ts +0 -12
  78. package/dist/shell/ConfigForm.d.ts +0 -42
  79. package/dist/shell/Frame.d.ts +0 -59
  80. package/dist/shell/Help.d.ts +0 -9
  81. package/dist/shell/Popover.d.ts +0 -12
  82. package/dist/shell/Settings.d.ts +0 -6
  83. package/dist/shell/Shell.d.ts +0 -11
  84. package/dist/shell/Tabs.d.ts +0 -36
  85. package/dist/shell/actions.d.ts +0 -49
  86. package/dist/types/index.d.ts +0 -295
  87. package/dist/utils/branding.d.ts +0 -28
  88. package/dist/utils/config.d.ts +0 -27
  89. package/dist/utils/create-app.d.ts +0 -53
  90. package/dist/utils/environments.d.ts +0 -20
  91. package/dist/utils/i18n.d.ts +0 -12
  92. package/dist/utils/jsonc.d.ts +0 -19
  93. package/dist/utils/keychain.d.ts +0 -15
  94. package/dist/utils/oauth2-auth.d.ts +0 -64
  95. package/dist/utils/password-prompt.d.ts +0 -14
  96. package/dist/utils/process-runner.d.ts +0 -32
  97. package/dist/utils/project-detection.d.ts +0 -210
  98. package/dist/utils/session-cookie-auth.d.ts +0 -35
  99. package/dist/utils/signing-password.d.ts +0 -12
  100. package/dist/utils/signing-step.d.ts +0 -15
  101. package/dist/utils/signing-step.js +0 -16
  102. package/dist/utils/sitevision-api.d.ts +0 -155
  103. package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
  104. package/dist/utils/tasks.d.ts +0 -61
  105. package/dist/utils/version-check.d.ts +0 -9
  106. package/dist/utils/webpack-runner.d.ts +0 -76
  107. package/dist/utils/workspace.d.ts +0 -17
  108. package/dist/utils/zip.d.ts +0 -71
@@ -1,36 +1,73 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
- import { getSettings, setSettings, settingsFile } from '../utils/config.js';
4
+ import { configProblem, getGlobalSigning, getSettings, setGlobalSigning, setSettings, settingsFile, } from '../utils/config.js';
5
5
  import { LANGUAGES, LANGUAGE_NAMES, setLanguage, t, } from '../utils/i18n.js';
6
6
  import { ACCENT } from './Frame.js';
7
7
  const ROWS = [
8
8
  {
9
9
  key: 'language',
10
10
  label: 'Language',
11
+ help: 'The language svc itself speaks.',
11
12
  options: LANGUAGES,
12
13
  display: value => LANGUAGE_NAMES[value],
13
14
  },
14
15
  {
15
16
  key: 'introAnimation',
16
17
  label: 'Intro animation',
18
+ help: 'Play the logo animation when the shell starts.',
17
19
  options: ['on', 'off'],
18
20
  display: value => t(value),
19
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
+ },
20
50
  ];
21
51
  /** Global preferences, saved per field like the Config tab. */
22
52
  export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
23
53
  const [values, setValues] = useState(() => {
24
54
  const s = getSettings();
55
+ const signing = getGlobalSigning();
25
56
  return {
26
57
  language: s.language,
27
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 ?? '',
28
63
  };
29
64
  });
30
65
  const [cursor, setCursor] = useState(0);
31
66
  const [editing, setEditing] = useState(false);
32
67
  const [draft, setDraft] = useState('');
33
- const [note, setNote] = useState('');
68
+ const [note, setNote] = useState(() => configProblem()
69
+ ? t('The settings file does not parse and is ignored until it is fixed.')
70
+ : '');
34
71
  const rowCount = ROWS.length + (onOpenWorkspace ? 1 : 0);
35
72
  const onWorkspaceRow = cursor === ROWS.length;
36
73
  const current = ROWS[Math.min(cursor, ROWS.length - 1)];
@@ -42,8 +79,16 @@ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
42
79
  setSettings({
43
80
  language: next.language,
44
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,
45
88
  });
46
- setNote(t('Saved.'));
89
+ setNote(configProblem()
90
+ ? t('Not saved: the settings file does not parse.')
91
+ : t('Saved.'));
47
92
  onChanged();
48
93
  };
49
94
  useInput((input, key) => {
@@ -56,6 +101,12 @@ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
56
101
  if (draft !== values[current.key])
57
102
  commit(draft);
58
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
+ }
59
110
  else {
60
111
  const step = key.leftArrow || key.upArrow
61
112
  ? -1
@@ -89,8 +140,10 @@ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
89
140
  const focused = i === cursor && !onWorkspaceRow;
90
141
  const typing = focused && editing;
91
142
  const chosen = typing ? draft : values[row.key];
92
- return (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(row.label).padEnd(18) }), _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));
93
- }), onOpenWorkspace && (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: onWorkspaceRow ? ACCENT : undefined, bold: onWorkspaceRow, dimColor: !onWorkspaceRow, children: (onWorkspaceRow ? '▸ ' : ' ') + t('Workspace config').padEnd(18) }), _jsx(Text, { dimColor: true, children: t('shared auth and site config for every app') })] })), _jsx(Text, { color: "yellow", children: note }), _jsx(Text, { dimColor: true, children: editing
94
- ? t('←→ choose · Enter confirm · Esc cancel')
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')
95
148
  : t('↑↓ setting · Enter edit · Esc close') })] }));
96
149
  }
@@ -8,7 +8,7 @@ import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, re
8
8
  import { appGroup, configIncomplete, discoverApps, needsOnboarding, } from '../utils/workspace.js';
9
9
  import { createAppInTerminal, isEmptyOrMissing, seedNewApp, startCreateApp, appNameAdvice, appNameProblem, } from '../utils/create-app.js';
10
10
  import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
11
- import { useTasks, runningTasks, startActivate, getTasks, } from '../utils/tasks.js';
11
+ import { useTasks, runningTasks, startActivate, getTasks, scaffoldRunning, } from '../utils/tasks.js';
12
12
  import { PasswordInput } from '../components/PasswordInput.js';
13
13
  import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
14
14
  import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
@@ -22,6 +22,7 @@ import { Popover } from './Popover.js';
22
22
  import { ChangelogPanel } from './Changelog.js';
23
23
  import { baseEnvironment, environmentColor, environmentNames, environmentProject, isProductionEnvironment, resolveEnvironment, } from '../utils/environments.js';
24
24
  import { t } from '../utils/i18n.js';
25
+ import { onKeychainSaveFailed } from '../utils/keychain.js';
25
26
  import { actionForKey, actions, authState, resolveDeployConfig, } from './actions.js';
26
27
  function useSize() {
27
28
  const { stdout } = useStdout();
@@ -37,7 +38,7 @@ function useSize() {
37
38
  }, [stdout]);
38
39
  return size;
39
40
  }
40
- export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, handover, }) {
41
+ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, handover, skipped, }) {
41
42
  const { exit } = useApp();
42
43
  const { columns, rows } = useSize();
43
44
  const tasks = useTasks();
@@ -47,14 +48,31 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
47
48
  const [selected, setSelected] = useState(onboard ? initialApps.length : 0);
48
49
  const [tab, setTab] = useState('overview');
49
50
  const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
50
- const [overlay, setOverlay] = useState(updatedFrom ? { kind: 'changelog', since: updatedFrom } : null);
51
+ const [overlays, setOverlays] = useState(updatedFrom ? [{ kind: 'changelog', since: updatedFrom }] : []);
52
+ const setOverlay = useCallback((next) => {
53
+ setOverlays(stack => overlayStack(stack, next));
54
+ }, []);
55
+ const [editing, setEditing] = useState(false);
56
+ // A confirm that turns up mid-edit stays hidden until the field is done.
57
+ const top = overlays.at(-1) ?? null;
58
+ const overlay = top?.kind === 'confirm' && editing ? null : top;
51
59
  const [filter, setFilter] = useState('');
52
60
  const [versions, setVersions] = useState({});
53
61
  const [versionRow, setVersionRow] = useState(0);
54
62
  const [logScroll, setLogScroll] = useState(0);
55
63
  const [logWrap, setLogWrap] = useState(false);
56
64
  const [offerAddon, setOfferAddon] = useState();
57
- const [notice, setNotice] = useState(null);
65
+ const [notice, setNotice] = useState(skipped?.length
66
+ ? {
67
+ text: skipped.length === 1
68
+ ? t('Skipped: {reason}', { reason: skipped[0] })
69
+ : t('Skipped {n} apps. First: {reason}', {
70
+ n: skipped.length,
71
+ reason: skipped[0],
72
+ }),
73
+ level: 'warn',
74
+ }
75
+ : null);
58
76
  const [, tick] = useReducer((n) => n + 1, 0);
59
77
  // In workspace mode the row after the last app is "Workspace settings".
60
78
  const settings = Boolean(workspaceRoot) && selected === apps.length;
@@ -117,12 +135,35 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
117
135
  const notify = useCallback((text, level = 'info') => {
118
136
  setNotice({ text, level });
119
137
  }, []);
138
+ // "Save to keychain" that did not save would otherwise just ask again.
139
+ useEffect(() => {
140
+ onKeychainSaveFailed(() => {
141
+ notify(t('Could not save to the OS keychain; you will be asked again next time.'), 'warn');
142
+ });
143
+ }, [notify]);
120
144
  const quit = useCallback(() => {
121
- for (const task of getTasks())
122
- if (task.status === 'running')
123
- task.stop();
124
- exit();
125
- }, [exit]);
145
+ const leave = () => {
146
+ for (const task of getTasks())
147
+ if (task.status === 'running')
148
+ task.stop();
149
+ exit();
150
+ };
151
+ // Between the scaffolder's questions nothing is open, so q is live:
152
+ // quitting then would leave a half-made app behind without a word.
153
+ if (scaffoldRunning()) {
154
+ setOverlay({
155
+ kind: 'confirm',
156
+ message: t('A new app is still being created. Stop it and quit?'),
157
+ resolve(yes) {
158
+ if (yes)
159
+ leave();
160
+ },
161
+ });
162
+ }
163
+ else {
164
+ leave();
165
+ }
166
+ }, [exit, setOverlay]);
126
167
  const context = useMemo(() => ({
127
168
  project,
128
169
  reload,
@@ -173,8 +214,18 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
173
214
  notify(t('environment {env} added', { env: clean }), 'ok');
174
215
  },
175
216
  async createApp() {
217
+ // Only what this flow opened is closed when the scaffolder ends.
218
+ const asked = new Set();
219
+ const openOwn = (next) => {
220
+ asked.add(next);
221
+ setOverlay(next);
222
+ };
223
+ if (scaffoldRunning()) {
224
+ notify(t('An app is already being created.'), 'warn');
225
+ return;
226
+ }
176
227
  const promptText = async (label, initial, validate, error, advise) => new Promise(resolve => {
177
- setOverlay({
228
+ openOwn({
178
229
  kind: 'prompt',
179
230
  label,
180
231
  initial,
@@ -210,7 +261,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
210
261
  ].toSorted((a, b) => a.localeCompare(b));
211
262
  // eslint-disable-next-line no-await-in-loop
212
263
  const folderPick = await new Promise(resolve => {
213
- setOverlay({
264
+ openOwn({
214
265
  kind: 'choice',
215
266
  label: t('Create it in folder'),
216
267
  choices: [...folders, t('Other folder…')],
@@ -258,20 +309,20 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
258
309
  : question.message;
259
310
  if (question.type === 'confirm') {
260
311
  const value = await new Promise(resolve => {
261
- setOverlay({ kind: 'confirm', message: label, resolve });
312
+ openOwn({ kind: 'confirm', message: label, resolve });
262
313
  });
263
314
  return { value };
264
315
  }
265
316
  if (question.type === 'password') {
266
317
  const secret = await new Promise(resolve => {
267
- setOverlay({ kind: 'password', label, resolve });
318
+ openOwn({ kind: 'password', label, resolve });
268
319
  });
269
320
  return secret && { value: secret.password };
270
321
  }
271
322
  if (question.choices.length > 0) {
272
323
  const multi = question.type === 'checkbox';
273
324
  const picked = await new Promise(resolve => {
274
- setOverlay({
325
+ openOwn({
275
326
  kind: 'choice',
276
327
  label,
277
328
  choices: question.choices,
@@ -290,7 +341,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
290
341
  setTab('log');
291
342
  const { done } = startCreateApp({ name, parentDir, ask });
292
343
  let outcome = await done;
293
- setOverlay(null);
344
+ setOverlays(stack => withoutOverlays(stack, asked));
294
345
  if (outcome === 'unmanaged') {
295
346
  if (!handover) {
296
347
  notify(t('The scaffolder could not be run from here'), 'error');
@@ -323,7 +374,10 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
323
374
  notify(t('{app} created in {dir}', { app: name, dir: appDir }), 'ok');
324
375
  return;
325
376
  }
326
- const found = discoverApps(workspaceRoot);
377
+ const left = [];
378
+ const found = discoverApps(workspaceRoot, left);
379
+ if (left[0])
380
+ notify(t('Skipped: {reason}', { reason: left[0] }), 'warn');
327
381
  setApps(found);
328
382
  setFilter('');
329
383
  setSelected(Math.max(0, found.findIndex(app => app.root === appDir)));
@@ -366,7 +420,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
366
420
  action.run(context).catch((error) => {
367
421
  notify(error instanceof Error ? error.message : String(error), 'error');
368
422
  });
369
- }, [context, project, notify]);
423
+ }, [context, project, notify, setOverlay]);
370
424
  const fetchVersions = useCallback(async (fresh = false) => {
371
425
  const key = versionsKey;
372
426
  const config = await resolveDeployConfig(context, fresh);
@@ -407,13 +461,20 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
407
461
  await wait();
408
462
  notify(task.error ?? t('{v} activated', { v: executable.appVersion }), task.error ? 'error' : 'ok');
409
463
  await fetchVersions();
410
- }, [versions, project, versionRow, context, notify, fetchVersions]);
464
+ }, [
465
+ versions,
466
+ versionsKey,
467
+ project,
468
+ versionRow,
469
+ context,
470
+ notify,
471
+ fetchVersions,
472
+ ]);
411
473
  // A popover owns the keyboard; the form stays mounted underneath it.
412
474
  const formActive = (tab === 'config' || settings) && focus === 'content' && !overlay;
413
- const [editing, setEditing] = useState(false);
414
475
  const pickAddon = useCallback(async () => new Promise(resolve => {
415
476
  setOverlay({ kind: 'picker', resolve });
416
- }), []);
477
+ }), [setOverlay]);
417
478
  // A freshly created app: once it is the selected project, offer its addon.
418
479
  useEffect(() => {
419
480
  if (!offerAddon || project.root !== offerAddon || overlay)
@@ -434,7 +495,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
434
495
  });
435
496
  }, [offerAddon, project, overlay, context, run]);
436
497
  const loadAddons = useCallback(async () => {
437
- const config = await resolveDeployConfig(context);
498
+ const config = await resolveDeployConfig(context, false, { addon: false });
438
499
  return config ? listAddons(config) : { error: t('No credentials.') };
439
500
  }, [context]);
440
501
  const appTasks = tasks.filter(task => task.appRoot === project.root);
@@ -464,6 +525,9 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
464
525
  setFocus(single ? 'content' : 'nav');
465
526
  return;
466
527
  }
528
+ // Ctrl+D is not d: only plain keys are shortcuts.
529
+ if (key.ctrl || key.meta)
530
+ return;
467
531
  if (input === '/') {
468
532
  setOverlay({ kind: 'palette' });
469
533
  return;
@@ -472,6 +536,11 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
472
536
  setOverlay({ kind: 'help' });
473
537
  return;
474
538
  }
539
+ // Reachable from the navigator's search and the workspace form too.
540
+ if (input === ',') {
541
+ setOverlay({ kind: 'settings' });
542
+ return;
543
+ }
475
544
  // The config form uses Tab/Shift+Tab to move between fields.
476
545
  if (key.tab && !formActive) {
477
546
  setFilter('');
@@ -508,6 +577,23 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
508
577
  search(filter + input);
509
578
  return;
510
579
  }
580
+ if (settings && focus === 'content') {
581
+ // Settings pane: the form owns everything but q, y and Tab/Esc above.
582
+ if (input === 'q')
583
+ quit();
584
+ else if (input === 'y') {
585
+ try {
586
+ if (syncDevPropertiesToPackageJson(workspaceRoot)) {
587
+ reload();
588
+ notify(t('package.json updated'), 'ok');
589
+ }
590
+ }
591
+ catch (error) {
592
+ notify(error instanceof Error ? error.message : String(error), 'error');
593
+ }
594
+ }
595
+ return;
596
+ }
511
597
  if (input === 'a' && tab !== 'versions') {
512
598
  setTab('versions');
513
599
  setFocus('content');
@@ -526,23 +612,6 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
526
612
  setTab(TABS[(i + (key.rightArrow ? 1 : TABS.length - 1)) % TABS.length].id);
527
613
  return;
528
614
  }
529
- if (settings && focus === 'content') {
530
- // Settings pane: the form owns everything but q, y and Tab/Esc above.
531
- if (input === 'q')
532
- quit();
533
- else if (input === 'y') {
534
- try {
535
- if (syncDevPropertiesToPackageJson(workspaceRoot)) {
536
- reload();
537
- notify(t('package.json updated'), 'ok');
538
- }
539
- }
540
- catch (error) {
541
- notify(error instanceof Error ? error.message : String(error), 'error');
542
- }
543
- }
544
- return;
545
- }
546
615
  if (tab === 'versions') {
547
616
  const count = versions[versionsKey]?.executables?.length ?? 0;
548
617
  if (key.upArrow)
@@ -795,11 +864,25 @@ function renderOverlay(overlay, env) {
795
864
  } }));
796
865
  }
797
866
  }
798
- function Confirm({ message, onAnswer, }) {
799
- useInput(input => {
867
+ /**
868
+ * The open overlays, last one shown. `null` closes the shown one. A new overlay
869
+ * covers the current one instead of replacing it, so whoever awaits the covered
870
+ * one still gets an answer. A confirm goes underneath: background tasks raise
871
+ * them, and on top it would take the keys being typed into a prompt.
872
+ */
873
+ export function overlayStack(stack, next) {
874
+ if (next === null)
875
+ return stack.slice(0, -1);
876
+ return next.kind === 'confirm' ? [next, ...stack] : [...stack, next];
877
+ }
878
+ export function withoutOverlays(stack, gone) {
879
+ return stack.filter(overlay => !gone.has(overlay));
880
+ }
881
+ export function Confirm({ message, onAnswer, }) {
882
+ useInput((input, key) => {
800
883
  if (input === 'y' || input === 'Y')
801
884
  onAnswer(true);
802
- else if (input === 'n' || input === 'N')
885
+ else if (input === 'n' || input === 'N' || key.escape)
803
886
  onAnswer(false);
804
887
  });
805
888
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: message }), _jsx(Text, { dimColor: true, children: t('y confirm · n cancel') })] }));
@@ -5,6 +5,7 @@ import { appTypeOf, getPackageJsonSyncChanges, readAncestorDevProperties, locali
5
5
  import { checkSitevisionScriptsCompatibility } from '../utils/sitevision-scripts-runner.js';
6
6
  import { ACCENT, elapsed } from './Frame.js';
7
7
  import { t } from '../utils/i18n.js';
8
+ import { getGlobalSigning } from '../utils/config.js';
8
9
  export const TABS = [
9
10
  { id: 'overview', label: 'Overview', short: 'Ovw' },
10
11
  { id: 'config', label: 'Config', short: 'Cfg' },
@@ -40,11 +41,16 @@ export function Overview({ project, tasks, height, }) {
40
41
  const dev = project.devProperties;
41
42
  const inherited = new Set(project.inheritedKeys);
42
43
  const ancestors = readAncestorDevProperties(project.root);
44
+ const globalSigning = getGlobalSigning();
43
45
  const src = (key) => inherited.has(key)
44
46
  ? Object.hasOwn(ancestors, key)
45
47
  ? t('↑ root')
46
48
  : 'package.json'
47
- : undefined;
49
+ : globalSigning[key] !== undefined &&
50
+ globalSigning[key] ===
51
+ dev?.[key]
52
+ ? t('global')
53
+ : undefined;
48
54
  const notSet = t('not set');
49
55
  const sync = getPackageJsonSyncChanges(project.root).length;
50
56
  const scripts = checkSitevisionScriptsCompatibility(project.root);
@@ -64,7 +70,7 @@ export function Versions({ project, state, selected, }) {
64
70
  return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "yellow", children: t('⚠ Configure dev properties first (e).') }) }));
65
71
  }
66
72
  if (!state) {
67
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: t('Press R to fetch versions from {domain}.', {
73
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: t('Press r to fetch versions from {domain}.', {
68
74
  domain: project.devProperties.domain,
69
75
  }) }) }));
70
76
  }
Binary file
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
+ import { parseJsonc } from './jsonc.js';
4
5
  function configDir() {
5
6
  const base = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
6
7
  return path.join(base, 'sitevision-cli');
@@ -10,13 +11,33 @@ function configFile() {
10
11
  }
11
12
  function readConfig() {
12
13
  try {
13
- return JSON.parse(fs.readFileSync(configFile(), 'utf8'));
14
+ return parseJsonc(fs.readFileSync(configFile(), 'utf8'));
14
15
  }
15
16
  catch {
16
17
  return {};
17
18
  }
18
19
  }
20
+ /** Set when the file exists but does not parse; it is then left untouched. */
21
+ export function configProblem() {
22
+ let text;
23
+ try {
24
+ text = fs.readFileSync(configFile(), 'utf8');
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ try {
30
+ parseJsonc(text);
31
+ return undefined;
32
+ }
33
+ catch (error) {
34
+ return error instanceof Error ? error.message : String(error);
35
+ }
36
+ }
19
37
  function writeConfig(config) {
38
+ // A hand-edited file with a typo must not be replaced by defaults.
39
+ if (configProblem())
40
+ return;
20
41
  try {
21
42
  fs.mkdirSync(configDir(), { recursive: true });
22
43
  fs.writeFileSync(configFile(), JSON.stringify(config, null, 2));
@@ -31,10 +52,30 @@ export function getSettings() {
31
52
  return {
32
53
  language: config.language ?? 'en',
33
54
  introAnimation: config.introAnimation ?? true,
55
+ updateCheck: config.updateCheck ?? true,
56
+ addonNameDriftWarning: config.warnings?.addonNameDrift ?? true,
34
57
  };
35
58
  }
36
59
  export function setSettings(patch) {
37
- writeConfig({ ...readConfig(), ...patch });
60
+ const { addonNameDriftWarning, ...rest } = patch;
61
+ const config = { ...readConfig(), ...rest };
62
+ if (addonNameDriftWarning !== undefined) {
63
+ config.warnings = {
64
+ ...config.warnings,
65
+ addonNameDrift: addonNameDriftWarning,
66
+ };
67
+ }
68
+ writeConfig(config);
69
+ }
70
+ export function getGlobalSigning() {
71
+ const { signingUsername, certificateName } = readConfig();
72
+ return {
73
+ ...(signingUsername && { signingUsername }),
74
+ ...(certificateName && { certificateName }),
75
+ };
76
+ }
77
+ export function setGlobalSigning(signing) {
78
+ writeConfig({ ...readConfig(), ...signing });
38
79
  }
39
80
  /** Path of the config file, for display. */
40
81
  export function settingsFile() {
@@ -44,7 +85,7 @@ export function settingsFile() {
44
85
  * True until the user has completed the first-run welcome at least once.
45
86
  */
46
87
  export function isFirstRun() {
47
- return !readConfig().firstRunCompleted;
88
+ return !configProblem() && !readConfig().firstRunCompleted;
48
89
  }
49
90
  /**
50
91
  * Persist that the first-run welcome has been seen.
@@ -66,6 +107,8 @@ export function getLastSeenVersion() {
66
107
  */
67
108
  export function setLastSeenVersion(version) {
68
109
  const config = readConfig();
110
+ if (config.lastSeenVersion === version)
111
+ return;
69
112
  config.lastSeenVersion = version;
70
113
  writeConfig(config);
71
114
  }
@@ -154,7 +154,8 @@ export function startCreateApp(options) {
154
154
  /** Run the scaffolder on the real terminal; resolves when it exits. */
155
155
  export async function createAppInTerminal(name, parentDir) {
156
156
  await new Promise(resolve => {
157
- const child = spawn(CREATE_COMMAND[0], [...CREATE_COMMAND.slice(1), name], {
157
+ // `name` is validated by the caller; joined to avoid DEP0190.
158
+ const child = spawn([...CREATE_COMMAND, name].join(' '), {
158
159
  cwd: parentDir,
159
160
  stdio: 'inherit',
160
161
  shell: true,
@@ -162,7 +163,13 @@ export async function createAppInTerminal(name, parentDir) {
162
163
  child.on('close', () => resolve());
163
164
  child.on('error', () => resolve());
164
165
  });
165
- return Boolean(readManifest(path.join(parentDir, name)));
166
+ try {
167
+ return Boolean(readManifest(path.join(parentDir, name)));
168
+ }
169
+ catch {
170
+ // A half-written manifest is a failed scaffold, not a crash.
171
+ return false;
172
+ }
166
173
  }
167
174
  /**
168
175
  * Replace the template's placeholders with what is known, and fold the config