sitevision-cli 1.0.0-beta.15 → 1.0.0-beta.17
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/components/AnimatedLogo.js +8 -2
- package/dist/shell/ConfigForm.d.ts +1 -1
- package/dist/shell/ConfigForm.js +37 -16
- package/dist/shell/Frame.d.ts +8 -1
- package/dist/shell/Frame.js +30 -6
- package/dist/shell/Shell.js +115 -61
- package/dist/utils/i18n.js +11 -3
- package/dist/utils/project-detection.js +7 -4
- package/dist/utils/session-cookie-auth.js +1 -1
- package/dist/utils/workspace.d.ts +9 -0
- package/dist/utils/workspace.js +21 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { AUTHOR, BIG_LOGO } from '../utils/branding.js';
|
|
5
5
|
const FRAME_MS = 45;
|
|
6
6
|
const SWEEP_COLS_PER_FRAME = 7; // how fast the wipe edge moves left → right
|
|
@@ -83,5 +83,11 @@ export function AnimatedLogo({ art = BIG_LOGO, onDone }) {
|
|
|
83
83
|
}
|
|
84
84
|
}, [frame, total, onDone]);
|
|
85
85
|
const edge = (frame + 1) * SWEEP_COLS_PER_FRAME;
|
|
86
|
-
|
|
86
|
+
// Centre the wordmark on the alternate screen. `rows - 1` leaves room for
|
|
87
|
+
// Ink's trailing newline; a terminal too short for the art just renders it
|
|
88
|
+
// top-aligned rather than scrolling.
|
|
89
|
+
const { stdout } = useStdout();
|
|
90
|
+
const height = (stdout.rows || 24) - 1;
|
|
91
|
+
const fits = height >= art.length + 2;
|
|
92
|
+
return (_jsx(Box, { width: stdout.columns || 80, height: fits ? height : undefined, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", padding: 1, children: [art.map((line, y) => (_jsx(Text, { children: buildSpans(line, y, frame, edge).map((span, index) => (_jsx(Fragment, { children: span.color ? (_jsx(Text, { color: span.color, children: span.text })) : (_jsx(Text, { children: span.text })) }, index))) }, y))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ' a tool by ' }), _jsx(Text, { bold: true, children: AUTHOR })] })] }) }));
|
|
87
93
|
}
|
package/dist/shell/ConfigForm.js
CHANGED
|
@@ -11,8 +11,8 @@ const METHODS = ['basic', 'oauth2', 'cookie'];
|
|
|
11
11
|
const FIELDS = [
|
|
12
12
|
{
|
|
13
13
|
key: 'domain',
|
|
14
|
-
help:
|
|
15
|
-
label: '
|
|
14
|
+
help: "Domain of this environment's site (USE or TSE) without https://, e.g. myorg-use.sitevision-cloud.se. Deploys and version lookups go here.",
|
|
15
|
+
label: 'Domain',
|
|
16
16
|
required: true,
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -31,9 +31,9 @@ const FIELDS = [
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
key: 'username',
|
|
34
|
-
help: 'Sitevision account used for deploys, usually your Sitevision Cloud e-mail. It needs DEVELOPER or MANAGE_ADDONS permission on the site.',
|
|
34
|
+
help: 'Sitevision account used for deploys, usually your Sitevision Cloud e-mail. It needs DEVELOPER or MANAGE_ADDONS permission on the site. Required for basic auth; with oauth2 or cookie it only labels the stored credential.',
|
|
35
35
|
label: 'Username',
|
|
36
|
-
required:
|
|
36
|
+
required: 'basic',
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
key: 'authMethod',
|
|
@@ -274,6 +274,7 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
274
274
|
const [cursor, setCursor] = useState(0);
|
|
275
275
|
const [editing, setEditing] = useState(false);
|
|
276
276
|
const [draft, setDraft] = useState('');
|
|
277
|
+
const [caret, setCaret] = useState(0);
|
|
277
278
|
const [note, setNote] = useState('');
|
|
278
279
|
// Values always mirror the project; edits are committed field by field.
|
|
279
280
|
useEffect(() => {
|
|
@@ -369,11 +370,21 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
369
370
|
else if (key.ctrl && input === 'o' && current.key === 'addonName') {
|
|
370
371
|
openPicker();
|
|
371
372
|
}
|
|
373
|
+
else if (key.leftArrow) {
|
|
374
|
+
setCaret(Math.max(0, caret - 1));
|
|
375
|
+
}
|
|
376
|
+
else if (key.rightArrow) {
|
|
377
|
+
setCaret(Math.min(draft.length, caret + 1));
|
|
378
|
+
}
|
|
372
379
|
else if (key.backspace || key.delete) {
|
|
373
|
-
|
|
380
|
+
if (caret > 0) {
|
|
381
|
+
setDraft(draft.slice(0, caret - 1) + draft.slice(caret));
|
|
382
|
+
setCaret(caret - 1);
|
|
383
|
+
}
|
|
374
384
|
}
|
|
375
385
|
else if (input && !key.ctrl && !key.meta) {
|
|
376
|
-
setDraft(
|
|
386
|
+
setDraft(draft.slice(0, caret) + input + draft.slice(caret));
|
|
387
|
+
setCaret(caret + input.length);
|
|
377
388
|
}
|
|
378
389
|
return;
|
|
379
390
|
}
|
|
@@ -387,12 +398,15 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
387
398
|
openPicker();
|
|
388
399
|
}
|
|
389
400
|
else if (key.return) {
|
|
390
|
-
|
|
401
|
+
const start = current.kind === 'secret' ? '' : (values[current.key] ?? '');
|
|
402
|
+
setDraft(start);
|
|
403
|
+
setCaret(start.length);
|
|
391
404
|
setEditing(true);
|
|
392
405
|
}
|
|
393
406
|
}, { isActive: active });
|
|
394
407
|
const source = (f) => {
|
|
395
|
-
|
|
408
|
+
const required = f.required === true || f.required === method;
|
|
409
|
+
if (required && !(values[f.key] ?? ''))
|
|
396
410
|
return { text: t('✗ required'), color: 'red' };
|
|
397
411
|
if (f.kind === 'secret') {
|
|
398
412
|
return { text: storedSecret(project, f.key) ? t('keychain') : '—' };
|
|
@@ -438,10 +452,17 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
438
452
|
}
|
|
439
453
|
const focused = active && f === current;
|
|
440
454
|
const typing = focused && editing;
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
455
|
+
// A value being typed into, scrolled so the caret stays in view and drawn
|
|
456
|
+
// with the caret as an inverted cell.
|
|
457
|
+
const withCaret = (text) => {
|
|
458
|
+
const max = Math.max(1, valueWidth - 1);
|
|
459
|
+
const start = text.length > max
|
|
460
|
+
? Math.min(Math.max(0, caret - max + 1), text.length - max)
|
|
461
|
+
: 0;
|
|
462
|
+
const shown = text.slice(start, start + max);
|
|
463
|
+
const at = caret - start;
|
|
464
|
+
return (_jsxs(Text, { children: [shown.slice(0, at), _jsx(Text, { inverse: true, children: shown[at] ?? ' ' }), shown.slice(at + 1)] }));
|
|
465
|
+
};
|
|
445
466
|
let display;
|
|
446
467
|
if (f.kind === 'method' || f.kind === 'bool') {
|
|
447
468
|
const choices = options(f);
|
|
@@ -449,18 +470,18 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
449
470
|
display = choices.map((m, i) => (_jsxs(Text, { children: [_jsx(Text, { bold: m === chosen, color: m === chosen ? ACCENT : undefined, inverse: typing && m === chosen, dimColor: m !== chosen, children: f.kind === 'bool' ? t(m) : m }), i < choices.length - 1 && _jsx(Text, { dimColor: true, children: " \u00B7 " })] }, m)));
|
|
450
471
|
}
|
|
451
472
|
else if (f.kind === 'secret') {
|
|
452
|
-
display = typing ? (
|
|
473
|
+
display = typing ? (withCaret('•'.repeat(draft.length))) : (_jsx(Text, { dimColor: true, children: storedSecret(project, f.key)
|
|
453
474
|
? t('•••••••• keychain')
|
|
454
475
|
: f.hint
|
|
455
476
|
? t(f.hint)
|
|
456
477
|
: '' }));
|
|
457
478
|
}
|
|
458
479
|
else {
|
|
459
|
-
const value =
|
|
460
|
-
display = value ? (_jsx(Text, { children:
|
|
480
|
+
const value = values[f.key];
|
|
481
|
+
display = typing ? (withCaret(draft)) : value ? (_jsx(Text, { children: value })) : (_jsx(Text, { dimColor: true, children: f.hint ? t(f.hint) : '—' }));
|
|
461
482
|
}
|
|
462
483
|
const src = source(f);
|
|
463
|
-
rows.push(_jsxs(Box, { height: 1, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(f.label).padEnd(22) }), _jsx(Box, { width: valueWidth, flexShrink: 0, children:
|
|
484
|
+
rows.push(_jsxs(Box, { height: 1, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(f.label).padEnd(22) }), _jsx(Box, { width: valueWidth, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", children: display }) }), _jsx(Box, { width: SOURCE_WIDTH, flexShrink: 0, children: _jsx(Text, { dimColor: !src.color, color: src.color, wrap: "truncate", children: focused && f.hint && f.key === 'addonName' ? t(f.hint) : src.text }) })] }, f.key));
|
|
464
485
|
}
|
|
465
486
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", height: height, children: [_jsxs(Text, { dimColor: true, children: [(' ' + t('FIELD')).padEnd(24), t('VALUE').padEnd(valueWidth), t('SOURCE')] }), rows, project.workspace && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t("Shared by every app below {root}. An app's own value wins.", {
|
|
466
487
|
root: project.root,
|
package/dist/shell/Frame.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export declare function appStatus(project: ProjectInfo): {
|
|
|
28
28
|
signing: boolean;
|
|
29
29
|
scriptsWarning: string | undefined;
|
|
30
30
|
};
|
|
31
|
+
/** The name shown for an app in the navigator, and what the filter matches. */
|
|
32
|
+
export declare function appLabel(app: ProjectInfo): string;
|
|
33
|
+
/** Indices into `apps` whose label fuzzy-matches the filter. */
|
|
34
|
+
export declare function navMatches(apps: ProjectInfo[], filter: string): number[];
|
|
35
|
+
/** Next selectable index when moving by `delta`, wrapping at both ends. */
|
|
36
|
+
export declare function navMove(ring: number[], selected: number, delta: number): number;
|
|
31
37
|
export interface NavigatorProps {
|
|
32
38
|
apps: ProjectInfo[];
|
|
33
39
|
groupOf: (app: ProjectInfo) => string;
|
|
@@ -38,8 +44,9 @@ export interface NavigatorProps {
|
|
|
38
44
|
single: boolean;
|
|
39
45
|
settingsSelected?: boolean;
|
|
40
46
|
width: number;
|
|
47
|
+
filter?: string;
|
|
41
48
|
}
|
|
42
|
-
export declare function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected, width, }: NavigatorProps): import("react").JSX.Element;
|
|
49
|
+
export declare function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected, width, filter, }: NavigatorProps): import("react").JSX.Element;
|
|
43
50
|
export declare function NavigatorStrip({ apps, selected, focused, }: Pick<NavigatorProps, 'apps' | 'selected' | 'focused'>): import("react").JSX.Element;
|
|
44
51
|
export declare function elapsed(task: Task): string;
|
|
45
52
|
export interface Hint {
|
package/dist/shell/Frame.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { appTypeOf, getPackageJsonSyncChanges, localizedText, } from '../utils/project-detection.js';
|
|
5
|
+
import { fuzzyMatch } from './actions.js';
|
|
5
6
|
import { t } from '../utils/i18n.js';
|
|
6
7
|
export const ACCENT = 'cyan';
|
|
7
8
|
export const NARROW_BELOW = 100;
|
|
@@ -40,7 +41,24 @@ function Dots({ project }) {
|
|
|
40
41
|
const dot = (ok, warn = false) => (_jsx(Text, { color: ok ? 'green' : warn ? 'yellow' : 'red', children: "\u25CF" }));
|
|
41
42
|
return (_jsxs(Text, { children: [dot(s.deps), dot(s.config), dot(s.sync === 0, s.sync > 0), dot(s.signing, !s.signing)] }));
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
/** The name shown for an app in the navigator, and what the filter matches. */
|
|
45
|
+
export function appLabel(app) {
|
|
46
|
+
return localizedText(app.manifest.name) || app.manifest.id;
|
|
47
|
+
}
|
|
48
|
+
/** Indices into `apps` whose label fuzzy-matches the filter. */
|
|
49
|
+
export function navMatches(apps, filter) {
|
|
50
|
+
return apps
|
|
51
|
+
.map((_, index) => index)
|
|
52
|
+
.filter(index => fuzzyMatch(filter, appLabel(apps[index])));
|
|
53
|
+
}
|
|
54
|
+
/** Next selectable index when moving by `delta`, wrapping at both ends. */
|
|
55
|
+
export function navMove(ring, selected, delta) {
|
|
56
|
+
if (ring.length === 0)
|
|
57
|
+
return selected;
|
|
58
|
+
const at = ring.indexOf(selected);
|
|
59
|
+
return ring[at === -1 ? 0 : (at + delta + ring.length) % ring.length];
|
|
60
|
+
}
|
|
61
|
+
export function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected = false, width, filter = '', }) {
|
|
44
62
|
// Row: marker(1) glyph(3) sp name sp version(6) sp dots(4) inside the padding.
|
|
45
63
|
const nameWidth = width - 2 - 17;
|
|
46
64
|
const running = tasks.filter(task => task.status === 'running');
|
|
@@ -51,16 +69,20 @@ export function Navigator({ apps, groupOf, selected, focused, tasks, height, sin
|
|
|
51
69
|
for (const [index, app] of apps.entries()) {
|
|
52
70
|
const group = groupOf(app);
|
|
53
71
|
if (!single && group !== lastGroup) {
|
|
54
|
-
rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', group] }) }, `g-${group}`));
|
|
72
|
+
rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', group] }) }, `g-${index}-${group}`));
|
|
55
73
|
rowApp.push(-1);
|
|
56
74
|
lastGroup = group;
|
|
57
75
|
}
|
|
58
76
|
rowApp.push(index);
|
|
59
77
|
const active = index === selected;
|
|
60
78
|
const busy = running.some(task => task.appRoot === app.root);
|
|
61
|
-
const name =
|
|
79
|
+
const name = appLabel(app);
|
|
62
80
|
rows.push(_jsxs(Box, { width: width - 2, height: 1, flexShrink: 0, children: [_jsxs(Text, { backgroundColor: active && focused ? ACCENT : undefined, color: active && focused ? 'black' : undefined, bold: active, wrap: "truncate", children: [active ? '▎' : ' ', _jsx(Text, { dimColor: !active, children: typeGlyph(app.manifest) }), ' ', name.padEnd(nameWidth).slice(0, nameWidth), ' ', _jsx(Text, { dimColor: true, children: app.manifest.version.padStart(6).slice(0, 6) }), ' '] }), busy ? (_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) })) : (_jsx(Dots, { project: app }))] }, app.root));
|
|
63
81
|
}
|
|
82
|
+
if (rows.length === 0) {
|
|
83
|
+
rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: ' ' + t('no matches') }) }, "none"));
|
|
84
|
+
rowApp.push(-1);
|
|
85
|
+
}
|
|
64
86
|
// Window the list so the selected app stays visible; the lines outside
|
|
65
87
|
// are summarised as "… n more".
|
|
66
88
|
const fixed = 1 + 1 + (single ? 0 : 2) + (running.length > 0 ? running.length + 2 : 0);
|
|
@@ -79,14 +101,16 @@ export function Navigator({ apps, groupOf, selected, focused, tasks, height, sin
|
|
|
79
101
|
if (end < rows.length)
|
|
80
102
|
shown[shown.length - 1] = more(rows.length - end, '↓');
|
|
81
103
|
}
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderDimColor: true, borderTop: false, borderBottom: false, borderLeft: false, paddingX: 1, overflow: "hidden", children: [_jsx(Text, {
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderDimColor: true, borderTop: false, borderBottom: false, borderLeft: false, paddingX: 1, overflow: "hidden", children: [filter ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), filter, _jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { dimColor: true, children: [' ', apps.length === 1
|
|
105
|
+
? t('1 match')
|
|
106
|
+
: t('{n} matches', { n: apps.length })] })] })) : (_jsx(Text, { bold: true, dimColor: true, children: single
|
|
83
107
|
? t('APP')
|
|
84
108
|
: apps.length === 1
|
|
85
109
|
? t('WORKSPACE 1 app')
|
|
86
|
-
: t('WORKSPACE {n} apps', { n: apps.length }) }), shown, _jsx(Text, { dimColor: true, children: ' ' + t('deps·config·sync·signing') }), !single && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: settingsSelected && focused ? ACCENT : undefined, color: settingsSelected && focused ? 'black' : undefined, bold: settingsSelected, children: [settingsSelected ? '▎' : ' ', "\u2699 ", t('Workspace settings')] }) })), running.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('TASKS') }), running.map(task => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', task.label, " ", task.appName, " ", _jsx(Text, { dimColor: true, children: elapsed(task) })] }, task.id)))] }))] }));
|
|
110
|
+
: t('WORKSPACE {n} apps', { n: apps.length }) })), shown, _jsx(Text, { dimColor: true, children: ' ' + t('deps·config·sync·signing') }), !single && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: settingsSelected && focused ? ACCENT : undefined, color: settingsSelected && focused ? 'black' : undefined, bold: settingsSelected, children: [settingsSelected ? '▎' : ' ', "\u2699 ", t('Workspace settings')] }) })), running.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('TASKS') }), running.map(task => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', task.label, " ", task.appName, " ", _jsx(Text, { dimColor: true, children: elapsed(task) })] }, task.id)))] }))] }));
|
|
87
111
|
}
|
|
88
112
|
export function NavigatorStrip({ apps, selected, focused, }) {
|
|
89
|
-
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { wrap: "truncate", children: apps.map((app, index) => (_jsxs(Text, { backgroundColor: index === selected && focused ? ACCENT : undefined, color: index === selected && focused ? 'black' : undefined, bold: index === selected, children: [' ', typeGlyph(app.manifest),
|
|
113
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { wrap: "truncate", children: apps.map((app, index) => (_jsxs(Text, { backgroundColor: index === selected && focused ? ACCENT : undefined, color: index === selected && focused ? 'black' : undefined, bold: index === selected, children: [' ', typeGlyph(app.manifest), " ", appLabel(app), ' '] }, app.root))) }) }));
|
|
90
114
|
}
|
|
91
115
|
export function elapsed(task) {
|
|
92
116
|
const ms = (task.endedAt ?? Date.now()) - task.startedAt;
|
package/dist/shell/Shell.js
CHANGED
|
@@ -4,12 +4,12 @@ import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
|
|
4
4
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
5
|
import Spinner from 'ink-spinner';
|
|
6
6
|
import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, } from '../utils/project-detection.js';
|
|
7
|
-
import { appGroup } from '../utils/workspace.js';
|
|
7
|
+
import { appGroup, configIncomplete, needsOnboarding, } from '../utils/workspace.js';
|
|
8
8
|
import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
|
|
9
9
|
import { useTasks, runningTasks, startActivate, getTasks, } from '../utils/tasks.js';
|
|
10
10
|
import { PasswordInput } from '../components/PasswordInput.js';
|
|
11
11
|
import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
|
|
12
|
-
import { TopBar, Navigator, NavigatorStrip, BottomBar, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
|
|
12
|
+
import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
|
|
13
13
|
import { TabBar, TABS, Overview, Versions, Log, } from './Tabs.js';
|
|
14
14
|
import { CommandPalette } from './CommandPalette.js';
|
|
15
15
|
import { ConfigForm } from './ConfigForm.js';
|
|
@@ -37,10 +37,13 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
37
37
|
const { columns, rows } = useSize();
|
|
38
38
|
const tasks = useTasks();
|
|
39
39
|
const [apps, setApps] = useState(initialApps);
|
|
40
|
-
|
|
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);
|
|
41
43
|
const [tab, setTab] = useState('overview');
|
|
42
|
-
const [focus, setFocus] = useState(workspaceRoot ? 'nav' : 'content');
|
|
44
|
+
const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
|
|
43
45
|
const [overlay, setOverlay] = useState(null);
|
|
46
|
+
const [filter, setFilter] = useState('');
|
|
44
47
|
const [versions, setVersions] = useState({});
|
|
45
48
|
const [versionRow, setVersionRow] = useState(0);
|
|
46
49
|
const [logScroll, setLogScroll] = useState(0);
|
|
@@ -76,6 +79,10 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
76
79
|
[workspaceRoot, apps, env]);
|
|
77
80
|
const narrow = columns < NARROW_BELOW;
|
|
78
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];
|
|
79
86
|
const running = runningTasks();
|
|
80
87
|
// Re-render once a second while something runs so elapsed times move.
|
|
81
88
|
useEffect(() => {
|
|
@@ -252,6 +259,13 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
252
259
|
? raw.toUpperCase()
|
|
253
260
|
: raw;
|
|
254
261
|
if (key.escape) {
|
|
262
|
+
if (focus === 'nav' && filter) {
|
|
263
|
+
setFilter('');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Esc backs out of the workspace settings pane, not just its focus.
|
|
267
|
+
if (settings)
|
|
268
|
+
setSelected(0);
|
|
255
269
|
setFocus(single ? 'content' : 'nav');
|
|
256
270
|
return;
|
|
257
271
|
}
|
|
@@ -260,9 +274,40 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
260
274
|
return;
|
|
261
275
|
}
|
|
262
276
|
if (key.tab) {
|
|
277
|
+
setFilter('');
|
|
263
278
|
setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
|
|
264
279
|
return;
|
|
265
280
|
}
|
|
281
|
+
// Navigator: typing searches, so no action key fires until Enter has
|
|
282
|
+
// moved the focus into the content pane.
|
|
283
|
+
if (focus === 'nav') {
|
|
284
|
+
const move = (delta) => {
|
|
285
|
+
setSelected(navMove(ring, selected, delta));
|
|
286
|
+
};
|
|
287
|
+
const search = (next) => {
|
|
288
|
+
setFilter(next);
|
|
289
|
+
const found = navMatches(apps, next);
|
|
290
|
+
if (found.length > 0 && !found.includes(selected))
|
|
291
|
+
setSelected(found[0]);
|
|
292
|
+
};
|
|
293
|
+
if (key.upArrow)
|
|
294
|
+
move(-1);
|
|
295
|
+
else if (key.downArrow)
|
|
296
|
+
move(1);
|
|
297
|
+
else if (key.return) {
|
|
298
|
+
if (matches.length > 0 || settings) {
|
|
299
|
+
setFilter('');
|
|
300
|
+
setFocus('content');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (key.backspace || key.delete)
|
|
304
|
+
search(filter.slice(0, -1));
|
|
305
|
+
else if (input === 'q' && !filter)
|
|
306
|
+
quit();
|
|
307
|
+
else if (input?.length === 1 && input >= ' ' && !key.ctrl && !key.meta)
|
|
308
|
+
search(filter + input);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
266
311
|
if (input === 'a' && tab !== 'versions') {
|
|
267
312
|
setTab('versions');
|
|
268
313
|
setFocus('content');
|
|
@@ -287,16 +332,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
287
332
|
quit();
|
|
288
333
|
return;
|
|
289
334
|
}
|
|
290
|
-
if (
|
|
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') {
|
|
335
|
+
if (tab === 'versions') {
|
|
300
336
|
const count = versions[versionsKey]?.executables?.length ?? 0;
|
|
301
337
|
if (key.upArrow)
|
|
302
338
|
setVersionRow(r => Math.max(0, r - 1));
|
|
@@ -331,7 +367,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
331
367
|
const mainHeight = frameRows - 2;
|
|
332
368
|
const contentHeight = mainHeight - 1 - (narrow ? 1 : 0);
|
|
333
369
|
const groupOf = (app) => workspaceRoot ? appGroup(workspaceRoot, app.root) : '.';
|
|
334
|
-
const appName =
|
|
370
|
+
const appName = appLabel(project);
|
|
335
371
|
const tabName = t(TABS.find(entry => entry.id === tab).label).toLowerCase();
|
|
336
372
|
const contextLabel = settings
|
|
337
373
|
? `${t('workspace')} ▸ ${t('settings')}`
|
|
@@ -356,61 +392,77 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
356
392
|
['↑↓', 'apps'],
|
|
357
393
|
['q', 'quit'],
|
|
358
394
|
]);
|
|
395
|
+
const navHints = filter
|
|
396
|
+
? h([
|
|
397
|
+
['↑↓', 'move'],
|
|
398
|
+
['Enter', 'select'],
|
|
399
|
+
['Esc', 'clear'],
|
|
400
|
+
['/', 'commands'],
|
|
401
|
+
])
|
|
402
|
+
: h([
|
|
403
|
+
['a–z', 'search'],
|
|
404
|
+
['↑↓', 'move'],
|
|
405
|
+
['Enter', 'select'],
|
|
406
|
+
['/', 'commands'],
|
|
407
|
+
['q', 'quit'],
|
|
408
|
+
]);
|
|
359
409
|
const hints = overlay
|
|
360
410
|
? h([['Esc', 'cancel']])
|
|
361
|
-
: settings
|
|
362
|
-
?
|
|
363
|
-
:
|
|
364
|
-
?
|
|
365
|
-
|
|
366
|
-
['R', 'refresh'],
|
|
367
|
-
['p', 'deploy'],
|
|
368
|
-
['/', 'commands'],
|
|
369
|
-
['q', 'quit'],
|
|
370
|
-
])
|
|
371
|
-
: tab === 'log'
|
|
411
|
+
: focus === 'nav' && !settings
|
|
412
|
+
? navHints
|
|
413
|
+
: settings
|
|
414
|
+
? settingsHints
|
|
415
|
+
: tab === 'versions'
|
|
372
416
|
? h([
|
|
373
|
-
['
|
|
374
|
-
['
|
|
375
|
-
['K', 'stop'],
|
|
417
|
+
['a', 'activate'],
|
|
418
|
+
['R', 'refresh'],
|
|
376
419
|
['p', 'deploy'],
|
|
377
420
|
['/', 'commands'],
|
|
378
421
|
['q', 'quit'],
|
|
379
422
|
])
|
|
380
|
-
: tab === '
|
|
381
|
-
?
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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'],
|
|
423
|
+
: tab === 'log'
|
|
424
|
+
? h([
|
|
425
|
+
['f', 'follow'],
|
|
426
|
+
['x', 'wrap'],
|
|
427
|
+
['K', 'stop'],
|
|
407
428
|
['p', 'deploy'],
|
|
408
|
-
['a', 'activate'],
|
|
409
|
-
['E', 'env'],
|
|
410
|
-
['i', 'install'],
|
|
411
429
|
['/', 'commands'],
|
|
412
430
|
['q', 'quit'],
|
|
413
|
-
])
|
|
431
|
+
])
|
|
432
|
+
: tab === 'config'
|
|
433
|
+
? editing
|
|
434
|
+
? h([
|
|
435
|
+
['Enter', 'save'],
|
|
436
|
+
['Esc', 'cancel'],
|
|
437
|
+
])
|
|
438
|
+
: formActive
|
|
439
|
+
? h([
|
|
440
|
+
['↑↓', 'field'],
|
|
441
|
+
['Enter', 'edit'],
|
|
442
|
+
['^O', 'pick addon'],
|
|
443
|
+
['y', 'sync'],
|
|
444
|
+
['/', 'commands'],
|
|
445
|
+
['q', 'quit'],
|
|
446
|
+
])
|
|
447
|
+
: h([
|
|
448
|
+
['Tab', 'edit'],
|
|
449
|
+
['y', 'sync'],
|
|
450
|
+
['l', 'login'],
|
|
451
|
+
['/', 'commands'],
|
|
452
|
+
['q', 'quit'],
|
|
453
|
+
])
|
|
454
|
+
: h([
|
|
455
|
+
['d', 'dev'],
|
|
456
|
+
['w', 'watch'],
|
|
457
|
+
['b', 'build'],
|
|
458
|
+
['s', 'sign'],
|
|
459
|
+
['p', 'deploy'],
|
|
460
|
+
['a', 'activate'],
|
|
461
|
+
['E', 'env'],
|
|
462
|
+
['i', 'install'],
|
|
463
|
+
['/', 'commands'],
|
|
464
|
+
['q', 'quit'],
|
|
465
|
+
]);
|
|
414
466
|
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
467
|
const closeOverlay = () => setOverlay(null);
|
|
416
468
|
const content = overlay ? (renderOverlay(overlay, {
|
|
@@ -443,7 +495,9 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
443
495
|
return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
|
|
444
496
|
name: env,
|
|
445
497
|
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:
|
|
498
|
+
}, 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' })), 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)
|
|
499
|
+
? t(' · new workspace: fill in once, every app inherits · Esc skips')
|
|
500
|
+
: 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
501
|
}
|
|
448
502
|
function renderOverlay(overlay, env) {
|
|
449
503
|
const { project, closeOverlay, run, notify, loadAddons, height, rerender, openWorkspace, } = env;
|
package/dist/utils/i18n.js
CHANGED
|
@@ -147,7 +147,7 @@ const sv = {
|
|
|
147
147
|
'logged in': 'inloggad',
|
|
148
148
|
'credentials removed': 'uppgifter borttagna',
|
|
149
149
|
// Config form
|
|
150
|
-
|
|
150
|
+
Domain: 'Domän',
|
|
151
151
|
'Site name': 'Webbplatsnamn',
|
|
152
152
|
'Addon name': 'Tilläggsnamn',
|
|
153
153
|
Username: 'Användarnamn',
|
|
@@ -203,6 +203,13 @@ const sv = {
|
|
|
203
203
|
edit: 'redigera',
|
|
204
204
|
back: 'tillbaka',
|
|
205
205
|
quit: 'avsluta',
|
|
206
|
+
search: 'sök',
|
|
207
|
+
move: 'flytta',
|
|
208
|
+
select: 'välj',
|
|
209
|
+
clear: 'rensa',
|
|
210
|
+
'no matches': 'inga träffar',
|
|
211
|
+
'{n} matches': '{n} träffar',
|
|
212
|
+
'1 match': '1 träff',
|
|
206
213
|
'edit settings': 'redigera inställningar',
|
|
207
214
|
apps: 'appar',
|
|
208
215
|
activate: 'aktivera',
|
|
@@ -224,12 +231,13 @@ const sv = {
|
|
|
224
231
|
'workspace config saved': 'arbetsytans konfig sparad',
|
|
225
232
|
'config saved': 'konfig sparad',
|
|
226
233
|
' · shared .dev_properties.json at the root': ' · delad .dev_properties.json i roten',
|
|
234
|
+
' · new workspace: fill in once, every app inherits · Esc skips': ' · ny arbetsyta: fyll i en gång, alla appar ärver · Esc hoppar över',
|
|
227
235
|
'y confirm · n cancel': 'y bekräfta · n avbryt',
|
|
228
236
|
// Config help
|
|
229
|
-
|
|
237
|
+
"Domain of this environment's site (USE or TSE) without https://, e.g. myorg-use.sitevision-cloud.se. Deploys and version lookups go here.": 'Domän för den här miljöns webbplats (USE eller TSE) utan https://, t.ex. myorg-use.sitevision-cloud.se. Driftsättningar och versionslistor går hit.',
|
|
230
238
|
"Name of the site's root node in Sitevision, exactly as shown in the site tree. It becomes part of the REST API path.": 'Namnet på webbplatsens rotnod i Sitevision, exakt som i webbplatsträdet. Det blir en del av REST API-sökvägen.',
|
|
231
239
|
"Name of the addon (custom module) in the site's Addon Repository that this app is uploaded into. Ctrl+O lists the existing ones.": 'Namnet på tillägget (custom module) i webbplatsens tilläggsförråd som appen laddas upp till. Ctrl+O listar befintliga.',
|
|
232
|
-
'Sitevision account used for deploys, usually your Sitevision Cloud e-mail. It needs DEVELOPER or MANAGE_ADDONS permission on the site.': 'Sitevision-konto som används för driftsättning, oftast din Sitevision Cloud-e-post. Behöver DEVELOPER eller MANAGE_ADDONS på webbplatsen.',
|
|
240
|
+
'Sitevision account used for deploys, usually your Sitevision Cloud e-mail. It needs DEVELOPER or MANAGE_ADDONS permission on the site. Required for basic auth; with oauth2 or cookie it only labels the stored credential.': 'Sitevision-konto som används för driftsättning, oftast din Sitevision Cloud-e-post. Behöver DEVELOPER eller MANAGE_ADDONS på webbplatsen. Krävs för basic; med oauth2 eller cookie används det bara för att märka den sparade inloggningen.',
|
|
233
241
|
"How deploys authenticate: basic = username and password; oauth2 = bearer token from the site's OAuth2 provider (PKCE, opens a browser); cookie = reuse a browser SSO/SAML session.": 'Hur driftsättningar autentiseras: basic = användarnamn och lösenord; oauth2 = bearer-token från webbplatsens OAuth2-provider (PKCE, öppnar webbläsare); cookie = återanvänd en SSO/SAML-session från webbläsaren.',
|
|
234
242
|
'Deploy password for the account above. Stored in the OS keychain, never in a file. Leave empty to be asked on each run.': 'Driftsättningslösenord för kontot ovan. Sparas i nyckelringen, aldrig i en fil. Lämna tomt för att bli tillfrågad varje gång.',
|
|
235
243
|
'Client id of the OAuth2 client registered on the site. Its redirect URI must be http://127.0.0.1:8137/callback.': 'Klient-id för OAuth2-klienten som är registrerad på webbplatsen. Dess redirect-URI måste vara http://127.0.0.1:8137/callback.',
|
|
@@ -343,7 +343,7 @@ export function resolveRuntimeSecrets(dev) {
|
|
|
343
343
|
if (dev.authMethod === 'oauth2') {
|
|
344
344
|
dev.accessToken = process.env['SITEVISION_ACCESS_TOKEN'] ?? undefined;
|
|
345
345
|
}
|
|
346
|
-
if (dev.authMethod === 'cookie' && dev.domain
|
|
346
|
+
if (dev.authMethod === 'cookie' && dev.domain) {
|
|
347
347
|
dev.sessionCookie =
|
|
348
348
|
process.env['SITEVISION_SESSION_COOKIE'] ??
|
|
349
349
|
getSessionCookie(dev.domain, dev.username) ??
|
|
@@ -419,10 +419,13 @@ export function writeDevProperties(projectRoot, properties) {
|
|
|
419
419
|
getDefaultDevPropertiesPath(projectRoot);
|
|
420
420
|
const { password: _password, accessToken: _accessToken, sessionCookie: _sessionCookie, environmentName: _environmentName, productionEnvironment: _productionEnvironment, ...persisted } = properties;
|
|
421
421
|
// Keep the app file minimal: values identical to the inherited ones stay
|
|
422
|
-
// at the workspace root instead of being copied into every app.
|
|
422
|
+
// at the workspace root instead of being copied into every app. An empty
|
|
423
|
+
// string means "unset", so it is dropped rather than written as an override
|
|
424
|
+
// that would shadow the inherited value.
|
|
423
425
|
const inherited = readInheritedDevProperties(projectRoot);
|
|
424
|
-
const own = Object.fromEntries(Object.entries(persisted).filter(([key, value]) =>
|
|
425
|
-
|
|
426
|
+
const own = Object.fromEntries(Object.entries(persisted).filter(([key, value]) => value !== '' &&
|
|
427
|
+
(!Object.hasOwn(inherited, key) ||
|
|
428
|
+
JSON.stringify(inherited[key]) !== JSON.stringify(value))));
|
|
426
429
|
fs.writeFileSync(devPropertiesPath, JSON.stringify(own, null, 2));
|
|
427
430
|
}
|
|
428
431
|
export function readSvcConfig(projectRoot) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ProjectInfo } from './project-detection.js';
|
|
2
|
+
import type { DevProperties } from '../types/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Find every Sitevision app below `root` (e.g. root/webapps/x, root/restapps/y).
|
|
4
5
|
* Depth-limited walk that skips dependency and output folders.
|
|
@@ -6,3 +7,11 @@ import { type ProjectInfo } from './project-detection.js';
|
|
|
6
7
|
export declare function discoverApps(root: string): ProjectInfo[];
|
|
7
8
|
/** Group label for an app: its parent folder relative to the workspace root. */
|
|
8
9
|
export declare function appGroup(root: string, appRoot: string): string;
|
|
10
|
+
/** Missing something nothing can deploy without. */
|
|
11
|
+
export declare function configIncomplete(dev?: Partial<DevProperties>): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* True when a workspace has apps but nothing usable to deploy with: neither the
|
|
14
|
+
* shared root config nor the apps themselves carry domain/site/username. The
|
|
15
|
+
* shell then opens on Workspace settings instead of the first app.
|
|
16
|
+
*/
|
|
17
|
+
export declare function needsOnboarding(root: string, apps: ProjectInfo[]): boolean;
|
package/dist/utils/workspace.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { detectProject } from './project-detection.js';
|
|
3
|
+
import { detectProject, readWorkspaceDevProperties, } from './project-detection.js';
|
|
4
4
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
5
5
|
const MAX_DEPTH = 3;
|
|
6
6
|
/**
|
|
@@ -39,10 +39,29 @@ export function discoverApps(root) {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
walk(root, 1);
|
|
42
|
-
|
|
42
|
+
// Group first, then path: a deeper folder (webapps/nested) must not split
|
|
43
|
+
// its parent's run of apps, or the same group heading renders twice.
|
|
44
|
+
return found.toSorted((a, b) => path.dirname(a.root).localeCompare(path.dirname(b.root)) ||
|
|
45
|
+
a.root.localeCompare(b.root));
|
|
43
46
|
}
|
|
44
47
|
/** Group label for an app: its parent folder relative to the workspace root. */
|
|
45
48
|
export function appGroup(root, appRoot) {
|
|
46
49
|
const relative = path.relative(root, path.dirname(appRoot));
|
|
47
50
|
return relative === '' ? '.' : relative;
|
|
48
51
|
}
|
|
52
|
+
/** Missing something nothing can deploy without. */
|
|
53
|
+
export function configIncomplete(dev) {
|
|
54
|
+
if (!dev?.domain || !dev.siteName)
|
|
55
|
+
return true;
|
|
56
|
+
return (dev.authMethod ?? 'basic') === 'basic' && !dev.username;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* True when a workspace has apps but nothing usable to deploy with: neither the
|
|
60
|
+
* shared root config nor the apps themselves carry domain/site/username. The
|
|
61
|
+
* shell then opens on Workspace settings instead of the first app.
|
|
62
|
+
*/
|
|
63
|
+
export function needsOnboarding(root, apps) {
|
|
64
|
+
return (apps.length > 0 &&
|
|
65
|
+
configIncomplete(readWorkspaceDevProperties(root)) &&
|
|
66
|
+
apps.some(app => configIncomplete(app.devProperties)));
|
|
67
|
+
}
|