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