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