sitevision-cli 1.0.0-beta.31 → 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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/dist/cli.js +73 -34
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +85 -238
- package/dist/commands/dev.js +34 -37
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +43 -28
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +0 -8
- package/dist/components/AuthLoginScreen.js +27 -12
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/ConfigForm.js +174 -29
- package/dist/shell/Frame.js +4 -1
- package/dist/shell/Help.js +2 -1
- package/dist/shell/Settings.js +59 -6
- package/dist/shell/Shell.js +361 -43
- package/dist/shell/Tabs.js +8 -2
- package/dist/shell/actions.js +0 -0
- package/dist/utils/config.js +46 -3
- package/dist/utils/create-app.js +226 -0
- package/dist/utils/i18n.js +68 -13
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +44 -4
- package/dist/utils/oauth2-auth.js +35 -7
- package/dist/utils/password-prompt.js +23 -3
- package/dist/utils/process-runner.js +8 -3
- package/dist/utils/project-detection.js +172 -44
- package/dist/utils/session-cookie-auth.js +5 -4
- package/dist/utils/sitevision-api.js +21 -14
- package/dist/utils/sitevision-scripts-runner.js +32 -15
- package/dist/utils/tasks.js +132 -66
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +6 -5
- package/dist/utils/workspace.js +41 -3
- package/dist/utils/zip.js +18 -19
- package/package.json +24 -3
- package/readme.md +11 -4
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -321
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -17
- package/dist/commands/dev.d.ts +0 -14
- package/dist/commands/index.d.ts +0 -4
- package/dist/commands/info.d.ts +0 -2
- package/dist/commands/setup-signing.d.ts +0 -2
- package/dist/commands/sign.d.ts +0 -14
- package/dist/commands/types.d.ts +0 -18
- package/dist/commands/watch.d.ts +0 -2
- package/dist/components/AnimatedLogo.d.ts +0 -10
- package/dist/components/AuthLoginScreen.d.ts +0 -21
- package/dist/components/DevPropertiesForm.d.ts +0 -11
- package/dist/components/DevPropertiesForm.js +0 -246
- package/dist/components/InfoScreen.d.ts +0 -7
- package/dist/components/InfoScreen.js +0 -14
- package/dist/components/KeychainPasswordChoice.d.ts +0 -8
- package/dist/components/KeychainPasswordChoice.js +0 -30
- package/dist/components/MainMenu.d.ts +0 -7
- package/dist/components/MainMenu.js +0 -88
- package/dist/components/PasswordInput.d.ts +0 -10
- package/dist/components/ProcessOutput.d.ts +0 -6
- package/dist/components/ProcessOutput.js +0 -30
- package/dist/components/SetupFlow.d.ts +0 -8
- package/dist/components/SetupFlow.js +0 -233
- package/dist/components/SigningPropertiesForm.d.ts +0 -7
- package/dist/components/SigningPropertiesForm.js +0 -38
- package/dist/components/StatusIndicator.d.ts +0 -8
- package/dist/components/TextInput.d.ts +0 -10
- package/dist/components/TextInput.js +0 -29
- package/dist/components/WelcomeScreen.d.ts +0 -14
- package/dist/shell/AddonPicker.d.ts +0 -14
- package/dist/shell/Changelog.d.ts +0 -10
- package/dist/shell/CommandPalette.d.ts +0 -12
- package/dist/shell/ConfigForm.d.ts +0 -36
- package/dist/shell/Frame.d.ts +0 -59
- package/dist/shell/Help.d.ts +0 -9
- package/dist/shell/Popover.d.ts +0 -12
- package/dist/shell/Settings.d.ts +0 -6
- package/dist/shell/Shell.d.ts +0 -10
- package/dist/shell/Tabs.d.ts +0 -36
- package/dist/shell/actions.d.ts +0 -48
- package/dist/types/index.d.ts +0 -295
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -27
- package/dist/utils/environments.d.ts +0 -20
- package/dist/utils/i18n.d.ts +0 -12
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -64
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -31
- package/dist/utils/project-detection.d.ts +0 -202
- package/dist/utils/session-cookie-auth.d.ts +0 -35
- package/dist/utils/signing-password.d.ts +0 -12
- package/dist/utils/signing-step.d.ts +0 -15
- package/dist/utils/signing-step.js +0 -16
- package/dist/utils/sitevision-api.d.ts +0 -155
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
- package/dist/utils/tasks.d.ts +0 -50
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/workspace.d.ts +0 -17
- package/dist/utils/zip.d.ts +0 -71
package/dist/shell/Shell.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
|
4
5
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
6
|
import Spinner from 'ink-spinner';
|
|
6
7
|
import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, syncDevPropertiesToPackageJson, } from '../utils/project-detection.js';
|
|
7
|
-
import { appGroup, configIncomplete, needsOnboarding, } from '../utils/workspace.js';
|
|
8
|
+
import { appGroup, configIncomplete, discoverApps, needsOnboarding, } from '../utils/workspace.js';
|
|
9
|
+
import { createAppInTerminal, isEmptyOrMissing, seedNewApp, startCreateApp, appNameAdvice, appNameProblem, } from '../utils/create-app.js';
|
|
8
10
|
import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
|
|
9
|
-
import { useTasks, runningTasks, startActivate, getTasks, } from '../utils/tasks.js';
|
|
11
|
+
import { useTasks, runningTasks, startActivate, getTasks, scaffoldRunning, } from '../utils/tasks.js';
|
|
10
12
|
import { PasswordInput } from '../components/PasswordInput.js';
|
|
11
13
|
import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
|
|
12
14
|
import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
|
|
@@ -20,7 +22,8 @@ import { Popover } from './Popover.js';
|
|
|
20
22
|
import { ChangelogPanel } from './Changelog.js';
|
|
21
23
|
import { baseEnvironment, environmentColor, environmentNames, environmentProject, isProductionEnvironment, resolveEnvironment, } from '../utils/environments.js';
|
|
22
24
|
import { t } from '../utils/i18n.js';
|
|
23
|
-
import {
|
|
25
|
+
import { onKeychainSaveFailed } from '../utils/keychain.js';
|
|
26
|
+
import { actionForKey, actions, authState, resolveDeployConfig, } from './actions.js';
|
|
24
27
|
function useSize() {
|
|
25
28
|
const { stdout } = useStdout();
|
|
26
29
|
const read = () => ({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
@@ -35,7 +38,7 @@ function useSize() {
|
|
|
35
38
|
}, [stdout]);
|
|
36
39
|
return size;
|
|
37
40
|
}
|
|
38
|
-
export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, }) {
|
|
41
|
+
export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, handover, skipped, }) {
|
|
39
42
|
const { exit } = useApp();
|
|
40
43
|
const { columns, rows } = useSize();
|
|
41
44
|
const tasks = useTasks();
|
|
@@ -45,13 +48,31 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
45
48
|
const [selected, setSelected] = useState(onboard ? initialApps.length : 0);
|
|
46
49
|
const [tab, setTab] = useState('overview');
|
|
47
50
|
const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
|
|
48
|
-
const [
|
|
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;
|
|
49
59
|
const [filter, setFilter] = useState('');
|
|
50
60
|
const [versions, setVersions] = useState({});
|
|
51
61
|
const [versionRow, setVersionRow] = useState(0);
|
|
52
62
|
const [logScroll, setLogScroll] = useState(0);
|
|
53
63
|
const [logWrap, setLogWrap] = useState(false);
|
|
54
|
-
const [
|
|
64
|
+
const [offerAddon, setOfferAddon] = useState();
|
|
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);
|
|
55
76
|
const [, tick] = useReducer((n) => n + 1, 0);
|
|
56
77
|
// In workspace mode the row after the last app is "Workspace settings".
|
|
57
78
|
const settings = Boolean(workspaceRoot) && selected === apps.length;
|
|
@@ -114,12 +135,35 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
114
135
|
const notify = useCallback((text, level = 'info') => {
|
|
115
136
|
setNotice({ text, level });
|
|
116
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]);
|
|
117
144
|
const quit = useCallback(() => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
task.
|
|
121
|
-
|
|
122
|
-
|
|
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]);
|
|
123
167
|
const context = useMemo(() => ({
|
|
124
168
|
project,
|
|
125
169
|
reload,
|
|
@@ -169,6 +213,178 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
169
213
|
writeSvcConfig(configRoot, { environment: clean });
|
|
170
214
|
notify(t('environment {env} added', { env: clean }), 'ok');
|
|
171
215
|
},
|
|
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
|
+
}
|
|
227
|
+
const promptText = async (label, initial, validate, error, advise) => new Promise(resolve => {
|
|
228
|
+
openOwn({
|
|
229
|
+
kind: 'prompt',
|
|
230
|
+
label,
|
|
231
|
+
initial,
|
|
232
|
+
validate,
|
|
233
|
+
advise,
|
|
234
|
+
error,
|
|
235
|
+
resolve,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
const base = workspaceRoot ?? path.dirname(rawProject.root);
|
|
239
|
+
const suggested = workspaceRoot
|
|
240
|
+
? path.relative(workspaceRoot, path.dirname(rawProject.root)) || '.'
|
|
241
|
+
: base;
|
|
242
|
+
// Asked again, with the reason, until the name is free in that folder.
|
|
243
|
+
let name = '';
|
|
244
|
+
let parentDir = '';
|
|
245
|
+
let appDir = '';
|
|
246
|
+
let taken;
|
|
247
|
+
for (;;) {
|
|
248
|
+
// eslint-disable-next-line no-await-in-loop
|
|
249
|
+
const typed = await promptText(t('Name of the new app'), name, appNameProblem, taken, appNameAdvice);
|
|
250
|
+
if (typed === null)
|
|
251
|
+
return;
|
|
252
|
+
name = typed.trim();
|
|
253
|
+
// Folders that already hold apps, then a way to type another one.
|
|
254
|
+
const folders = [
|
|
255
|
+
...new Set([
|
|
256
|
+
suggested,
|
|
257
|
+
...apps.map(app => workspaceRoot
|
|
258
|
+
? path.relative(base, path.dirname(app.root)) || '.'
|
|
259
|
+
: path.dirname(app.root)),
|
|
260
|
+
]),
|
|
261
|
+
].toSorted((a, b) => a.localeCompare(b));
|
|
262
|
+
// eslint-disable-next-line no-await-in-loop
|
|
263
|
+
const folderPick = await new Promise(resolve => {
|
|
264
|
+
openOwn({
|
|
265
|
+
kind: 'choice',
|
|
266
|
+
label: t('Create it in folder'),
|
|
267
|
+
choices: [...folders, t('Other folder…')],
|
|
268
|
+
multi: false,
|
|
269
|
+
initial: [folders.indexOf(suggested)],
|
|
270
|
+
resolve,
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
if (!folderPick)
|
|
274
|
+
return;
|
|
275
|
+
const folder = folders[folderPick[0]] ??
|
|
276
|
+
// eslint-disable-next-line no-await-in-loop
|
|
277
|
+
(await promptText(t('Create it in folder'), suggested));
|
|
278
|
+
if (folder === null)
|
|
279
|
+
return;
|
|
280
|
+
parentDir = path.resolve(base, folder.trim() || '.');
|
|
281
|
+
appDir = path.join(parentDir, name);
|
|
282
|
+
if (isEmptyOrMissing(appDir))
|
|
283
|
+
break;
|
|
284
|
+
taken = t('{dir} already exists. Pick another name, or keep it and choose a different folder next.', { dir: path.relative(base, appDir) });
|
|
285
|
+
}
|
|
286
|
+
// Both were checked above, so a run that leaves no app may remove them.
|
|
287
|
+
const madeParent = !fs.existsSync(parentDir);
|
|
288
|
+
const discard = () => {
|
|
289
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
290
|
+
if (madeParent && isEmptyOrMissing(parentDir))
|
|
291
|
+
fs.rmSync(parentDir, { recursive: true, force: true });
|
|
292
|
+
};
|
|
293
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
294
|
+
const known = (workspaceRoot
|
|
295
|
+
? readWorkspaceDevProperties(workspaceRoot)
|
|
296
|
+
: rawProject.devProperties);
|
|
297
|
+
const ask = async (question) => {
|
|
298
|
+
// svc keeps passwords in the keychain, never in the app's file.
|
|
299
|
+
if (question.name === 'password')
|
|
300
|
+
return { skip: true };
|
|
301
|
+
const shared = known?.[question.name];
|
|
302
|
+
if (!question.error &&
|
|
303
|
+
['domain', 'siteName', 'username', 'useHTTPForDevDeploy'].includes(question.name) &&
|
|
304
|
+
shared !== undefined &&
|
|
305
|
+
shared !== '')
|
|
306
|
+
return { value: shared };
|
|
307
|
+
const label = question.error
|
|
308
|
+
? `${question.message} (${question.error})`
|
|
309
|
+
: question.message;
|
|
310
|
+
if (question.type === 'confirm') {
|
|
311
|
+
const value = await new Promise(resolve => {
|
|
312
|
+
openOwn({ kind: 'confirm', message: label, resolve });
|
|
313
|
+
});
|
|
314
|
+
return { value };
|
|
315
|
+
}
|
|
316
|
+
if (question.type === 'password') {
|
|
317
|
+
const secret = await new Promise(resolve => {
|
|
318
|
+
openOwn({ kind: 'password', label, resolve });
|
|
319
|
+
});
|
|
320
|
+
return secret && { value: secret.password };
|
|
321
|
+
}
|
|
322
|
+
if (question.choices.length > 0) {
|
|
323
|
+
const multi = question.type === 'checkbox';
|
|
324
|
+
const picked = await new Promise(resolve => {
|
|
325
|
+
openOwn({
|
|
326
|
+
kind: 'choice',
|
|
327
|
+
label,
|
|
328
|
+
choices: question.choices,
|
|
329
|
+
multi,
|
|
330
|
+
initial: [question.default ?? []].flat(),
|
|
331
|
+
resolve,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
return picked && { value: multi ? picked : picked[0] };
|
|
335
|
+
}
|
|
336
|
+
const initial = question.default ?? (question.name === 'addonName' ? name : '');
|
|
337
|
+
const value = await promptText(label, typeof initial === 'string' ? initial : JSON.stringify(initial));
|
|
338
|
+
return value === null ? null : { value };
|
|
339
|
+
};
|
|
340
|
+
notify(t('creating {app}: installing, questions follow', { app: name }));
|
|
341
|
+
setTab('log');
|
|
342
|
+
const { done } = startCreateApp({ name, parentDir, ask });
|
|
343
|
+
let outcome = await done;
|
|
344
|
+
setOverlays(stack => withoutOverlays(stack, asked));
|
|
345
|
+
if (outcome === 'unmanaged') {
|
|
346
|
+
if (!handover) {
|
|
347
|
+
notify(t('The scaffolder could not be run from here'), 'error');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
351
|
+
handover(async () => {
|
|
352
|
+
if (await createAppInTerminal(name, parentDir))
|
|
353
|
+
seedNewApp(appDir, workspaceRoot);
|
|
354
|
+
else
|
|
355
|
+
discard();
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (outcome !== 'created') {
|
|
360
|
+
discard();
|
|
361
|
+
if (outcome === 'failed')
|
|
362
|
+
notify(t('creating {app} failed, see the log', { app: name }), 'error');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
seedNewApp(appDir, workspaceRoot);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
notify(error instanceof Error ? error.message : String(error), 'warn');
|
|
370
|
+
outcome = 'failed';
|
|
371
|
+
}
|
|
372
|
+
if (!workspaceRoot ||
|
|
373
|
+
path.relative(workspaceRoot, appDir).startsWith('..')) {
|
|
374
|
+
notify(t('{app} created in {dir}', { app: name, dir: appDir }), 'ok');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const left = [];
|
|
378
|
+
const found = discoverApps(workspaceRoot, left);
|
|
379
|
+
if (left[0])
|
|
380
|
+
notify(t('Skipped: {reason}', { reason: left[0] }), 'warn');
|
|
381
|
+
setApps(found);
|
|
382
|
+
setFilter('');
|
|
383
|
+
setSelected(Math.max(0, found.findIndex(app => app.root === appDir)));
|
|
384
|
+
if (outcome === 'created')
|
|
385
|
+
notify(t('{app} created', { app: name }), 'ok');
|
|
386
|
+
setOfferAddon(appDir);
|
|
387
|
+
},
|
|
172
388
|
openWorkspaceSettings: workspaceRoot
|
|
173
389
|
? () => {
|
|
174
390
|
setSelected(apps.length);
|
|
@@ -204,7 +420,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
204
420
|
action.run(context).catch((error) => {
|
|
205
421
|
notify(error instanceof Error ? error.message : String(error), 'error');
|
|
206
422
|
});
|
|
207
|
-
}, [context, project, notify]);
|
|
423
|
+
}, [context, project, notify, setOverlay]);
|
|
208
424
|
const fetchVersions = useCallback(async (fresh = false) => {
|
|
209
425
|
const key = versionsKey;
|
|
210
426
|
const config = await resolveDeployConfig(context, fresh);
|
|
@@ -245,19 +461,52 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
245
461
|
await wait();
|
|
246
462
|
notify(task.error ?? t('{v} activated', { v: executable.appVersion }), task.error ? 'error' : 'ok');
|
|
247
463
|
await fetchVersions();
|
|
248
|
-
}, [
|
|
464
|
+
}, [
|
|
465
|
+
versions,
|
|
466
|
+
versionsKey,
|
|
467
|
+
project,
|
|
468
|
+
versionRow,
|
|
469
|
+
context,
|
|
470
|
+
notify,
|
|
471
|
+
fetchVersions,
|
|
472
|
+
]);
|
|
249
473
|
// A popover owns the keyboard; the form stays mounted underneath it.
|
|
250
474
|
const formActive = (tab === 'config' || settings) && focus === 'content' && !overlay;
|
|
251
|
-
const [editing, setEditing] = useState(false);
|
|
252
475
|
const pickAddon = useCallback(async () => new Promise(resolve => {
|
|
253
476
|
setOverlay({ kind: 'picker', resolve });
|
|
254
|
-
}), []);
|
|
477
|
+
}), [setOverlay]);
|
|
478
|
+
// A freshly created app: once it is the selected project, offer its addon.
|
|
479
|
+
useEffect(() => {
|
|
480
|
+
if (!offerAddon || project.root !== offerAddon || overlay)
|
|
481
|
+
return;
|
|
482
|
+
setOfferAddon(undefined);
|
|
483
|
+
const addon = project.devProperties?.addonName;
|
|
484
|
+
const action = actions.find(entry => entry.id === 'create-addon');
|
|
485
|
+
if (!addon || !action || !project.devProperties?.domain)
|
|
486
|
+
return;
|
|
487
|
+
void context
|
|
488
|
+
.confirm(t('Create the addon "{addon}" on {domain} now?', {
|
|
489
|
+
addon,
|
|
490
|
+
domain: project.devProperties.domain,
|
|
491
|
+
}))
|
|
492
|
+
.then(yes => {
|
|
493
|
+
if (yes)
|
|
494
|
+
run(action);
|
|
495
|
+
});
|
|
496
|
+
}, [offerAddon, project, overlay, context, run]);
|
|
255
497
|
const loadAddons = useCallback(async () => {
|
|
256
|
-
const config = await resolveDeployConfig(context);
|
|
498
|
+
const config = await resolveDeployConfig(context, false, { addon: false });
|
|
257
499
|
return config ? listAddons(config) : { error: t('No credentials.') };
|
|
258
500
|
}, [context]);
|
|
259
501
|
const appTasks = tasks.filter(task => task.appRoot === project.root);
|
|
260
|
-
|
|
502
|
+
// A new app is not in the list while it is being created, so its log shows
|
|
503
|
+
// wherever you are; a failed one stays up until another task runs.
|
|
504
|
+
const creating = tasks.findLast(task => task.kind === 'create');
|
|
505
|
+
const logTask = appTasks.find(task => task.status === 'running') ??
|
|
506
|
+
(creating?.status === 'running' ||
|
|
507
|
+
(creating?.status === 'error' && creating === tasks.at(-1))
|
|
508
|
+
? creating
|
|
509
|
+
: appTasks.at(-1));
|
|
261
510
|
useInput((raw, key) => {
|
|
262
511
|
// Terminals speaking the kitty keyboard protocol report shift+s as
|
|
263
512
|
// "s" plus a shift flag; fold that back into the uppercase letter so
|
|
@@ -276,6 +525,9 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
276
525
|
setFocus(single ? 'content' : 'nav');
|
|
277
526
|
return;
|
|
278
527
|
}
|
|
528
|
+
// Ctrl+D is not d: only plain keys are shortcuts.
|
|
529
|
+
if (key.ctrl || key.meta)
|
|
530
|
+
return;
|
|
279
531
|
if (input === '/') {
|
|
280
532
|
setOverlay({ kind: 'palette' });
|
|
281
533
|
return;
|
|
@@ -284,6 +536,11 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
284
536
|
setOverlay({ kind: 'help' });
|
|
285
537
|
return;
|
|
286
538
|
}
|
|
539
|
+
// Reachable from the navigator's search and the workspace form too.
|
|
540
|
+
if (input === ',') {
|
|
541
|
+
setOverlay({ kind: 'settings' });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
287
544
|
// The config form uses Tab/Shift+Tab to move between fields.
|
|
288
545
|
if (key.tab && !formActive) {
|
|
289
546
|
setFilter('');
|
|
@@ -320,6 +577,23 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
320
577
|
search(filter + input);
|
|
321
578
|
return;
|
|
322
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
|
+
}
|
|
323
597
|
if (input === 'a' && tab !== 'versions') {
|
|
324
598
|
setTab('versions');
|
|
325
599
|
setFocus('content');
|
|
@@ -338,23 +612,6 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
338
612
|
setTab(TABS[(i + (key.rightArrow ? 1 : TABS.length - 1)) % TABS.length].id);
|
|
339
613
|
return;
|
|
340
614
|
}
|
|
341
|
-
if (settings && focus === 'content') {
|
|
342
|
-
// Settings pane: the form owns everything but q, y and Tab/Esc above.
|
|
343
|
-
if (input === 'q')
|
|
344
|
-
quit();
|
|
345
|
-
else if (input === 'y') {
|
|
346
|
-
try {
|
|
347
|
-
if (syncDevPropertiesToPackageJson(workspaceRoot)) {
|
|
348
|
-
reload();
|
|
349
|
-
notify(t('package.json updated'), 'ok');
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
notify(error instanceof Error ? error.message : String(error), 'error');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
615
|
if (tab === 'versions') {
|
|
359
616
|
const count = versions[versionsKey]?.executables?.length ?? 0;
|
|
360
617
|
if (key.upArrow)
|
|
@@ -532,6 +789,8 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
532
789
|
base: rawProject.devProperties,
|
|
533
790
|
environment: env,
|
|
534
791
|
workspaceRoot,
|
|
792
|
+
manifest: project.manifest,
|
|
793
|
+
manifestPath: project.paths.manifest,
|
|
535
794
|
}, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: pickAddon, onSaved: () => {
|
|
536
795
|
reload();
|
|
537
796
|
notify(t('config saved'), 'ok');
|
|
@@ -574,13 +833,21 @@ function renderOverlay(overlay, env) {
|
|
|
574
833
|
overlay.resolve(answer);
|
|
575
834
|
} }));
|
|
576
835
|
case 'prompt':
|
|
577
|
-
return (_jsx(TextPrompt, { label: overlay.label, onSubmit: value => {
|
|
836
|
+
return (_jsx(TextPrompt, { label: overlay.label, initial: overlay.initial, validate: overlay.validate, advise: overlay.advise, error: overlay.error, onSubmit: value => {
|
|
578
837
|
closeOverlay();
|
|
579
838
|
overlay.resolve(value);
|
|
580
839
|
}, onCancel: () => {
|
|
581
840
|
closeOverlay();
|
|
582
841
|
overlay.resolve(null);
|
|
583
|
-
} }));
|
|
842
|
+
} }, overlay.label));
|
|
843
|
+
case 'choice':
|
|
844
|
+
return (_jsx(ChoicePrompt, { label: overlay.label, choices: overlay.choices, multi: overlay.multi, initial: overlay.initial, onSubmit: picked => {
|
|
845
|
+
closeOverlay();
|
|
846
|
+
overlay.resolve(picked);
|
|
847
|
+
}, onCancel: () => {
|
|
848
|
+
closeOverlay();
|
|
849
|
+
overlay.resolve(null);
|
|
850
|
+
} }, overlay.label));
|
|
584
851
|
case 'help':
|
|
585
852
|
return (_jsx(HelpPanel, { here: here, where: where, height: height, onClose: closeOverlay }));
|
|
586
853
|
case 'changelog':
|
|
@@ -597,26 +864,77 @@ function renderOverlay(overlay, env) {
|
|
|
597
864
|
} }));
|
|
598
865
|
}
|
|
599
866
|
}
|
|
600
|
-
|
|
601
|
-
|
|
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) => {
|
|
602
883
|
if (input === 'y' || input === 'Y')
|
|
603
884
|
onAnswer(true);
|
|
604
|
-
else if (input === 'n' || input === 'N')
|
|
885
|
+
else if (input === 'n' || input === 'N' || key.escape)
|
|
605
886
|
onAnswer(false);
|
|
606
887
|
});
|
|
607
888
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: message }), _jsx(Text, { dimColor: true, children: t('y confirm · n cancel') })] }));
|
|
608
889
|
}
|
|
609
|
-
function
|
|
610
|
-
const [
|
|
890
|
+
function ChoicePrompt({ label, choices, multi, initial, onSubmit, onCancel, }) {
|
|
891
|
+
const [cursor, setCursor] = useState(multi ? 0 : (initial[0] ?? 0));
|
|
892
|
+
const [checked, setChecked] = useState(() => new Set(multi ? initial : []));
|
|
611
893
|
useInput((input, key) => {
|
|
612
894
|
if (key.escape)
|
|
613
895
|
onCancel();
|
|
614
896
|
else if (key.return)
|
|
615
|
-
onSubmit(
|
|
897
|
+
onSubmit(multi ? [...checked].toSorted((a, b) => a - b) : [cursor]);
|
|
898
|
+
else if (key.upArrow)
|
|
899
|
+
setCursor(c => (c - 1 + choices.length) % choices.length);
|
|
900
|
+
else if (key.downArrow)
|
|
901
|
+
setCursor(c => (c + 1) % choices.length);
|
|
902
|
+
else if (multi && input === ' ') {
|
|
903
|
+
setChecked(current => {
|
|
904
|
+
const next = new Set(current);
|
|
905
|
+
if (!next.delete(cursor))
|
|
906
|
+
next.add(cursor);
|
|
907
|
+
return next;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: label }), choices.map((choice, i) => (_jsxs(Text, { color: i === cursor ? ACCENT : undefined, bold: i === cursor, children: [i === cursor ? '▸ ' : ' ', multi ? (checked.has(i) ? '◉ ' : '◯ ') : '', choice] }, choice))), _jsx(Text, { dimColor: true, children: multi
|
|
912
|
+
? t('↑↓ move · Space toggle · Enter submit · Esc cancel')
|
|
913
|
+
: t('↑↓ move · Enter select · Esc cancel') })] }));
|
|
914
|
+
}
|
|
915
|
+
function TextPrompt({ label, initial = '', validate, advise, error, onSubmit, onCancel, }) {
|
|
916
|
+
const [value, setValue] = useState(initial);
|
|
917
|
+
const [problem, setProblem] = useState(error);
|
|
918
|
+
// The value an advice was shown for; submitting it again keeps it.
|
|
919
|
+
const [advised, setAdvised] = useState();
|
|
920
|
+
const advice = advised === value ? advise?.(value) : undefined;
|
|
921
|
+
useInput((input, key) => {
|
|
922
|
+
if (key.escape)
|
|
923
|
+
onCancel();
|
|
924
|
+
else if (key.return) {
|
|
925
|
+
const invalid = validate?.(value);
|
|
926
|
+
setProblem(invalid);
|
|
927
|
+
if (invalid)
|
|
928
|
+
return;
|
|
929
|
+
if (advise?.(value) && advised !== value)
|
|
930
|
+
setAdvised(value);
|
|
931
|
+
else
|
|
932
|
+
onSubmit(value);
|
|
933
|
+
}
|
|
616
934
|
else if (key.backspace || key.delete)
|
|
617
935
|
setValue(v => v.slice(0, -1));
|
|
618
936
|
else if (input && !key.ctrl && !key.meta)
|
|
619
937
|
setValue(v => v + input);
|
|
620
938
|
});
|
|
621
|
-
return (_jsxs(Box, { flexDirection: "column", 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') })] }));
|
|
939
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: label }), _jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), value, _jsx(Text, { inverse: true, children: " " })] }), problem && _jsxs(Text, { color: "red", children: ["\u2717 ", problem] }), !problem && advice && _jsx(Text, { color: "yellow", children: advice }), _jsx(Text, { dimColor: true, children: t('Press Enter to submit, Esc to cancel') })] }));
|
|
622
940
|
}
|
package/dist/shell/Tabs.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/shell/actions.js
CHANGED
|
Binary file
|
package/dist/utils/config.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|