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.
- package/CHANGELOG.md +138 -0
- package/LICENSE +21 -0
- package/dist/cli.js +194 -70
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +95 -212
- package/dist/commands/dev.js +87 -416
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +50 -33
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +5 -31
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.js +33 -15
- package/dist/components/PasswordInput.js +2 -1
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.js +44 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.js +703 -0
- package/dist/shell/Frame.js +138 -0
- package/dist/shell/Help.js +96 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.js +149 -0
- package/dist/shell/Shell.js +953 -0
- package/dist/shell/Tabs.js +96 -0
- package/dist/shell/actions.js +493 -0
- package/dist/utils/config.js +62 -2
- package/dist/utils/create-app.js +231 -0
- package/dist/utils/debug.js +32 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.js +369 -0
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +54 -6
- package/dist/utils/oauth2-auth.js +146 -43
- package/dist/utils/password-prompt.js +37 -3
- package/dist/utils/process-runner.js +77 -38
- package/dist/utils/project-detection.js +457 -112
- package/dist/utils/session-cookie-auth.js +10 -6
- package/dist/utils/sitevision-api.js +162 -32
- package/dist/utils/sitevision-scripts-runner.js +50 -22
- package/dist/utils/tasks.js +510 -0
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +9 -5
- package/dist/utils/workspace.js +105 -0
- package/dist/utils/zip.js +18 -19
- package/package.json +27 -4
- package/readme.md +113 -141
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -307
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -16
- package/dist/commands/dev.d.ts +0 -16
- 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 -10
- package/dist/components/DevPropertiesForm.js +0 -210
- 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 -83
- 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/types/index.d.ts +0 -284
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -17
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -27
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -22
- package/dist/utils/project-detection.d.ts +0 -151
- package/dist/utils/session-cookie-auth.d.ts +0 -34
- 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 -112
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/zip.d.ts +0 -71
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
|
5
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
6
|
+
import Spinner from 'ink-spinner';
|
|
7
|
+
import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, syncDevPropertiesToPackageJson, } from '../utils/project-detection.js';
|
|
8
|
+
import { appGroup, configIncomplete, discoverApps, needsOnboarding, } from '../utils/workspace.js';
|
|
9
|
+
import { createAppInTerminal, isEmptyOrMissing, seedNewApp, startCreateApp, appNameAdvice, appNameProblem, } from '../utils/create-app.js';
|
|
10
|
+
import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
|
|
11
|
+
import { useTasks, runningTasks, startActivate, getTasks, scaffoldRunning, } from '../utils/tasks.js';
|
|
12
|
+
import { PasswordInput } from '../components/PasswordInput.js';
|
|
13
|
+
import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
|
|
14
|
+
import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
|
|
15
|
+
import { TabBar, TABS, Overview, Versions, Log, } from './Tabs.js';
|
|
16
|
+
import { CommandPalette } from './CommandPalette.js';
|
|
17
|
+
import { ConfigForm } from './ConfigForm.js';
|
|
18
|
+
import { AddonPicker } from './AddonPicker.js';
|
|
19
|
+
import { SettingsScreen } from './Settings.js';
|
|
20
|
+
import { HelpPanel } from './Help.js';
|
|
21
|
+
import { Popover } from './Popover.js';
|
|
22
|
+
import { ChangelogPanel } from './Changelog.js';
|
|
23
|
+
import { baseEnvironment, environmentColor, environmentNames, environmentProject, isProductionEnvironment, resolveEnvironment, } from '../utils/environments.js';
|
|
24
|
+
import { t } from '../utils/i18n.js';
|
|
25
|
+
import { onKeychainSaveFailed } from '../utils/keychain.js';
|
|
26
|
+
import { actionForKey, actions, authState, resolveDeployConfig, } from './actions.js';
|
|
27
|
+
import { debug } from '../utils/debug.js';
|
|
28
|
+
// Opening a view is not an action worth a debug line.
|
|
29
|
+
const NAVIGATION = new Set([
|
|
30
|
+
'versions',
|
|
31
|
+
'help',
|
|
32
|
+
'changelog',
|
|
33
|
+
'settings',
|
|
34
|
+
'config',
|
|
35
|
+
'workspace-settings',
|
|
36
|
+
]);
|
|
37
|
+
function useSize() {
|
|
38
|
+
const { stdout } = useStdout();
|
|
39
|
+
const read = () => ({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
40
|
+
const [size, setSize] = useState(read);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const onResize = () => setSize(read());
|
|
43
|
+
stdout.on('resize', onResize);
|
|
44
|
+
return () => {
|
|
45
|
+
stdout.off('resize', onResize);
|
|
46
|
+
};
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, [stdout]);
|
|
49
|
+
return size;
|
|
50
|
+
}
|
|
51
|
+
export function Shell({ apps: initialApps, workspaceRoot, version, minimal = false, updatedFrom, handover, skipped, }) {
|
|
52
|
+
const { exit } = useApp();
|
|
53
|
+
const { columns, rows } = useSize();
|
|
54
|
+
const tasks = useTasks();
|
|
55
|
+
const [apps, setApps] = useState(initialApps);
|
|
56
|
+
// A workspace with nothing to deploy against opens on its settings form.
|
|
57
|
+
const onboard = useMemo(() => Boolean(workspaceRoot) && needsOnboarding(workspaceRoot, initialApps), [workspaceRoot, initialApps]);
|
|
58
|
+
const [selected, setSelected] = useState(onboard ? initialApps.length : 0);
|
|
59
|
+
const [tab, setTab] = useState('overview');
|
|
60
|
+
const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
|
|
61
|
+
const [overlays, setOverlays] = useState(updatedFrom ? [{ kind: 'changelog', since: updatedFrom }] : []);
|
|
62
|
+
const setOverlay = useCallback((next) => {
|
|
63
|
+
setOverlays(stack => overlayStack(stack, next));
|
|
64
|
+
}, []);
|
|
65
|
+
const [editing, setEditing] = useState(false);
|
|
66
|
+
// A confirm that turns up mid-edit stays hidden until the field is done.
|
|
67
|
+
const top = overlays.at(-1) ?? null;
|
|
68
|
+
const overlay = top?.kind === 'confirm' && editing ? null : top;
|
|
69
|
+
const [filter, setFilter] = useState('');
|
|
70
|
+
const [versions, setVersions] = useState({});
|
|
71
|
+
const [versionRow, setVersionRow] = useState(0);
|
|
72
|
+
const [logScroll, setLogScroll] = useState(0);
|
|
73
|
+
const [logWrap, setLogWrap] = useState(false);
|
|
74
|
+
const [offerAddon, setOfferAddon] = useState();
|
|
75
|
+
const [notice, setNotice] = useState(skipped?.length
|
|
76
|
+
? {
|
|
77
|
+
text: skipped.length === 1
|
|
78
|
+
? t('Skipped: {reason}', { reason: skipped[0] })
|
|
79
|
+
: t('Skipped {n} apps. First: {reason}', {
|
|
80
|
+
n: skipped.length,
|
|
81
|
+
reason: skipped[0],
|
|
82
|
+
}),
|
|
83
|
+
level: 'warn',
|
|
84
|
+
}
|
|
85
|
+
: null);
|
|
86
|
+
const [, tick] = useReducer((n) => n + 1, 0);
|
|
87
|
+
// In workspace mode the row after the last app is "Workspace settings".
|
|
88
|
+
const settings = Boolean(workspaceRoot) && selected === apps.length;
|
|
89
|
+
const rawProject = apps[Math.min(selected, apps.length - 1)];
|
|
90
|
+
// Active environment, remembered per workspace (or app) in .svcconfig.
|
|
91
|
+
const configRoot = workspaceRoot ?? rawProject.root;
|
|
92
|
+
const [envChoice, setEnvChoice] = useState(() => readSvcConfig(configRoot).environment ?? '');
|
|
93
|
+
const envNames = environmentNames(rawProject.devProperties);
|
|
94
|
+
const envList = envNames.join(',');
|
|
95
|
+
const env = envNames.includes(envChoice)
|
|
96
|
+
? envChoice
|
|
97
|
+
: baseEnvironment(rawProject.devProperties);
|
|
98
|
+
const project = useMemo(() => environmentProject(rawProject, env), [rawProject, env]);
|
|
99
|
+
const isProduction = isProductionEnvironment(env, rawProject.devProperties);
|
|
100
|
+
const versionsKey = `${project.root}|${env}`;
|
|
101
|
+
const single = !workspaceRoot;
|
|
102
|
+
const workspaceTarget = useMemo(() => workspaceRoot
|
|
103
|
+
? {
|
|
104
|
+
root: workspaceRoot,
|
|
105
|
+
base: readWorkspaceDevProperties(workspaceRoot),
|
|
106
|
+
devProperties: resolveEnvironment(readWorkspaceDevProperties(workspaceRoot), env),
|
|
107
|
+
environment: env,
|
|
108
|
+
workspace: true,
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
// Re-read after any reload so saved values show up.
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
+
[workspaceRoot, apps, env]);
|
|
114
|
+
const narrow = minimal || columns < NARROW_BELOW;
|
|
115
|
+
const sidebar = navWidth(columns);
|
|
116
|
+
// The navigator shows the fuzzy matches; `selected` stays an index into
|
|
117
|
+
// `apps` (with `apps.length` meaning the workspace settings row).
|
|
118
|
+
const matches = useMemo(() => navMatches(apps, filter), [apps, filter]);
|
|
119
|
+
const ring = single ? matches : [...matches, apps.length];
|
|
120
|
+
const running = runningTasks();
|
|
121
|
+
// Re-render once a second while something runs so elapsed times move.
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (running.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
const timer = setInterval(tick, 1000);
|
|
126
|
+
return () => clearInterval(timer);
|
|
127
|
+
}, [running.length]);
|
|
128
|
+
const reload = useCallback(() => {
|
|
129
|
+
setApps(current => current.map(app => {
|
|
130
|
+
try {
|
|
131
|
+
return detectProject(app.root) ?? app;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return app;
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
}, []);
|
|
138
|
+
// A finished npm install changes what detection sees (node_modules,
|
|
139
|
+
// sitevision-scripts); re-detect so the status strip flips.
|
|
140
|
+
const installsDone = tasks.filter(task => task.kind === 'install' && task.status !== 'running').length;
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (installsDone > 0)
|
|
143
|
+
reload();
|
|
144
|
+
}, [installsDone, reload]);
|
|
145
|
+
const notify = useCallback((text, level = 'info') => {
|
|
146
|
+
debug('notice', `${level} ${text}`);
|
|
147
|
+
setNotice({ text, level });
|
|
148
|
+
}, []);
|
|
149
|
+
// "Save to keychain" that did not save would otherwise just ask again.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
onKeychainSaveFailed(() => {
|
|
152
|
+
notify(t('Could not save to the OS keychain; you will be asked again next time.'), 'warn');
|
|
153
|
+
});
|
|
154
|
+
}, [notify]);
|
|
155
|
+
const quit = useCallback(() => {
|
|
156
|
+
const leave = () => {
|
|
157
|
+
for (const task of getTasks())
|
|
158
|
+
if (task.status === 'running')
|
|
159
|
+
task.stop();
|
|
160
|
+
exit();
|
|
161
|
+
};
|
|
162
|
+
// Between the scaffolder's questions nothing is open, so q is live:
|
|
163
|
+
// quitting then would leave a half-made app behind without a word.
|
|
164
|
+
if (scaffoldRunning()) {
|
|
165
|
+
setOverlay({
|
|
166
|
+
kind: 'confirm',
|
|
167
|
+
message: t('A new app is still being created. Stop it and quit?'),
|
|
168
|
+
resolve(yes) {
|
|
169
|
+
if (yes)
|
|
170
|
+
leave();
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
leave();
|
|
176
|
+
}
|
|
177
|
+
}, [exit, setOverlay]);
|
|
178
|
+
const context = useMemo(() => ({
|
|
179
|
+
project,
|
|
180
|
+
reload,
|
|
181
|
+
notify,
|
|
182
|
+
quit,
|
|
183
|
+
setTab(next) {
|
|
184
|
+
setTab(next);
|
|
185
|
+
setFocus('content');
|
|
186
|
+
},
|
|
187
|
+
openSettings() {
|
|
188
|
+
setOverlay({ kind: 'settings' });
|
|
189
|
+
},
|
|
190
|
+
openChangelog() {
|
|
191
|
+
setOverlay({ kind: 'changelog' });
|
|
192
|
+
},
|
|
193
|
+
environment: env,
|
|
194
|
+
isProduction,
|
|
195
|
+
cycleEnvironment() {
|
|
196
|
+
const next = envNames[(envNames.indexOf(env) + 1) % envNames.length];
|
|
197
|
+
setEnvChoice(next);
|
|
198
|
+
writeSvcConfig(configRoot, { environment: next });
|
|
199
|
+
notify(t('switched to {env}', { env: next }));
|
|
200
|
+
},
|
|
201
|
+
async addEnvironment() {
|
|
202
|
+
const name = await new Promise(resolve => {
|
|
203
|
+
setOverlay({
|
|
204
|
+
kind: 'prompt',
|
|
205
|
+
label: t('Environment name (e.g. test, prod)'),
|
|
206
|
+
resolve,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
const clean = name
|
|
210
|
+
?.trim()
|
|
211
|
+
.toLowerCase()
|
|
212
|
+
.replaceAll(/[^\d\-a-z]/g, '');
|
|
213
|
+
if (!clean || clean === baseEnvironment(rawProject.devProperties))
|
|
214
|
+
return;
|
|
215
|
+
const targetRoot = workspaceRoot ?? rawProject.root;
|
|
216
|
+
const base = (workspaceRoot
|
|
217
|
+
? readWorkspaceDevProperties(workspaceRoot)
|
|
218
|
+
: rawProject.devProperties);
|
|
219
|
+
if (!base)
|
|
220
|
+
return;
|
|
221
|
+
writeDevProperties(targetRoot, { ...base, environments: { ...base.environments, [clean]: {} } }, { complete: !workspaceRoot });
|
|
222
|
+
reload();
|
|
223
|
+
setEnvChoice(clean);
|
|
224
|
+
writeSvcConfig(configRoot, { environment: clean });
|
|
225
|
+
notify(t('environment {env} added', { env: clean }), 'ok');
|
|
226
|
+
},
|
|
227
|
+
async createApp() {
|
|
228
|
+
// Only what this flow opened is closed when the scaffolder ends.
|
|
229
|
+
const asked = new Set();
|
|
230
|
+
const openOwn = (next) => {
|
|
231
|
+
asked.add(next);
|
|
232
|
+
setOverlay(next);
|
|
233
|
+
};
|
|
234
|
+
if (scaffoldRunning()) {
|
|
235
|
+
notify(t('An app is already being created.'), 'warn');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const promptText = async (label, initial, validate, error, advise) => new Promise(resolve => {
|
|
239
|
+
openOwn({
|
|
240
|
+
kind: 'prompt',
|
|
241
|
+
label,
|
|
242
|
+
initial,
|
|
243
|
+
validate,
|
|
244
|
+
advise,
|
|
245
|
+
error,
|
|
246
|
+
resolve,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
const base = workspaceRoot ?? path.dirname(rawProject.root);
|
|
250
|
+
const suggested = workspaceRoot
|
|
251
|
+
? path.relative(workspaceRoot, path.dirname(rawProject.root)) || '.'
|
|
252
|
+
: base;
|
|
253
|
+
// Asked again, with the reason, until the name is free in that folder.
|
|
254
|
+
let name = '';
|
|
255
|
+
let parentDir = '';
|
|
256
|
+
let appDir = '';
|
|
257
|
+
let taken;
|
|
258
|
+
for (;;) {
|
|
259
|
+
// eslint-disable-next-line no-await-in-loop
|
|
260
|
+
const typed = await promptText(t('Name of the new app'), name, appNameProblem, taken, appNameAdvice);
|
|
261
|
+
if (typed === null)
|
|
262
|
+
return;
|
|
263
|
+
name = typed.trim();
|
|
264
|
+
// Folders that already hold apps, then a way to type another one.
|
|
265
|
+
const folders = [
|
|
266
|
+
...new Set([
|
|
267
|
+
suggested,
|
|
268
|
+
...apps.map(app => workspaceRoot
|
|
269
|
+
? path.relative(base, path.dirname(app.root)) || '.'
|
|
270
|
+
: path.dirname(app.root)),
|
|
271
|
+
]),
|
|
272
|
+
].toSorted((a, b) => a.localeCompare(b));
|
|
273
|
+
// eslint-disable-next-line no-await-in-loop
|
|
274
|
+
const folderPick = await new Promise(resolve => {
|
|
275
|
+
openOwn({
|
|
276
|
+
kind: 'choice',
|
|
277
|
+
label: t('Create it in folder'),
|
|
278
|
+
choices: [...folders, t('Other folder…')],
|
|
279
|
+
multi: false,
|
|
280
|
+
initial: [folders.indexOf(suggested)],
|
|
281
|
+
resolve,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
if (!folderPick)
|
|
285
|
+
return;
|
|
286
|
+
const folder = folders[folderPick[0]] ??
|
|
287
|
+
// eslint-disable-next-line no-await-in-loop
|
|
288
|
+
(await promptText(t('Create it in folder'), suggested));
|
|
289
|
+
if (folder === null)
|
|
290
|
+
return;
|
|
291
|
+
parentDir = path.resolve(base, folder.trim() || '.');
|
|
292
|
+
appDir = path.join(parentDir, name);
|
|
293
|
+
if (isEmptyOrMissing(appDir))
|
|
294
|
+
break;
|
|
295
|
+
taken = t('{dir} already exists. Pick another name, or keep it and choose a different folder next.', { dir: path.relative(base, appDir) });
|
|
296
|
+
}
|
|
297
|
+
// Both were checked above, so a run that leaves no app may remove them.
|
|
298
|
+
const madeParent = !fs.existsSync(parentDir);
|
|
299
|
+
const discard = () => {
|
|
300
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
301
|
+
if (madeParent && isEmptyOrMissing(parentDir))
|
|
302
|
+
fs.rmSync(parentDir, { recursive: true, force: true });
|
|
303
|
+
};
|
|
304
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
305
|
+
const known = (workspaceRoot
|
|
306
|
+
? readWorkspaceDevProperties(workspaceRoot)
|
|
307
|
+
: rawProject.devProperties);
|
|
308
|
+
const ask = async (question) => {
|
|
309
|
+
// svc keeps passwords in the keychain, never in the app's file.
|
|
310
|
+
if (question.name === 'password')
|
|
311
|
+
return { skip: true };
|
|
312
|
+
const shared = known?.[question.name];
|
|
313
|
+
if (!question.error &&
|
|
314
|
+
['domain', 'siteName', 'username', 'useHTTPForDevDeploy'].includes(question.name) &&
|
|
315
|
+
shared !== undefined &&
|
|
316
|
+
shared !== '')
|
|
317
|
+
return { value: shared };
|
|
318
|
+
const label = question.error
|
|
319
|
+
? `${question.message} (${question.error})`
|
|
320
|
+
: question.message;
|
|
321
|
+
if (question.type === 'confirm') {
|
|
322
|
+
const value = await new Promise(resolve => {
|
|
323
|
+
openOwn({ kind: 'confirm', message: label, resolve });
|
|
324
|
+
});
|
|
325
|
+
return { value };
|
|
326
|
+
}
|
|
327
|
+
if (question.type === 'password') {
|
|
328
|
+
const secret = await new Promise(resolve => {
|
|
329
|
+
openOwn({ kind: 'password', label, resolve });
|
|
330
|
+
});
|
|
331
|
+
return secret && { value: secret.password };
|
|
332
|
+
}
|
|
333
|
+
if (question.choices.length > 0) {
|
|
334
|
+
const multi = question.type === 'checkbox';
|
|
335
|
+
const picked = await new Promise(resolve => {
|
|
336
|
+
openOwn({
|
|
337
|
+
kind: 'choice',
|
|
338
|
+
label,
|
|
339
|
+
choices: question.choices,
|
|
340
|
+
multi,
|
|
341
|
+
initial: [question.default ?? []].flat(),
|
|
342
|
+
resolve,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
return picked && { value: multi ? picked : picked[0] };
|
|
346
|
+
}
|
|
347
|
+
const initial = question.default ?? (question.name === 'addonName' ? name : '');
|
|
348
|
+
const value = await promptText(label, typeof initial === 'string' ? initial : JSON.stringify(initial));
|
|
349
|
+
return value === null ? null : { value };
|
|
350
|
+
};
|
|
351
|
+
notify(t('creating {app}: installing, questions follow', { app: name }));
|
|
352
|
+
setTab('log');
|
|
353
|
+
const { done } = startCreateApp({ name, parentDir, ask });
|
|
354
|
+
let outcome = await done;
|
|
355
|
+
setOverlays(stack => withoutOverlays(stack, asked));
|
|
356
|
+
if (outcome === 'unmanaged') {
|
|
357
|
+
if (!handover) {
|
|
358
|
+
notify(t('The scaffolder could not be run from here'), 'error');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
362
|
+
handover(async () => {
|
|
363
|
+
if (await createAppInTerminal(name, parentDir))
|
|
364
|
+
seedNewApp(appDir, workspaceRoot);
|
|
365
|
+
else
|
|
366
|
+
discard();
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (outcome !== 'created') {
|
|
371
|
+
discard();
|
|
372
|
+
if (outcome === 'failed')
|
|
373
|
+
notify(t('creating {app} failed, see the log', { app: name }), 'error');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
seedNewApp(appDir, workspaceRoot);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
notify(error instanceof Error ? error.message : String(error), 'warn');
|
|
381
|
+
outcome = 'failed';
|
|
382
|
+
}
|
|
383
|
+
if (!workspaceRoot ||
|
|
384
|
+
path.relative(workspaceRoot, appDir).startsWith('..')) {
|
|
385
|
+
notify(t('{app} created in {dir}', { app: name, dir: appDir }), 'ok');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const left = [];
|
|
389
|
+
const found = discoverApps(workspaceRoot, left);
|
|
390
|
+
if (left[0])
|
|
391
|
+
notify(t('Skipped: {reason}', { reason: left[0] }), 'warn');
|
|
392
|
+
setApps(found);
|
|
393
|
+
setFilter('');
|
|
394
|
+
setSelected(Math.max(0, found.findIndex(app => app.root === appDir)));
|
|
395
|
+
if (outcome === 'created')
|
|
396
|
+
notify(t('{app} created', { app: name }), 'ok');
|
|
397
|
+
setOfferAddon(appDir);
|
|
398
|
+
},
|
|
399
|
+
openWorkspaceSettings: workspaceRoot
|
|
400
|
+
? () => {
|
|
401
|
+
setSelected(apps.length);
|
|
402
|
+
setFocus('content');
|
|
403
|
+
}
|
|
404
|
+
: undefined,
|
|
405
|
+
askPassword: (label, rememberLabel) => new Promise(resolve => {
|
|
406
|
+
setOverlay({ kind: 'password', label, rememberLabel, resolve });
|
|
407
|
+
}),
|
|
408
|
+
login: method => new Promise(resolve => {
|
|
409
|
+
setOverlay({
|
|
410
|
+
kind: 'login',
|
|
411
|
+
method,
|
|
412
|
+
devProperties: project.devProperties,
|
|
413
|
+
resolve,
|
|
414
|
+
});
|
|
415
|
+
}),
|
|
416
|
+
confirm: message => new Promise(resolve => {
|
|
417
|
+
setOverlay({ kind: 'confirm', message, resolve });
|
|
418
|
+
}),
|
|
419
|
+
openHelp() {
|
|
420
|
+
setOverlay({ kind: 'help' });
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
424
|
+
[project, reload, notify, quit, workspaceRoot, apps.length, env, envList]);
|
|
425
|
+
const run = useCallback((action) => {
|
|
426
|
+
setOverlay(null);
|
|
427
|
+
if (action.enabled && !action.enabled(project)) {
|
|
428
|
+
notify(`${t(action.label)}: ${action.detail?.(project) ?? t('not available')}`, 'warn');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!NAVIGATION.has(action.id))
|
|
432
|
+
debug('action', action.id);
|
|
433
|
+
action.run(context).catch((error) => {
|
|
434
|
+
notify(error instanceof Error ? error.message : String(error), 'error');
|
|
435
|
+
});
|
|
436
|
+
}, [context, project, notify, setOverlay]);
|
|
437
|
+
const fetchVersions = useCallback(async (fresh = false) => {
|
|
438
|
+
const key = versionsKey;
|
|
439
|
+
const config = await resolveDeployConfig(context, fresh);
|
|
440
|
+
if (!config)
|
|
441
|
+
return;
|
|
442
|
+
setVersions(v => ({
|
|
443
|
+
...v,
|
|
444
|
+
[key]: { ...v[key], loading: true, error: undefined },
|
|
445
|
+
}));
|
|
446
|
+
const result = await listExecutables(config);
|
|
447
|
+
setVersions(v => ({
|
|
448
|
+
...v,
|
|
449
|
+
[key]: {
|
|
450
|
+
loading: false,
|
|
451
|
+
executables: result.executables ?? v[key]?.executables,
|
|
452
|
+
error: result.error,
|
|
453
|
+
fetchedAt: result.success ? Date.now() : v[key]?.fetchedAt,
|
|
454
|
+
},
|
|
455
|
+
}));
|
|
456
|
+
setVersionRow(0);
|
|
457
|
+
}, [versionsKey, context]);
|
|
458
|
+
const activateSelected = useCallback(async () => {
|
|
459
|
+
const executable = versions[versionsKey]?.executables?.[versionRow];
|
|
460
|
+
if (!executable)
|
|
461
|
+
return;
|
|
462
|
+
if (executable.active) {
|
|
463
|
+
notify(t('{v} is already active', { v: executable.appVersion }));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const config = await resolveDeployConfig(context);
|
|
467
|
+
if (!config)
|
|
468
|
+
return;
|
|
469
|
+
const task = startActivate(project, config, executable.id, executable.appVersion);
|
|
470
|
+
const wait = () => new Promise(resolve => {
|
|
471
|
+
const check = () => task.status === 'running' ? setTimeout(check, 200) : resolve();
|
|
472
|
+
check();
|
|
473
|
+
});
|
|
474
|
+
await wait();
|
|
475
|
+
notify(task.error ?? t('{v} activated', { v: executable.appVersion }), task.error ? 'error' : 'ok');
|
|
476
|
+
await fetchVersions();
|
|
477
|
+
}, [
|
|
478
|
+
versions,
|
|
479
|
+
versionsKey,
|
|
480
|
+
project,
|
|
481
|
+
versionRow,
|
|
482
|
+
context,
|
|
483
|
+
notify,
|
|
484
|
+
fetchVersions,
|
|
485
|
+
]);
|
|
486
|
+
// A popover owns the keyboard; the form stays mounted underneath it.
|
|
487
|
+
const formActive = (tab === 'config' || settings) && focus === 'content' && !overlay;
|
|
488
|
+
const pickAddon = useCallback(async () => new Promise(resolve => {
|
|
489
|
+
setOverlay({ kind: 'picker', resolve });
|
|
490
|
+
}), [setOverlay]);
|
|
491
|
+
// A freshly created app: once it is the selected project, offer its addon.
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
if (!offerAddon || project.root !== offerAddon || overlay)
|
|
494
|
+
return;
|
|
495
|
+
setOfferAddon(undefined);
|
|
496
|
+
const addon = project.devProperties?.addonName;
|
|
497
|
+
const action = actions.find(entry => entry.id === 'create-addon');
|
|
498
|
+
if (!addon || !action || !project.devProperties?.domain)
|
|
499
|
+
return;
|
|
500
|
+
void context
|
|
501
|
+
.confirm(t('Create the addon "{addon}" on {domain} now?', {
|
|
502
|
+
addon,
|
|
503
|
+
domain: project.devProperties.domain,
|
|
504
|
+
}))
|
|
505
|
+
.then(yes => {
|
|
506
|
+
if (yes)
|
|
507
|
+
run(action);
|
|
508
|
+
});
|
|
509
|
+
}, [offerAddon, project, overlay, context, run]);
|
|
510
|
+
const loadAddons = useCallback(async () => {
|
|
511
|
+
const config = await resolveDeployConfig(context, false, { addon: false });
|
|
512
|
+
return config ? listAddons(config) : { error: t('No credentials.') };
|
|
513
|
+
}, [context]);
|
|
514
|
+
const appTasks = tasks.filter(task => task.appRoot === project.root);
|
|
515
|
+
// A new app is not in the list while it is being created, so its log shows
|
|
516
|
+
// wherever you are; a failed one stays up until another task runs.
|
|
517
|
+
const creating = tasks.findLast(task => task.kind === 'create');
|
|
518
|
+
const logTask = appTasks.find(task => task.status === 'running') ??
|
|
519
|
+
(creating?.status === 'running' ||
|
|
520
|
+
(creating?.status === 'error' && creating === tasks.at(-1))
|
|
521
|
+
? creating
|
|
522
|
+
: appTasks.at(-1));
|
|
523
|
+
useInput((raw, key) => {
|
|
524
|
+
// Terminals speaking the kitty keyboard protocol report shift+s as
|
|
525
|
+
// "s" plus a shift flag; fold that back into the uppercase letter so
|
|
526
|
+
// P and K behave the same everywhere.
|
|
527
|
+
const input = key.shift && raw.length === 1 && /[a-z]/.test(raw)
|
|
528
|
+
? raw.toUpperCase()
|
|
529
|
+
: raw;
|
|
530
|
+
if (key.escape) {
|
|
531
|
+
if (focus === 'nav' && filter) {
|
|
532
|
+
setFilter('');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Esc backs out of the workspace settings pane, not just its focus.
|
|
536
|
+
if (settings)
|
|
537
|
+
setSelected(0);
|
|
538
|
+
setFocus(single ? 'content' : 'nav');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// Ctrl+D is not d: only plain keys are shortcuts.
|
|
542
|
+
if (key.ctrl || key.meta)
|
|
543
|
+
return;
|
|
544
|
+
if (input === '/') {
|
|
545
|
+
setOverlay({ kind: 'palette' });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (input === '?') {
|
|
549
|
+
setOverlay({ kind: 'help' });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Reachable from the navigator's search and the workspace form too.
|
|
553
|
+
if (input === ',') {
|
|
554
|
+
setOverlay({ kind: 'settings' });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// The config form uses Tab/Shift+Tab to move between fields.
|
|
558
|
+
if (key.tab && !formActive) {
|
|
559
|
+
setFilter('');
|
|
560
|
+
setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Navigator: typing searches, so no action key fires until Enter has
|
|
564
|
+
// moved the focus into the content pane.
|
|
565
|
+
if (focus === 'nav') {
|
|
566
|
+
const move = (delta) => {
|
|
567
|
+
setSelected(navMove(ring, selected, delta));
|
|
568
|
+
};
|
|
569
|
+
const search = (next) => {
|
|
570
|
+
setFilter(next);
|
|
571
|
+
const found = navMatches(apps, next);
|
|
572
|
+
if (found.length > 0 && !found.includes(selected))
|
|
573
|
+
setSelected(found[0]);
|
|
574
|
+
};
|
|
575
|
+
if (key.upArrow)
|
|
576
|
+
move(-1);
|
|
577
|
+
else if (key.downArrow)
|
|
578
|
+
move(1);
|
|
579
|
+
else if (key.return) {
|
|
580
|
+
if (matches.length > 0 || settings) {
|
|
581
|
+
setFilter('');
|
|
582
|
+
setFocus('content');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else if (key.backspace || key.delete)
|
|
586
|
+
search(filter.slice(0, -1));
|
|
587
|
+
else if (input === 'q' && !filter)
|
|
588
|
+
quit();
|
|
589
|
+
else if (input?.length === 1 && input >= ' ' && !key.ctrl && !key.meta)
|
|
590
|
+
search(filter + input);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (settings && focus === 'content') {
|
|
594
|
+
// Settings pane: the form owns everything but q, y and Tab/Esc above.
|
|
595
|
+
if (input === 'q')
|
|
596
|
+
quit();
|
|
597
|
+
else if (input === 'y') {
|
|
598
|
+
try {
|
|
599
|
+
if (syncDevPropertiesToPackageJson(workspaceRoot)) {
|
|
600
|
+
reload();
|
|
601
|
+
notify(t('package.json updated'), 'ok');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
notify(error instanceof Error ? error.message : String(error), 'error');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (input === 'a' && tab !== 'versions') {
|
|
611
|
+
setTab('versions');
|
|
612
|
+
setFocus('content');
|
|
613
|
+
if (!Object.hasOwn(versions, versionsKey))
|
|
614
|
+
void fetchVersions(false);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const digit = Number.parseInt(input, 10);
|
|
618
|
+
if (digit >= 1 && digit <= TABS.length) {
|
|
619
|
+
setTab(TABS[digit - 1].id);
|
|
620
|
+
setFocus('content');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (key.leftArrow || key.rightArrow) {
|
|
624
|
+
const i = TABS.findIndex(entry => entry.id === tab);
|
|
625
|
+
setTab(TABS[(i + (key.rightArrow ? 1 : TABS.length - 1)) % TABS.length].id);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (tab === 'versions') {
|
|
629
|
+
const count = versions[versionsKey]?.executables?.length ?? 0;
|
|
630
|
+
if (key.upArrow)
|
|
631
|
+
setVersionRow(r => Math.max(0, r - 1));
|
|
632
|
+
if (key.downArrow)
|
|
633
|
+
setVersionRow(r => Math.min(Math.max(0, count - 1), r + 1));
|
|
634
|
+
if (input === 'r')
|
|
635
|
+
void fetchVersions(false);
|
|
636
|
+
else if (input === 'a' && count > 0)
|
|
637
|
+
void activateSelected();
|
|
638
|
+
}
|
|
639
|
+
else if (tab === 'log') {
|
|
640
|
+
const max = Math.max(0, (logTask?.lines.length ?? 0) - 1);
|
|
641
|
+
if (key.upArrow)
|
|
642
|
+
setLogScroll(s => Math.min(max, s + 1));
|
|
643
|
+
if (key.downArrow)
|
|
644
|
+
setLogScroll(s => Math.max(0, s - 1));
|
|
645
|
+
if (key.pageUp)
|
|
646
|
+
setLogScroll(s => Math.min(max, s + 10));
|
|
647
|
+
if (key.pageDown)
|
|
648
|
+
setLogScroll(s => Math.max(0, s - 10));
|
|
649
|
+
if (input === 'f')
|
|
650
|
+
setLogScroll(0);
|
|
651
|
+
else if (input === 'x')
|
|
652
|
+
setLogWrap(w => !w);
|
|
653
|
+
}
|
|
654
|
+
const action = actionForKey(input);
|
|
655
|
+
if (action)
|
|
656
|
+
run(action);
|
|
657
|
+
}, { isActive: overlay === null && !editing });
|
|
658
|
+
// Frame geometry: one row for Ink's trailing newline, top bar, bottom bar.
|
|
659
|
+
// The sidebar layout needs 10 rows to stack its own fixed rows without
|
|
660
|
+
// overflowing; the compact one has no sidebar and fits in 6.
|
|
661
|
+
const frameRows = Math.max(narrow ? 6 : 10, rows - 1);
|
|
662
|
+
const mainHeight = frameRows - 2;
|
|
663
|
+
const contentHeight = mainHeight - 1 - (narrow ? 1 : 0);
|
|
664
|
+
const groupOf = (app) => workspaceRoot ? appGroup(workspaceRoot, app.root) : '.';
|
|
665
|
+
const appName = appLabel(project);
|
|
666
|
+
const tabName = t(TABS.find(entry => entry.id === tab).label).toLowerCase();
|
|
667
|
+
const contextLabel = settings
|
|
668
|
+
? `${t('workspace')} ▸ ${t('settings')}`
|
|
669
|
+
: workspaceRoot
|
|
670
|
+
? `${t('workspace')} ▸ ${path.relative(workspaceRoot, project.root)} ▸ ${tabName}`
|
|
671
|
+
: `${appName} ▸ ${tabName}`;
|
|
672
|
+
const h = (pairs) => pairs.map(([key, label]) => ({ key, label: t(label) }));
|
|
673
|
+
const settingsHints = editing
|
|
674
|
+
? h([
|
|
675
|
+
['Enter', 'save'],
|
|
676
|
+
['Esc', 'cancel'],
|
|
677
|
+
])
|
|
678
|
+
: formActive
|
|
679
|
+
? h([
|
|
680
|
+
['↑↓', 'field'],
|
|
681
|
+
['Enter', 'edit'],
|
|
682
|
+
['y', 'sync'],
|
|
683
|
+
['Esc', 'back'],
|
|
684
|
+
['q', 'quit'],
|
|
685
|
+
])
|
|
686
|
+
: h([
|
|
687
|
+
['Enter', 'edit settings'],
|
|
688
|
+
['↑↓', 'apps'],
|
|
689
|
+
['q', 'quit'],
|
|
690
|
+
]);
|
|
691
|
+
const navHints = filter
|
|
692
|
+
? h([
|
|
693
|
+
['↑↓', 'move'],
|
|
694
|
+
['Enter', 'select'],
|
|
695
|
+
['Esc', 'clear'],
|
|
696
|
+
['/', 'commands'],
|
|
697
|
+
])
|
|
698
|
+
: h([
|
|
699
|
+
['a–z', 'search'],
|
|
700
|
+
['↑↓', 'move'],
|
|
701
|
+
['Enter', 'select'],
|
|
702
|
+
['/', 'commands'],
|
|
703
|
+
['q', 'quit'],
|
|
704
|
+
]);
|
|
705
|
+
const where = focus === 'nav' && !settings ? 'nav' : settings ? 'workspace' : tab;
|
|
706
|
+
const hereHints = focus === 'nav' && !settings
|
|
707
|
+
? navHints
|
|
708
|
+
: settings
|
|
709
|
+
? settingsHints
|
|
710
|
+
: tab === 'versions'
|
|
711
|
+
? h([
|
|
712
|
+
['a', 'activate'],
|
|
713
|
+
['r', 'refresh'],
|
|
714
|
+
['p', 'deploy'],
|
|
715
|
+
['P', 'force'],
|
|
716
|
+
['/', 'commands'],
|
|
717
|
+
['q', 'quit'],
|
|
718
|
+
])
|
|
719
|
+
: tab === 'log'
|
|
720
|
+
? h([
|
|
721
|
+
['f', 'follow'],
|
|
722
|
+
['x', 'wrap'],
|
|
723
|
+
['K', 'stop'],
|
|
724
|
+
['p', 'deploy'],
|
|
725
|
+
['P', 'force'],
|
|
726
|
+
['/', 'commands'],
|
|
727
|
+
['q', 'quit'],
|
|
728
|
+
])
|
|
729
|
+
: tab === 'config'
|
|
730
|
+
? editing
|
|
731
|
+
? h([
|
|
732
|
+
['Enter', 'save'],
|
|
733
|
+
['Esc', 'cancel'],
|
|
734
|
+
])
|
|
735
|
+
: formActive
|
|
736
|
+
? h([
|
|
737
|
+
['↑↓', 'field'],
|
|
738
|
+
['Enter', 'edit'],
|
|
739
|
+
['^O', 'pick addon'],
|
|
740
|
+
['y', 'sync'],
|
|
741
|
+
['/', 'commands'],
|
|
742
|
+
['q', 'quit'],
|
|
743
|
+
])
|
|
744
|
+
: h([
|
|
745
|
+
['Tab', 'edit'],
|
|
746
|
+
['y', 'sync'],
|
|
747
|
+
['l', 'login'],
|
|
748
|
+
['/', 'commands'],
|
|
749
|
+
['q', 'quit'],
|
|
750
|
+
])
|
|
751
|
+
: h([
|
|
752
|
+
['d', 'dev'],
|
|
753
|
+
['w', 'watch'],
|
|
754
|
+
['b', 'build'],
|
|
755
|
+
['s', 'sign'],
|
|
756
|
+
['p', 'deploy'],
|
|
757
|
+
['P', 'force'],
|
|
758
|
+
['v', 'env'],
|
|
759
|
+
['K', 'stop'],
|
|
760
|
+
['a', 'versions'],
|
|
761
|
+
['e', 'config'],
|
|
762
|
+
['i', 'install'],
|
|
763
|
+
['l', 'login'],
|
|
764
|
+
[',', 'settings'],
|
|
765
|
+
['/', 'commands'],
|
|
766
|
+
['q', 'quit'],
|
|
767
|
+
]);
|
|
768
|
+
const hints = overlay
|
|
769
|
+
? h([['Esc', 'cancel']])
|
|
770
|
+
: editing
|
|
771
|
+
? hereHints
|
|
772
|
+
: [...hereHints, ...h([['?', 'help']])];
|
|
773
|
+
const right = running.length > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', running[0].label, " ", running[0].appName, running.length > 1 && (_jsxs(Text, { dimColor: true, children: [" \u00B7 ", t('{n} tasks', { n: running.length })] }))] })) : notice ? (_jsx(Text, { color: { info: undefined, ok: 'green', warn: 'yellow', error: 'red' }[notice.level], wrap: "truncate", children: notice.text })) : (_jsx(Text, { dimColor: true, children: t('idle') }));
|
|
774
|
+
const closeOverlay = () => setOverlay(null);
|
|
775
|
+
const popoverWidth = Math.min(96, columns - 8);
|
|
776
|
+
const popoverHeight = Math.min(30, frameRows - 4);
|
|
777
|
+
const popover = overlay &&
|
|
778
|
+
renderOverlay(overlay, {
|
|
779
|
+
project,
|
|
780
|
+
here: hereHints,
|
|
781
|
+
where,
|
|
782
|
+
closeOverlay,
|
|
783
|
+
run,
|
|
784
|
+
notify,
|
|
785
|
+
loadAddons,
|
|
786
|
+
height: popoverHeight - 2,
|
|
787
|
+
rerender: tick,
|
|
788
|
+
openWorkspace: workspaceRoot
|
|
789
|
+
? () => {
|
|
790
|
+
setOverlay(null);
|
|
791
|
+
setSelected(apps.length);
|
|
792
|
+
setFocus('content');
|
|
793
|
+
}
|
|
794
|
+
: undefined,
|
|
795
|
+
});
|
|
796
|
+
const content = settings && workspaceTarget ? (_jsx(ConfigForm, { project: workspaceTarget, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: async () => null, onSaved: () => {
|
|
797
|
+
reload();
|
|
798
|
+
notify(t('workspace config saved'), 'ok');
|
|
799
|
+
}, onEditingChange: setEditing }, "workspace")) : (_jsxs(_Fragment, { children: [tab === 'overview' && (_jsx(Overview, { project: project, tasks: tasks, height: contentHeight })), tab === 'config' && (_jsx(ConfigForm, { project: {
|
|
800
|
+
root: project.root,
|
|
801
|
+
devProperties: project.devProperties,
|
|
802
|
+
base: rawProject.devProperties,
|
|
803
|
+
environment: env,
|
|
804
|
+
workspaceRoot,
|
|
805
|
+
manifest: project.manifest,
|
|
806
|
+
manifestPath: project.paths.manifest,
|
|
807
|
+
}, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: pickAddon, onSaved: () => {
|
|
808
|
+
reload();
|
|
809
|
+
notify(t('config saved'), 'ok');
|
|
810
|
+
}, onEditingChange: setEditing }, `${project.root}|${env}`)), tab === 'versions' && (_jsx(Versions, { project: project, state: versions[versionsKey], selected: versionRow })), tab === 'log' && (_jsx(Log, { task: logTask, height: contentHeight, scroll: logScroll, wrap: logWrap }))] }));
|
|
811
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
|
|
812
|
+
name: env,
|
|
813
|
+
color: environmentColor(env, rawProject.devProperties),
|
|
814
|
+
}, version: version }), _jsxs(Box, { flexGrow: 1, height: mainHeight, children: [!narrow && (_jsx(Navigator, { apps: matches.map(i => apps[i]), groupOf: groupOf, selected: matches.indexOf(selected), focused: focus === 'nav', tasks: tasks, height: mainHeight, single: single, settingsSelected: settings, width: sidebar, filter: filter })), _jsxs(Box, { flexDirection: "column", width: narrow ? columns : columns - sidebar, overflow: "hidden", borderStyle: "single", borderLeft: false, borderRight: false, borderBottom: false, borderColor: focus === 'content' ? ACCENT : undefined, borderDimColor: focus !== 'content', children: [narrow && !single && (_jsx(NavigatorStrip, { apps: matches.map(i => apps[i]), selected: matches.indexOf(selected), focused: focus === 'nav', width: columns, filter: filter })), settings ? (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: ACCENT, children: t('Workspace settings') }), _jsx(Text, { dimColor: true, children: onboard && configIncomplete(workspaceTarget?.base)
|
|
815
|
+
? t(' · new workspace: fill in once, every app inherits · Esc skips')
|
|
816
|
+
: t(' · shared .dev_properties.json at the root') })] })) : (_jsx(TabBar, { tab: tab, narrow: narrow, focused: focus === 'content' })), _jsx(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden", alignItems: "flex-start", children: content })] })] }), _jsx(BottomBar, { hints: hints, right: right }), popover && (_jsx(Popover, { columns: columns, rows: frameRows, width: popoverWidth, height: popoverHeight, children: popover }))] }));
|
|
817
|
+
}
|
|
818
|
+
function renderOverlay(overlay, env) {
|
|
819
|
+
const { project, here, where, closeOverlay, run, notify, loadAddons, height, rerender, openWorkspace, } = env;
|
|
820
|
+
switch (overlay.kind) {
|
|
821
|
+
case 'palette':
|
|
822
|
+
return (_jsx(CommandPalette, { project: project, onRun: run, onClose: closeOverlay, height: height }));
|
|
823
|
+
case 'password':
|
|
824
|
+
return (_jsx(PasswordInput, { label: overlay.label, showRememberOption: Boolean(overlay.rememberLabel), rememberLabel: overlay.rememberLabel ? `${overlay.rememberLabel}: ` : undefined, onSubmit: (password, remember) => {
|
|
825
|
+
closeOverlay();
|
|
826
|
+
overlay.resolve({ password, remember });
|
|
827
|
+
}, onCancel: () => {
|
|
828
|
+
closeOverlay();
|
|
829
|
+
overlay.resolve(null);
|
|
830
|
+
} }, overlay.label));
|
|
831
|
+
case 'login':
|
|
832
|
+
return (_jsx(AuthLoginScreen, { method: overlay.method, devProperties: overlay.devProperties, onComplete: credential => {
|
|
833
|
+
closeOverlay();
|
|
834
|
+
overlay.resolve(credential);
|
|
835
|
+
}, onError: message => {
|
|
836
|
+
closeOverlay();
|
|
837
|
+
notify(message, 'error');
|
|
838
|
+
overlay.resolve(null);
|
|
839
|
+
}, onCancel: () => {
|
|
840
|
+
closeOverlay();
|
|
841
|
+
overlay.resolve(null);
|
|
842
|
+
} }));
|
|
843
|
+
case 'confirm':
|
|
844
|
+
return (_jsx(Confirm, { message: overlay.message, onAnswer: answer => {
|
|
845
|
+
closeOverlay();
|
|
846
|
+
overlay.resolve(answer);
|
|
847
|
+
} }));
|
|
848
|
+
case 'prompt':
|
|
849
|
+
return (_jsx(TextPrompt, { label: overlay.label, initial: overlay.initial, validate: overlay.validate, advise: overlay.advise, error: overlay.error, onSubmit: value => {
|
|
850
|
+
closeOverlay();
|
|
851
|
+
overlay.resolve(value);
|
|
852
|
+
}, onCancel: () => {
|
|
853
|
+
closeOverlay();
|
|
854
|
+
overlay.resolve(null);
|
|
855
|
+
} }, overlay.label));
|
|
856
|
+
case 'choice':
|
|
857
|
+
return (_jsx(ChoicePrompt, { label: overlay.label, choices: overlay.choices, multi: overlay.multi, initial: overlay.initial, onSubmit: picked => {
|
|
858
|
+
closeOverlay();
|
|
859
|
+
overlay.resolve(picked);
|
|
860
|
+
}, onCancel: () => {
|
|
861
|
+
closeOverlay();
|
|
862
|
+
overlay.resolve(null);
|
|
863
|
+
} }, overlay.label));
|
|
864
|
+
case 'help':
|
|
865
|
+
return (_jsx(HelpPanel, { here: here, where: where, height: height, onClose: closeOverlay }));
|
|
866
|
+
case 'changelog':
|
|
867
|
+
return (_jsx(ChangelogPanel, { since: overlay.since, height: height, onClose: closeOverlay }));
|
|
868
|
+
case 'settings':
|
|
869
|
+
return (_jsx(SettingsScreen, { onChanged: rerender, onClose: closeOverlay, onOpenWorkspace: openWorkspace }));
|
|
870
|
+
case 'picker':
|
|
871
|
+
return (_jsx(AddonPicker, { domain: project.devProperties?.domain ?? '', appType: appTypeOf(project.manifest), initialQuery: localizedText(project.manifest.name), load: loadAddons, height: height, onSelect: name => {
|
|
872
|
+
closeOverlay();
|
|
873
|
+
overlay.resolve(name);
|
|
874
|
+
}, onClose: () => {
|
|
875
|
+
closeOverlay();
|
|
876
|
+
overlay.resolve(null);
|
|
877
|
+
} }));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* The open overlays, last one shown. `null` closes the shown one. A new overlay
|
|
882
|
+
* covers the current one instead of replacing it, so whoever awaits the covered
|
|
883
|
+
* one still gets an answer. A confirm goes underneath: background tasks raise
|
|
884
|
+
* them, and on top it would take the keys being typed into a prompt.
|
|
885
|
+
*/
|
|
886
|
+
export function overlayStack(stack, next) {
|
|
887
|
+
if (next === null)
|
|
888
|
+
return stack.slice(0, -1);
|
|
889
|
+
return next.kind === 'confirm' ? [next, ...stack] : [...stack, next];
|
|
890
|
+
}
|
|
891
|
+
export function withoutOverlays(stack, gone) {
|
|
892
|
+
return stack.filter(overlay => !gone.has(overlay));
|
|
893
|
+
}
|
|
894
|
+
export function Confirm({ message, onAnswer, }) {
|
|
895
|
+
useInput((input, key) => {
|
|
896
|
+
if (input === 'y' || input === 'Y')
|
|
897
|
+
onAnswer(true);
|
|
898
|
+
else if (input === 'n' || input === 'N' || key.escape)
|
|
899
|
+
onAnswer(false);
|
|
900
|
+
});
|
|
901
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: message }), _jsx(Text, { dimColor: true, children: t('y confirm · n cancel') })] }));
|
|
902
|
+
}
|
|
903
|
+
function ChoicePrompt({ label, choices, multi, initial, onSubmit, onCancel, }) {
|
|
904
|
+
const [cursor, setCursor] = useState(multi ? 0 : (initial[0] ?? 0));
|
|
905
|
+
const [checked, setChecked] = useState(() => new Set(multi ? initial : []));
|
|
906
|
+
useInput((input, key) => {
|
|
907
|
+
if (key.escape)
|
|
908
|
+
onCancel();
|
|
909
|
+
else if (key.return)
|
|
910
|
+
onSubmit(multi ? [...checked].toSorted((a, b) => a - b) : [cursor]);
|
|
911
|
+
else if (key.upArrow)
|
|
912
|
+
setCursor(c => (c - 1 + choices.length) % choices.length);
|
|
913
|
+
else if (key.downArrow)
|
|
914
|
+
setCursor(c => (c + 1) % choices.length);
|
|
915
|
+
else if (multi && input === ' ') {
|
|
916
|
+
setChecked(current => {
|
|
917
|
+
const next = new Set(current);
|
|
918
|
+
if (!next.delete(cursor))
|
|
919
|
+
next.add(cursor);
|
|
920
|
+
return next;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
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
|
|
925
|
+
? t('↑↓ move · Space toggle · Enter submit · Esc cancel')
|
|
926
|
+
: t('↑↓ move · Enter select · Esc cancel') })] }));
|
|
927
|
+
}
|
|
928
|
+
function TextPrompt({ label, initial = '', validate, advise, error, onSubmit, onCancel, }) {
|
|
929
|
+
const [value, setValue] = useState(initial);
|
|
930
|
+
const [problem, setProblem] = useState(error);
|
|
931
|
+
// The value an advice was shown for; submitting it again keeps it.
|
|
932
|
+
const [advised, setAdvised] = useState();
|
|
933
|
+
const advice = advised === value ? advise?.(value) : undefined;
|
|
934
|
+
useInput((input, key) => {
|
|
935
|
+
if (key.escape)
|
|
936
|
+
onCancel();
|
|
937
|
+
else if (key.return) {
|
|
938
|
+
const invalid = validate?.(value);
|
|
939
|
+
setProblem(invalid);
|
|
940
|
+
if (invalid)
|
|
941
|
+
return;
|
|
942
|
+
if (advise?.(value) && advised !== value)
|
|
943
|
+
setAdvised(value);
|
|
944
|
+
else
|
|
945
|
+
onSubmit(value);
|
|
946
|
+
}
|
|
947
|
+
else if (key.backspace || key.delete)
|
|
948
|
+
setValue(v => v.slice(0, -1));
|
|
949
|
+
else if (input && !key.ctrl && !key.meta)
|
|
950
|
+
setValue(v => v + input);
|
|
951
|
+
});
|
|
952
|
+
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') })] }));
|
|
953
|
+
}
|