sitevision-cli 1.0.0-beta.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/LICENSE +21 -0
- package/dist/cli.js +194 -70
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +95 -212
- package/dist/commands/dev.js +87 -416
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +50 -33
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +5 -31
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.js +33 -15
- package/dist/components/PasswordInput.js +2 -1
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.js +44 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.js +703 -0
- package/dist/shell/Frame.js +138 -0
- package/dist/shell/Help.js +96 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.js +149 -0
- package/dist/shell/Shell.js +953 -0
- package/dist/shell/Tabs.js +96 -0
- package/dist/shell/actions.js +493 -0
- package/dist/utils/config.js +62 -2
- package/dist/utils/create-app.js +231 -0
- package/dist/utils/debug.js +32 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.js +369 -0
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +54 -6
- package/dist/utils/oauth2-auth.js +146 -43
- package/dist/utils/password-prompt.js +37 -3
- package/dist/utils/process-runner.js +77 -38
- package/dist/utils/project-detection.js +457 -112
- package/dist/utils/session-cookie-auth.js +10 -6
- package/dist/utils/sitevision-api.js +134 -25
- package/dist/utils/sitevision-scripts-runner.js +50 -22
- package/dist/utils/tasks.js +510 -0
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +9 -5
- package/dist/utils/workspace.js +105 -0
- package/dist/utils/zip.js +18 -19
- package/package.json +27 -4
- package/readme.md +113 -141
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -307
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -16
- package/dist/commands/dev.d.ts +0 -16
- package/dist/commands/index.d.ts +0 -4
- package/dist/commands/info.d.ts +0 -2
- package/dist/commands/setup-signing.d.ts +0 -2
- package/dist/commands/sign.d.ts +0 -14
- package/dist/commands/types.d.ts +0 -18
- package/dist/commands/watch.d.ts +0 -2
- package/dist/components/AnimatedLogo.d.ts +0 -10
- package/dist/components/AuthLoginScreen.d.ts +0 -21
- package/dist/components/DevPropertiesForm.d.ts +0 -10
- package/dist/components/DevPropertiesForm.js +0 -210
- package/dist/components/InfoScreen.d.ts +0 -7
- package/dist/components/InfoScreen.js +0 -14
- package/dist/components/KeychainPasswordChoice.d.ts +0 -8
- package/dist/components/KeychainPasswordChoice.js +0 -30
- package/dist/components/MainMenu.d.ts +0 -7
- package/dist/components/MainMenu.js +0 -83
- package/dist/components/PasswordInput.d.ts +0 -10
- package/dist/components/ProcessOutput.d.ts +0 -6
- package/dist/components/ProcessOutput.js +0 -30
- package/dist/components/SetupFlow.d.ts +0 -8
- package/dist/components/SetupFlow.js +0 -233
- package/dist/components/SigningPropertiesForm.d.ts +0 -7
- package/dist/components/SigningPropertiesForm.js +0 -38
- package/dist/components/StatusIndicator.d.ts +0 -8
- package/dist/components/TextInput.d.ts +0 -10
- package/dist/components/TextInput.js +0 -29
- package/dist/components/WelcomeScreen.d.ts +0 -14
- package/dist/types/index.d.ts +0 -284
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -17
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -27
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -22
- package/dist/utils/project-detection.d.ts +0 -151
- package/dist/utils/session-cookie-auth.d.ts +0 -34
- package/dist/utils/signing-password.d.ts +0 -12
- package/dist/utils/signing-step.d.ts +0 -15
- package/dist/utils/signing-step.js +0 -16
- package/dist/utils/sitevision-api.d.ts +0 -112
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/zip.d.ts +0 -71
|
@@ -0,0 +1,96 @@
|
|
|
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, readAncestorDevProperties, 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
|
+
import { getGlobalSigning } from '../utils/config.js';
|
|
9
|
+
export const TABS = [
|
|
10
|
+
{ id: 'overview', label: 'Overview', short: 'Ovw' },
|
|
11
|
+
{ id: 'config', label: 'Config', short: 'Cfg' },
|
|
12
|
+
{ id: 'versions', label: 'Versions', short: 'Ver' },
|
|
13
|
+
{ id: 'log', label: 'Log', short: 'Log' },
|
|
14
|
+
];
|
|
15
|
+
export function TabBar({ tab, narrow, focused, }) {
|
|
16
|
+
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: [focused && `${i + 1} `, t(narrow ? entry.short : entry.label)] }), ' '.repeat(3)] }, entry.id))) }));
|
|
17
|
+
}
|
|
18
|
+
function Row({ label, value, dim, }) {
|
|
19
|
+
return (_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { dimColor: true, children: label.padEnd(14) }), value, dim && _jsxs(Text, { dimColor: true, children: [" ", dim] })] }) }));
|
|
20
|
+
}
|
|
21
|
+
const LEVEL_COLOR = {
|
|
22
|
+
info: undefined,
|
|
23
|
+
ok: 'green',
|
|
24
|
+
warn: 'yellow',
|
|
25
|
+
error: 'red',
|
|
26
|
+
};
|
|
27
|
+
const STATUS_GLYPH = {
|
|
28
|
+
running: _jsx(Spinner, { type: "dots" }),
|
|
29
|
+
success: _jsx(Text, { color: "green", children: "\u2713" }),
|
|
30
|
+
error: _jsx(Text, { color: "red", children: "\u2717" }),
|
|
31
|
+
stopped: _jsx(Text, { dimColor: true, children: "\u25A0" }),
|
|
32
|
+
};
|
|
33
|
+
function time(ms, seconds = false) {
|
|
34
|
+
const d = new Date(ms);
|
|
35
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
36
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
37
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
38
|
+
return seconds ? `${hh}:${mm}:${ss}` : `${hh}:${mm}`;
|
|
39
|
+
}
|
|
40
|
+
export function Overview({ project, tasks, height, }) {
|
|
41
|
+
const dev = project.devProperties;
|
|
42
|
+
const inherited = new Set(project.inheritedKeys);
|
|
43
|
+
const ancestors = readAncestorDevProperties(project.root);
|
|
44
|
+
const globalSigning = getGlobalSigning();
|
|
45
|
+
const src = (key) => inherited.has(key)
|
|
46
|
+
? Object.hasOwn(ancestors, key)
|
|
47
|
+
? t('↑ root')
|
|
48
|
+
: 'package.json'
|
|
49
|
+
: globalSigning[key] !== undefined &&
|
|
50
|
+
globalSigning[key] ===
|
|
51
|
+
dev?.[key]
|
|
52
|
+
? t('global')
|
|
53
|
+
: undefined;
|
|
54
|
+
const notSet = t('not set');
|
|
55
|
+
const sync = getPackageJsonSyncChanges(project.root).length;
|
|
56
|
+
const scripts = checkSitevisionScriptsCompatibility(project.root);
|
|
57
|
+
const recent = tasks
|
|
58
|
+
.filter(task => task.appRoot === project.root && task.status !== 'running')
|
|
59
|
+
.slice(-Math.max(1, height - 18))
|
|
60
|
+
.toReversed();
|
|
61
|
+
const status = (ok, okText, badText, warn = false) => (_jsxs(Text, { children: [_jsx(Text, { color: ok ? 'green' : warn ? 'yellow' : 'red', children: ok ? '✓' : warn ? '~' : '✗' }), ' ', ok ? okText : badText] }));
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { bold: true, wrap: "truncate", 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", flexShrink: 0, 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
|
|
63
|
+
? t('1 diff · y to apply')
|
|
64
|
+
: 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
|
|
65
|
+
? `${scripts.installed} · ${scripts.status}`
|
|
66
|
+
: 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)))] }))] }));
|
|
67
|
+
}
|
|
68
|
+
export function Versions({ project, state, selected, }) {
|
|
69
|
+
if (!project.devProperties) {
|
|
70
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "yellow", children: t('⚠ Configure dev properties first (e).') }) }));
|
|
71
|
+
}
|
|
72
|
+
if (!state) {
|
|
73
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: t('Press r to fetch versions from {domain}.', {
|
|
74
|
+
domain: project.devProperties.domain,
|
|
75
|
+
}) }) }));
|
|
76
|
+
}
|
|
77
|
+
const list = state.executables ?? [];
|
|
78
|
+
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}.', {
|
|
79
|
+
addon: project.devProperties.addonName,
|
|
80
|
+
}) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [list.length === 1
|
|
81
|
+
? t('1 version · a activate selected · r refresh')
|
|
82
|
+
: t('{n} versions · a activate selected · r refresh', {
|
|
83
|
+
n: list.length,
|
|
84
|
+
}), state.fetchedAt
|
|
85
|
+
? t(' · fetched {time}', { time: time(state.fetchedAt) })
|
|
86
|
+
: ''] }) })] }));
|
|
87
|
+
}
|
|
88
|
+
export function Log({ task, height, scroll, wrap, }) {
|
|
89
|
+
if (!task) {
|
|
90
|
+
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') }) }));
|
|
91
|
+
}
|
|
92
|
+
const visible = Math.max(1, height - 2);
|
|
93
|
+
const end = Math.max(0, task.lines.length - scroll);
|
|
94
|
+
const lines = task.lines.slice(Math.max(0, end - visible), end);
|
|
95
|
+
return (_jsxs(Box, { width: "100%", 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')] })] }), _jsx(Box, { flexDirection: "column", maxHeight: visible, justifyContent: "flex-end", overflow: "hidden", children: lines.map((line, i) => (_jsx(Box, { flexShrink: 0, children: _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))) }, wrap ? 'wrap' : 'truncate')] }));
|
|
96
|
+
}
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { getPackageJsonSyncChanges, syncDevPropertiesToPackageJson, migrateLegacyPassword, getAppType, } from '../utils/project-detection.js';
|
|
2
|
+
import { createAddon } from '../utils/sitevision-api.js';
|
|
3
|
+
import { getSigningPassword, setSigningPassword, setDeployPassword, deleteDeployPassword, deleteOAuth2RefreshToken, deleteSessionCookie, getOAuth2RefreshToken, } from '../utils/keychain.js';
|
|
4
|
+
import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
|
|
5
|
+
import { t } from '../utils/i18n.js';
|
|
6
|
+
import { toDeployConfig } from '../utils/workspace.js';
|
|
7
|
+
import { startBuild, startDev, startDeploy, startInstall, startSign, runningTasks, stoppableTasks, } from '../utils/tasks.js';
|
|
8
|
+
// Credentials obtained during this run, so a password or token is asked for
|
|
9
|
+
// once per app rather than once per action. Never written to disk here.
|
|
10
|
+
const session = {
|
|
11
|
+
deployPassword: new Map(),
|
|
12
|
+
signingPassword: new Map(),
|
|
13
|
+
credential: new Map(),
|
|
14
|
+
// Addons a deploy confirmed missing; only these offer "Create addon".
|
|
15
|
+
missingAddons: new Set(),
|
|
16
|
+
};
|
|
17
|
+
const deployKey = (p) => `${p.devProperties?.domain}\0${p.devProperties?.username}`;
|
|
18
|
+
const addonKey = (p) => `${p.devProperties?.domain} ${p.devProperties?.siteName} ${p.devProperties?.addonName}`;
|
|
19
|
+
const askCreateAddon = (ctx) => async (addon) => {
|
|
20
|
+
session.missingAddons.add(addonKey(ctx.project));
|
|
21
|
+
const yes = await ctx.confirm(t('Addon {addon} does not exist on {domain}. Create it and deploy again?', {
|
|
22
|
+
addon,
|
|
23
|
+
domain: ctx.project.devProperties?.domain ?? '',
|
|
24
|
+
}));
|
|
25
|
+
if (yes)
|
|
26
|
+
session.missingAddons.delete(addonKey(ctx.project));
|
|
27
|
+
return yes;
|
|
28
|
+
};
|
|
29
|
+
export function forgetSession(project) {
|
|
30
|
+
session.deployPassword.delete(deployKey(project));
|
|
31
|
+
session.credential.delete(deployKey(project));
|
|
32
|
+
if (project.devProperties?.signingUsername) {
|
|
33
|
+
session.signingPassword.delete(project.devProperties.signingUsername);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Top-bar auth state without prompting for anything. */
|
|
37
|
+
export function authState(project) {
|
|
38
|
+
const dev = project.devProperties;
|
|
39
|
+
if (!dev)
|
|
40
|
+
return { ready: false, label: t('not configured') };
|
|
41
|
+
const method = dev.authMethod ?? 'basic';
|
|
42
|
+
const key = deployKey(project);
|
|
43
|
+
const who = dev.username ? ` ${dev.username}` : '';
|
|
44
|
+
if (method === 'basic') {
|
|
45
|
+
const ready = Boolean(dev.password || session.deployPassword.has(key));
|
|
46
|
+
return { ready, label: ready ? `basic${who}` : t('basic · password needed') };
|
|
47
|
+
}
|
|
48
|
+
if (method === 'oauth2') {
|
|
49
|
+
const ready = Boolean(dev.accessToken ||
|
|
50
|
+
session.credential.get(key)?.accessToken ||
|
|
51
|
+
(dev.oauth2?.clientId &&
|
|
52
|
+
getOAuth2RefreshToken(dev.domain, dev.oauth2.clientId)));
|
|
53
|
+
return { ready, label: ready ? `oauth2${who}` : t('oauth2 · not logged in') };
|
|
54
|
+
}
|
|
55
|
+
const ready = Boolean(dev.sessionCookie || session.credential.get(key)?.sessionCookie);
|
|
56
|
+
return { ready, label: ready ? `sso${who}` : t('sso · not logged in') };
|
|
57
|
+
}
|
|
58
|
+
export async function resolveDeployConfig(ctx, fresh = false,
|
|
59
|
+
// Site-level calls (list addons, log in) work before an addon is chosen.
|
|
60
|
+
{ addon = true } = {}) {
|
|
61
|
+
const dev = ctx.project.devProperties;
|
|
62
|
+
const complete = toDeployConfig(dev, { addon });
|
|
63
|
+
if (!dev || 'error' in complete) {
|
|
64
|
+
ctx.notify(dev && 'error' in complete
|
|
65
|
+
? complete.error
|
|
66
|
+
: t('Dev properties not configured. Edit config first.'), 'warn');
|
|
67
|
+
ctx.setTab('config');
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const key = deployKey(ctx.project);
|
|
71
|
+
// Credentials are resolved below, per auth method.
|
|
72
|
+
const base = {
|
|
73
|
+
...complete.config,
|
|
74
|
+
password: undefined,
|
|
75
|
+
accessToken: undefined,
|
|
76
|
+
sessionCookie: undefined,
|
|
77
|
+
};
|
|
78
|
+
const method = dev.authMethod ?? 'basic';
|
|
79
|
+
if (method === 'basic') {
|
|
80
|
+
let password = fresh
|
|
81
|
+
? undefined
|
|
82
|
+
: (dev.password ?? session.deployPassword.get(key));
|
|
83
|
+
if (!password) {
|
|
84
|
+
const answer = await ctx.askPassword(t('Deploy password for {user}@{domain}', {
|
|
85
|
+
user: dev.username,
|
|
86
|
+
domain: dev.domain,
|
|
87
|
+
}), t('Save to OS keychain'));
|
|
88
|
+
if (!answer?.password)
|
|
89
|
+
return null;
|
|
90
|
+
password = answer.password;
|
|
91
|
+
session.deployPassword.set(key, password);
|
|
92
|
+
if (answer.remember)
|
|
93
|
+
setDeployPassword(dev.domain, dev.username, password);
|
|
94
|
+
}
|
|
95
|
+
return { ...base, password };
|
|
96
|
+
}
|
|
97
|
+
let credential = fresh ? undefined : session.credential.get(key);
|
|
98
|
+
if (!credential && !fresh) {
|
|
99
|
+
if (method === 'cookie' && dev.sessionCookie) {
|
|
100
|
+
credential = { sessionCookie: dev.sessionCookie };
|
|
101
|
+
}
|
|
102
|
+
else if (method === 'oauth2') {
|
|
103
|
+
const token = dev.accessToken ?? (await resolveOAuth2AccessToken(dev));
|
|
104
|
+
if (token)
|
|
105
|
+
credential = { accessToken: token };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
credential ??= (await ctx.login(method)) ?? undefined;
|
|
109
|
+
if (!credential)
|
|
110
|
+
return null;
|
|
111
|
+
session.credential.set(key, credential);
|
|
112
|
+
return { ...base, ...credential };
|
|
113
|
+
}
|
|
114
|
+
async function resolveSigning(ctx) {
|
|
115
|
+
const dev = ctx.project.devProperties;
|
|
116
|
+
if (!dev?.signingUsername) {
|
|
117
|
+
ctx.notify(t('Signing credentials not configured.'), 'warn');
|
|
118
|
+
ctx.setTab('config');
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const username = dev.signingUsername;
|
|
122
|
+
let password = process.env['SITEVISION_SIGNING_PASSWORD'] ??
|
|
123
|
+
session.signingPassword.get(username) ??
|
|
124
|
+
getSigningPassword(username) ??
|
|
125
|
+
undefined;
|
|
126
|
+
if (!password) {
|
|
127
|
+
const answer = await ctx.askPassword(t('Signing password for {user} (developer.sitevision.se)', {
|
|
128
|
+
user: username,
|
|
129
|
+
}), t('Save to OS keychain'));
|
|
130
|
+
if (!answer?.password)
|
|
131
|
+
return null;
|
|
132
|
+
password = answer.password;
|
|
133
|
+
if (answer.remember)
|
|
134
|
+
setSigningPassword(username, password);
|
|
135
|
+
}
|
|
136
|
+
session.signingPassword.set(username, password);
|
|
137
|
+
return { username, password, certificateName: dev.certificateName };
|
|
138
|
+
}
|
|
139
|
+
const hasDev = (p) => Boolean(p.devProperties);
|
|
140
|
+
const deployDetail = (p) => {
|
|
141
|
+
const dev = p.devProperties;
|
|
142
|
+
const env = t('to {env}', { env: dev?.environmentName ?? 'dev' });
|
|
143
|
+
return dev?.productionEnvironment
|
|
144
|
+
? `${env} · ${t('signed zip · confirms first')}`
|
|
145
|
+
: env;
|
|
146
|
+
};
|
|
147
|
+
/** Deploy to the active environment; production means signed + activate. */
|
|
148
|
+
async function deployTo(ctx, force) {
|
|
149
|
+
if (ctx.isProduction &&
|
|
150
|
+
!(await ctx.confirm(t('Deploy the signed {id} to {env} (addon {addon}) and activate it?', {
|
|
151
|
+
id: ctx.project.manifest.id,
|
|
152
|
+
env: ctx.environment,
|
|
153
|
+
addon: ctx.project.devProperties?.addonName ?? '?',
|
|
154
|
+
})))) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const config = await resolveDeployConfig(ctx);
|
|
158
|
+
if (!config)
|
|
159
|
+
return;
|
|
160
|
+
startDeploy(ctx.project, config, {
|
|
161
|
+
...(ctx.isProduction ? { production: true, activate: true } : { force }),
|
|
162
|
+
onAddonMissing: askCreateAddon(ctx),
|
|
163
|
+
});
|
|
164
|
+
ctx.setTab('log');
|
|
165
|
+
}
|
|
166
|
+
const hasSigning = (p) => Boolean(p.devProperties?.signingUsername);
|
|
167
|
+
async function startDevOrWatch(ctx, deploy) {
|
|
168
|
+
if (deploy && ctx.isProduction) {
|
|
169
|
+
ctx.notify(t('Dev never deploys to a production environment ({env}). Switch with v.', {
|
|
170
|
+
env: ctx.environment,
|
|
171
|
+
}), 'warn');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (runningTasks(ctx.project.root).some(task => task.kind === 'dev' || task.kind === 'watch')) {
|
|
175
|
+
ctx.notify(t('A dev/watch task is already running for this app (K stops it).'), 'warn');
|
|
176
|
+
ctx.setTab('log');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const signingCredentials = hasSigning(ctx.project)
|
|
180
|
+
? ((await resolveSigning(ctx)) ?? undefined)
|
|
181
|
+
: undefined;
|
|
182
|
+
if (hasSigning(ctx.project) && !signingCredentials)
|
|
183
|
+
return;
|
|
184
|
+
const deployConfig = deploy ? await resolveDeployConfig(ctx) : undefined;
|
|
185
|
+
if (deploy && !deployConfig)
|
|
186
|
+
return;
|
|
187
|
+
startDev(ctx.project, {
|
|
188
|
+
deploy,
|
|
189
|
+
signingCredentials,
|
|
190
|
+
deployConfig: deployConfig ?? undefined,
|
|
191
|
+
onAddonMissing: askCreateAddon(ctx),
|
|
192
|
+
});
|
|
193
|
+
ctx.setTab('log');
|
|
194
|
+
}
|
|
195
|
+
export const actions = [
|
|
196
|
+
{
|
|
197
|
+
id: 'dev',
|
|
198
|
+
key: 'd',
|
|
199
|
+
group: 'app',
|
|
200
|
+
label: 'Dev',
|
|
201
|
+
detail: p => hasSigning(p)
|
|
202
|
+
? t('build, sign and deploy on change')
|
|
203
|
+
: t('build and deploy on change'),
|
|
204
|
+
enabled: hasDev,
|
|
205
|
+
async run(ctx) {
|
|
206
|
+
await startDevOrWatch(ctx, true);
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'watch',
|
|
211
|
+
key: 'w',
|
|
212
|
+
group: 'app',
|
|
213
|
+
label: 'Watch',
|
|
214
|
+
detail: p => hasSigning(p) ? t('rebuild and sign only') : t('rebuild only'),
|
|
215
|
+
async run(ctx) {
|
|
216
|
+
await startDevOrWatch(ctx, false);
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'build',
|
|
221
|
+
key: 'b',
|
|
222
|
+
group: 'app',
|
|
223
|
+
label: 'Build',
|
|
224
|
+
async run(ctx) {
|
|
225
|
+
startBuild(ctx.project);
|
|
226
|
+
ctx.setTab('log');
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'sign',
|
|
231
|
+
key: 's',
|
|
232
|
+
group: 'app',
|
|
233
|
+
label: 'Sign',
|
|
234
|
+
detail: p => (hasSigning(p) ? undefined : t('credentials missing')),
|
|
235
|
+
async run(ctx) {
|
|
236
|
+
const credentials = await resolveSigning(ctx);
|
|
237
|
+
if (!credentials)
|
|
238
|
+
return;
|
|
239
|
+
startSign(ctx.project, credentials);
|
|
240
|
+
ctx.setTab('log');
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'deploy',
|
|
245
|
+
key: 'p',
|
|
246
|
+
group: 'app',
|
|
247
|
+
label: 'Deploy',
|
|
248
|
+
detail: deployDetail,
|
|
249
|
+
enabled: hasDev,
|
|
250
|
+
async run(ctx) {
|
|
251
|
+
await deployTo(ctx, false);
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'deploy-force',
|
|
256
|
+
key: 'P',
|
|
257
|
+
group: 'app',
|
|
258
|
+
label: 'Deploy (force)',
|
|
259
|
+
detail: deployDetail,
|
|
260
|
+
enabled: hasDev,
|
|
261
|
+
async run(ctx) {
|
|
262
|
+
await deployTo(ctx, true);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'environment',
|
|
267
|
+
key: 'v',
|
|
268
|
+
group: 'app',
|
|
269
|
+
label: 'Switch environment',
|
|
270
|
+
detail: p => p.devProperties?.environmentName ?? 'dev',
|
|
271
|
+
enabled: hasDev,
|
|
272
|
+
async run(ctx) {
|
|
273
|
+
ctx.cycleEnvironment();
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'add-environment',
|
|
278
|
+
group: 'setup',
|
|
279
|
+
label: 'Add environment',
|
|
280
|
+
detail: () => t('e.g. test or prod, overriding domain and auth'),
|
|
281
|
+
enabled: hasDev,
|
|
282
|
+
async run(ctx) {
|
|
283
|
+
await ctx.addEnvironment();
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'versions',
|
|
288
|
+
key: 'a',
|
|
289
|
+
group: 'app',
|
|
290
|
+
label: 'Activate a remote version',
|
|
291
|
+
enabled: hasDev,
|
|
292
|
+
async run(ctx) {
|
|
293
|
+
ctx.setTab('versions');
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'stop',
|
|
298
|
+
key: 'K',
|
|
299
|
+
group: 'app',
|
|
300
|
+
label: 'Stop running task',
|
|
301
|
+
enabled: p => stoppableTasks(p.root).length > 0,
|
|
302
|
+
async run(ctx) {
|
|
303
|
+
const running = stoppableTasks(ctx.project.root);
|
|
304
|
+
for (const task of running)
|
|
305
|
+
task.stop();
|
|
306
|
+
ctx.notify(running.length > 0
|
|
307
|
+
? t('stopped {n} task(s)', { n: running.length })
|
|
308
|
+
: t('nothing running'));
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'workspace-settings',
|
|
313
|
+
group: 'setup',
|
|
314
|
+
label: 'Workspace settings',
|
|
315
|
+
detail: () => t('shared auth and site config for every app'),
|
|
316
|
+
async run(ctx) {
|
|
317
|
+
if (ctx.openWorkspaceSettings)
|
|
318
|
+
ctx.openWorkspaceSettings();
|
|
319
|
+
else
|
|
320
|
+
ctx.notify(t('Not in a workspace: run svc at the repo root.'), 'warn');
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'help',
|
|
325
|
+
key: '?',
|
|
326
|
+
group: 'setup',
|
|
327
|
+
label: 'Help',
|
|
328
|
+
detail: () => t('every key in one place'),
|
|
329
|
+
async run(ctx) {
|
|
330
|
+
ctx.openHelp();
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'changelog',
|
|
335
|
+
group: 'setup',
|
|
336
|
+
label: "What's new",
|
|
337
|
+
detail: () => t('changelog for every release'),
|
|
338
|
+
async run(ctx) {
|
|
339
|
+
ctx.openChangelog();
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'settings',
|
|
344
|
+
key: ',',
|
|
345
|
+
group: 'setup',
|
|
346
|
+
label: 'Settings',
|
|
347
|
+
detail: () => t('language, intro animation, workspace config'),
|
|
348
|
+
async run(ctx) {
|
|
349
|
+
ctx.openSettings();
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'config',
|
|
354
|
+
key: 'e',
|
|
355
|
+
group: 'setup',
|
|
356
|
+
label: 'Edit config',
|
|
357
|
+
detail: () => t('dev properties, auth method, signing'),
|
|
358
|
+
async run(ctx) {
|
|
359
|
+
ctx.setTab('config');
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: 'new-app',
|
|
364
|
+
group: 'setup',
|
|
365
|
+
label: 'New app',
|
|
366
|
+
detail: () => 'create-sitevision-app',
|
|
367
|
+
async run(ctx) {
|
|
368
|
+
await ctx.createApp();
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: 'create-addon',
|
|
373
|
+
group: 'setup',
|
|
374
|
+
label: 'Create addon',
|
|
375
|
+
detail: p => p.devProperties?.addonName,
|
|
376
|
+
enabled: hasDev,
|
|
377
|
+
hidden: p => !session.missingAddons.has(addonKey(p)),
|
|
378
|
+
async run(ctx) {
|
|
379
|
+
const config = await resolveDeployConfig(ctx);
|
|
380
|
+
if (!config)
|
|
381
|
+
return;
|
|
382
|
+
const result = await createAddon(config, getAppType(ctx.project.manifest));
|
|
383
|
+
if (!result.success) {
|
|
384
|
+
ctx.notify(result.error ?? t('Create addon failed'), 'error');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
session.missingAddons.delete(addonKey(ctx.project));
|
|
388
|
+
ctx.notify(t('addon {addon} created', { addon: config.addonName }), 'ok');
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 'sync-package',
|
|
393
|
+
key: 'y',
|
|
394
|
+
group: 'setup',
|
|
395
|
+
label: 'Sync package.json',
|
|
396
|
+
detail(p) {
|
|
397
|
+
const n = getPackageJsonSyncChanges(p.root).length;
|
|
398
|
+
return n === 0
|
|
399
|
+
? t('in sync')
|
|
400
|
+
: n === 1
|
|
401
|
+
? t('1 diff')
|
|
402
|
+
: t('{n} diffs', { n });
|
|
403
|
+
},
|
|
404
|
+
enabled: p => getPackageJsonSyncChanges(p.root).length > 0,
|
|
405
|
+
async run(ctx) {
|
|
406
|
+
syncDevPropertiesToPackageJson(ctx.project.root);
|
|
407
|
+
ctx.reload();
|
|
408
|
+
ctx.notify(t('package.json updated'), 'ok');
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: 'migrate-password',
|
|
413
|
+
group: 'setup',
|
|
414
|
+
label: 'Migrate password to OS keychain',
|
|
415
|
+
enabled: p => p.hasLegacyPassword,
|
|
416
|
+
async run(ctx) {
|
|
417
|
+
const moved = migrateLegacyPassword(ctx.project);
|
|
418
|
+
ctx.reload();
|
|
419
|
+
ctx.notify(moved
|
|
420
|
+
? t('password moved to keychain')
|
|
421
|
+
: t('could not access keychain'), moved ? 'ok' : 'error');
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: 'install',
|
|
426
|
+
key: 'i',
|
|
427
|
+
group: 'setup',
|
|
428
|
+
label: 'Install dependencies',
|
|
429
|
+
detail: () => 'npm install',
|
|
430
|
+
async run(ctx) {
|
|
431
|
+
startInstall(ctx.project);
|
|
432
|
+
ctx.setTab('log');
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: 'login',
|
|
437
|
+
key: 'l',
|
|
438
|
+
group: 'auth',
|
|
439
|
+
label: 'Log in',
|
|
440
|
+
detail: p => p.devProperties?.authMethod ?? 'basic',
|
|
441
|
+
enabled: hasDev,
|
|
442
|
+
async run(ctx) {
|
|
443
|
+
forgetSession(ctx.project);
|
|
444
|
+
if (await resolveDeployConfig(ctx, true, { addon: false }))
|
|
445
|
+
ctx.notify(t('logged in'), 'ok');
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'logout',
|
|
450
|
+
group: 'auth',
|
|
451
|
+
label: 'Log out',
|
|
452
|
+
detail: () => t('forget stored credentials for this site'),
|
|
453
|
+
enabled: hasDev,
|
|
454
|
+
async run(ctx) {
|
|
455
|
+
const dev = ctx.project.devProperties;
|
|
456
|
+
forgetSession(ctx.project);
|
|
457
|
+
deleteDeployPassword(dev.domain, dev.username);
|
|
458
|
+
deleteSessionCookie(dev.domain, dev.username);
|
|
459
|
+
if (dev.oauth2?.clientId)
|
|
460
|
+
deleteOAuth2RefreshToken(dev.domain, dev.oauth2.clientId);
|
|
461
|
+
dev.password = undefined;
|
|
462
|
+
dev.accessToken = undefined;
|
|
463
|
+
dev.sessionCookie = undefined;
|
|
464
|
+
ctx.reload();
|
|
465
|
+
ctx.notify(t('credentials removed'), 'ok');
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: 'quit',
|
|
470
|
+
key: 'q',
|
|
471
|
+
group: 'app',
|
|
472
|
+
label: 'Quit',
|
|
473
|
+
async run(ctx) {
|
|
474
|
+
ctx.quit();
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
];
|
|
478
|
+
export function actionForKey(key) {
|
|
479
|
+
return actions.find(a => a.key === key);
|
|
480
|
+
}
|
|
481
|
+
/** Subsequence match, case-insensitive: "dpp" matches "Deploy to production". */
|
|
482
|
+
export function fuzzyMatch(query, text) {
|
|
483
|
+
const q = query.toLowerCase();
|
|
484
|
+
const haystack = text.toLowerCase();
|
|
485
|
+
let i = 0;
|
|
486
|
+
for (const ch of haystack) {
|
|
487
|
+
if (ch === q[i])
|
|
488
|
+
i++;
|
|
489
|
+
if (i === q.length)
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
return q.length === 0;
|
|
493
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
+
import { parseJsonc } from './jsonc.js';
|
|
5
|
+
import { debug } from './debug.js';
|
|
4
6
|
function configDir() {
|
|
5
7
|
const base = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
|
6
8
|
return path.join(base, 'sitevision-cli');
|
|
@@ -10,13 +12,33 @@ function configFile() {
|
|
|
10
12
|
}
|
|
11
13
|
function readConfig() {
|
|
12
14
|
try {
|
|
13
|
-
return
|
|
15
|
+
return parseJsonc(fs.readFileSync(configFile(), 'utf8'));
|
|
14
16
|
}
|
|
15
17
|
catch {
|
|
16
18
|
return {};
|
|
17
19
|
}
|
|
18
20
|
}
|
|
21
|
+
/** Set when the file exists but does not parse; it is then left untouched. */
|
|
22
|
+
export function configProblem() {
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
text = fs.readFileSync(configFile(), 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
parseJsonc(text);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
19
38
|
function writeConfig(config) {
|
|
39
|
+
// A hand-edited file with a typo must not be replaced by defaults.
|
|
40
|
+
if (configProblem())
|
|
41
|
+
return;
|
|
20
42
|
try {
|
|
21
43
|
fs.mkdirSync(configDir(), { recursive: true });
|
|
22
44
|
fs.writeFileSync(configFile(), JSON.stringify(config, null, 2));
|
|
@@ -26,11 +48,47 @@ function writeConfig(config) {
|
|
|
26
48
|
// shows again next time, which is harmless.
|
|
27
49
|
}
|
|
28
50
|
}
|
|
51
|
+
export function getSettings() {
|
|
52
|
+
const config = readConfig();
|
|
53
|
+
return {
|
|
54
|
+
language: config.language ?? 'en',
|
|
55
|
+
introAnimation: config.introAnimation ?? true,
|
|
56
|
+
updateCheck: config.updateCheck ?? true,
|
|
57
|
+
addonNameDriftWarning: config.warnings?.addonNameDrift ?? true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function setSettings(patch) {
|
|
61
|
+
debug('config', `settings ${JSON.stringify(patch)}`);
|
|
62
|
+
const { addonNameDriftWarning, ...rest } = patch;
|
|
63
|
+
const config = { ...readConfig(), ...rest };
|
|
64
|
+
if (addonNameDriftWarning !== undefined) {
|
|
65
|
+
config.warnings = {
|
|
66
|
+
...config.warnings,
|
|
67
|
+
addonNameDrift: addonNameDriftWarning,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
writeConfig(config);
|
|
71
|
+
}
|
|
72
|
+
export function getGlobalSigning() {
|
|
73
|
+
const { signingUsername, certificateName } = readConfig();
|
|
74
|
+
return {
|
|
75
|
+
...(signingUsername && { signingUsername }),
|
|
76
|
+
...(certificateName && { certificateName }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function setGlobalSigning(signing) {
|
|
80
|
+
debug('config', `global signing ${Object.keys(signing).join(', ')}`);
|
|
81
|
+
writeConfig({ ...readConfig(), ...signing });
|
|
82
|
+
}
|
|
83
|
+
/** Path of the config file, for display. */
|
|
84
|
+
export function settingsFile() {
|
|
85
|
+
return configFile();
|
|
86
|
+
}
|
|
29
87
|
/**
|
|
30
88
|
* True until the user has completed the first-run welcome at least once.
|
|
31
89
|
*/
|
|
32
90
|
export function isFirstRun() {
|
|
33
|
-
return !readConfig().firstRunCompleted;
|
|
91
|
+
return !configProblem() && !readConfig().firstRunCompleted;
|
|
34
92
|
}
|
|
35
93
|
/**
|
|
36
94
|
* Persist that the first-run welcome has been seen.
|
|
@@ -52,6 +110,8 @@ export function getLastSeenVersion() {
|
|
|
52
110
|
*/
|
|
53
111
|
export function setLastSeenVersion(version) {
|
|
54
112
|
const config = readConfig();
|
|
113
|
+
if (config.lastSeenVersion === version)
|
|
114
|
+
return;
|
|
55
115
|
config.lastSeenVersion = version;
|
|
56
116
|
writeConfig(config);
|
|
57
117
|
}
|