sitevision-cli 1.0.0-beta.15 → 1.0.0-beta.16
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/Shell.js +11 -4
- package/dist/utils/i18n.js +4 -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 +17 -1
- 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/Shell.js
CHANGED
|
@@ -4,7 +4,7 @@ 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';
|
|
@@ -37,9 +37,11 @@ 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);
|
|
44
46
|
const [versions, setVersions] = useState({});
|
|
45
47
|
const [versionRow, setVersionRow] = useState(0);
|
|
@@ -252,6 +254,9 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
252
254
|
? raw.toUpperCase()
|
|
253
255
|
: raw;
|
|
254
256
|
if (key.escape) {
|
|
257
|
+
// Esc backs out of the workspace settings pane, not just its focus.
|
|
258
|
+
if (settings)
|
|
259
|
+
setSelected(0);
|
|
255
260
|
setFocus(single ? 'content' : 'nav');
|
|
256
261
|
return;
|
|
257
262
|
}
|
|
@@ -443,7 +448,9 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
|
|
|
443
448
|
return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
|
|
444
449
|
name: env,
|
|
445
450
|
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:
|
|
451
|
+
}, 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: onboard && configIncomplete(workspaceTarget?.base)
|
|
452
|
+
? t(' · new workspace: fill in once, every app inherits · Esc skips')
|
|
453
|
+
: 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
454
|
}
|
|
448
455
|
function renderOverlay(overlay, env) {
|
|
449
456
|
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',
|
|
@@ -224,12 +224,13 @@ const sv = {
|
|
|
224
224
|
'workspace config saved': 'arbetsytans konfig sparad',
|
|
225
225
|
'config saved': 'konfig sparad',
|
|
226
226
|
' · shared .dev_properties.json at the root': ' · delad .dev_properties.json i roten',
|
|
227
|
+
' · new workspace: fill in once, every app inherits · Esc skips': ' · ny arbetsyta: fyll i en gång, alla appar ärver · Esc hoppar över',
|
|
227
228
|
'y confirm · n cancel': 'y bekräfta · n avbryt',
|
|
228
229
|
// Config help
|
|
229
|
-
|
|
230
|
+
"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
231
|
"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
232
|
"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.',
|
|
233
|
+
'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
234
|
"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
235
|
'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
236
|
'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
|
/**
|
|
@@ -46,3 +46,19 @@ export function appGroup(root, appRoot) {
|
|
|
46
46
|
const relative = path.relative(root, path.dirname(appRoot));
|
|
47
47
|
return relative === '' ? '.' : relative;
|
|
48
48
|
}
|
|
49
|
+
/** Missing something nothing can deploy without. */
|
|
50
|
+
export function configIncomplete(dev) {
|
|
51
|
+
if (!dev?.domain || !dev.siteName)
|
|
52
|
+
return true;
|
|
53
|
+
return (dev.authMethod ?? 'basic') === 'basic' && !dev.username;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* True when a workspace has apps but nothing usable to deploy with: neither the
|
|
57
|
+
* shared root config nor the apps themselves carry domain/site/username. The
|
|
58
|
+
* shell then opens on Workspace settings instead of the first app.
|
|
59
|
+
*/
|
|
60
|
+
export function needsOnboarding(root, apps) {
|
|
61
|
+
return (apps.length > 0 &&
|
|
62
|
+
configIncomplete(readWorkspaceDevProperties(root)) &&
|
|
63
|
+
apps.some(app => configIncomplete(app.devProperties)));
|
|
64
|
+
}
|