sitevision-cli 1.0.0-beta.20 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shell/ConfigForm.d.ts +1 -0
- package/dist/shell/ConfigForm.js +76 -17
- package/dist/shell/Frame.js +1 -3
- package/dist/shell/Shell.js +16 -6
- package/dist/shell/Tabs.js +8 -3
- package/dist/shell/actions.js +0 -0
- package/dist/utils/i18n.js +2 -0
- package/dist/utils/project-detection.d.ts +31 -9
- package/dist/utils/project-detection.js +165 -55
- package/package.json +1 -1
- package/readme.md +102 -196
|
@@ -19,6 +19,7 @@ export interface ConfigTarget {
|
|
|
19
19
|
base?: Partial<DevProperties>;
|
|
20
20
|
environment?: string;
|
|
21
21
|
workspace?: boolean;
|
|
22
|
+
workspaceRoot?: string;
|
|
22
23
|
}
|
|
23
24
|
/** Apply the form to disk and the keychain. Exported for the test. */
|
|
24
25
|
export declare function saveConfig(project: ConfigTarget, values: Values, edited: Set<string>): void;
|
package/dist/shell/ConfigForm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { getPackageJsonSyncChanges, normalizeDomain, readInheritedDevProperties, writeDevProperties, } from '../utils/project-detection.js';
|
|
4
|
+
import { findDevPropertiesPath, getPackageJsonSyncChanges, hasPackageJson, IMPLICIT_VALUES, normalizeDomain, readAncestorDevProperties, readInheritedDevProperties, readWorkspaceDevProperties, updatePackageJson, writeDevProperties, } from '../utils/project-detection.js';
|
|
5
5
|
import { setDeployPassword, deleteDeployPassword, getOAuth2ClientSecret, setOAuth2ClientSecret, deleteOAuth2ClientSecret, getSigningPassword, setSigningPassword, deleteSigningPassword, } from '../utils/keychain.js';
|
|
6
6
|
import { DEFAULT_SCOPES, discoverOAuth2Config } from '../utils/oauth2-auth.js';
|
|
7
7
|
import { ACCENT } from './Frame.js';
|
|
@@ -185,6 +185,41 @@ function storedSecret(project, key) {
|
|
|
185
185
|
}
|
|
186
186
|
return Boolean(dev.signingUsername && getSigningPassword(dev.signingUsername));
|
|
187
187
|
}
|
|
188
|
+
const errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
189
|
+
const sameValue = (key, a, b) => JSON.stringify(a === '' || a === undefined ? IMPLICIT_VALUES[key] : a) ===
|
|
190
|
+
JSON.stringify(b === '' || b === undefined ? IMPLICIT_VALUES[key] : b);
|
|
191
|
+
/**
|
|
192
|
+
* App mode writes the app's complete file. Workspace mode never creates an
|
|
193
|
+
* app's .dev_properties.json: changes go to the root file, and the addon name
|
|
194
|
+
* to the app's package.json. An app that already has its own file keeps it.
|
|
195
|
+
*/
|
|
196
|
+
function writeConfigFile(project, file) {
|
|
197
|
+
const { workspaceRoot } = project;
|
|
198
|
+
if (!workspaceRoot ||
|
|
199
|
+
project.workspace ||
|
|
200
|
+
findDevPropertiesPath(project.root)) {
|
|
201
|
+
writeDevProperties(project.root, file, {
|
|
202
|
+
complete: !workspaceRoot && !project.workspace,
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const before = (project.base ?? project.devProperties ?? {});
|
|
207
|
+
const after = file;
|
|
208
|
+
const root = readWorkspaceDevProperties(workspaceRoot);
|
|
209
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
210
|
+
if (sameValue(key, after[key], before[key]))
|
|
211
|
+
continue;
|
|
212
|
+
if (key === 'addonName') {
|
|
213
|
+
updatePackageJson(project.root, packageJson => {
|
|
214
|
+
packageJson['addonName'] = after[key] || undefined;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
root[key] = after[key];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
writeDevProperties(workspaceRoot, root);
|
|
222
|
+
}
|
|
188
223
|
/** Apply the form to disk and the keychain. Exported for the test. */
|
|
189
224
|
export function saveConfig(project, values, edited) {
|
|
190
225
|
const method = values['authMethod'];
|
|
@@ -228,7 +263,7 @@ export function saveConfig(project, values, edited) {
|
|
|
228
263
|
base.certificateName = next.certificateName;
|
|
229
264
|
base.baseEnvironment = next.baseEnvironment;
|
|
230
265
|
base.production = next.production;
|
|
231
|
-
|
|
266
|
+
writeConfigFile(project, withEnvironmentOverride(base, env, {
|
|
232
267
|
domain: next.domain,
|
|
233
268
|
siteName: next.siteName,
|
|
234
269
|
addonName: next.addonName,
|
|
@@ -240,7 +275,7 @@ export function saveConfig(project, values, edited) {
|
|
|
240
275
|
}));
|
|
241
276
|
}
|
|
242
277
|
else {
|
|
243
|
-
|
|
278
|
+
writeConfigFile(project, {
|
|
244
279
|
...next,
|
|
245
280
|
environments: project.base?.environments ?? project.devProperties?.environments,
|
|
246
281
|
});
|
|
@@ -293,15 +328,21 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
293
328
|
const fields = visibleFields(method, project.workspace, envMode);
|
|
294
329
|
const current = fields[Math.min(cursor, fields.length - 1)];
|
|
295
330
|
const inherited = readInheritedDevProperties(project.root);
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
331
|
+
const ancestors = readAncestorDevProperties(project.root);
|
|
332
|
+
const changes = getPackageJsonSyncChanges(project.root);
|
|
333
|
+
const packageJsonExists = hasPackageJson(project.root);
|
|
299
334
|
// Write one field to disk (and the keychain for secrets) right away.
|
|
300
335
|
const commit = (key, value, label = current.label) => {
|
|
301
336
|
const clean = key === 'domain' ? normalizeDomain(value) : value;
|
|
302
337
|
const next = { ...values, [key]: clean };
|
|
303
338
|
setValues(next);
|
|
304
|
-
|
|
339
|
+
try {
|
|
340
|
+
saveConfig(project, next, new Set([key]));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
setNote(t('Not saved: {error}', { error: errorText(error) }));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
305
346
|
setNote(clean === value
|
|
306
347
|
? t('Saved {label}.', { label: t(label) })
|
|
307
348
|
: t('Saved {label} as {value} — a domain is a host only.', {
|
|
@@ -328,7 +369,13 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
328
369
|
tokenEndpoint: values['tokenEndpoint'] || found.tokenEndpoint,
|
|
329
370
|
};
|
|
330
371
|
setValues(next);
|
|
331
|
-
|
|
372
|
+
try {
|
|
373
|
+
saveConfig(project, next, new Set());
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
setNote(t('Not saved: {error}', { error: errorText(error) }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
332
379
|
onSaved();
|
|
333
380
|
setNote(t('Endpoints filled from the site OpenID config.'));
|
|
334
381
|
}
|
|
@@ -359,8 +406,12 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
359
406
|
}
|
|
360
407
|
else if (key.return) {
|
|
361
408
|
setEditing(false);
|
|
362
|
-
|
|
409
|
+
// A secret row always starts empty, so Enter on an empty one means
|
|
410
|
+
// "remove the stored secret", not "no change".
|
|
411
|
+
if (draft !== values[current.key] ||
|
|
412
|
+
(current.kind === 'secret' && storedSecret(project, current.key))) {
|
|
363
413
|
commit(current.key, draft);
|
|
414
|
+
}
|
|
364
415
|
}
|
|
365
416
|
else if (choices.length > 0) {
|
|
366
417
|
const step = key.leftArrow || key.upArrow
|
|
@@ -434,16 +485,22 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
434
485
|
color: overridden ? 'yellow' : undefined,
|
|
435
486
|
};
|
|
436
487
|
}
|
|
437
|
-
const
|
|
488
|
+
const inOAuth2 = [
|
|
438
489
|
'clientId',
|
|
439
490
|
'authorizationEndpoint',
|
|
440
491
|
'tokenEndpoint',
|
|
441
|
-
].includes(f.key)
|
|
492
|
+
].includes(f.key);
|
|
493
|
+
const inheritedValue = inOAuth2
|
|
442
494
|
? inherited['oauth2']?.[f.key]
|
|
443
495
|
: inherited[f.key];
|
|
444
496
|
if (inheritedValue !== undefined &&
|
|
445
497
|
JSON.stringify(inheritedValue) === JSON.stringify(value)) {
|
|
446
|
-
|
|
498
|
+
// A parent .dev_properties.json outranks package.json when it has the key.
|
|
499
|
+
return {
|
|
500
|
+
text: Object.hasOwn(ancestors, inOAuth2 ? 'oauth2' : f.key)
|
|
501
|
+
? t('↑ root')
|
|
502
|
+
: 'package.json',
|
|
503
|
+
};
|
|
447
504
|
}
|
|
448
505
|
return { text: (values[f.key] ?? '') ? t('local') : '' };
|
|
449
506
|
};
|
|
@@ -491,9 +548,11 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
491
548
|
}
|
|
492
549
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", height: height, children: [_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [(' ' + t('FIELD')).padEnd(24), t('VALUE').padEnd(valueWidth), t('SOURCE')] }) }), rows, project.workspace && (_jsx(Box, { marginTop: 1, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: t("Shared by every app below {root}. An app's own value wins.", {
|
|
493
550
|
root: project.root,
|
|
494
|
-
}) }) })),
|
|
495
|
-
? t('
|
|
496
|
-
: changes.length ===
|
|
497
|
-
? t('
|
|
498
|
-
:
|
|
551
|
+
}) }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { bold: true, dimColor: true, children: [t('PACKAGE.JSON SYNC'), ' ', _jsx(Text, { color: changes.length > 0 || !packageJsonExists ? 'yellow' : 'green', children: !packageJsonExists
|
|
552
|
+
? t('no workspace package.json · y to set up')
|
|
553
|
+
: changes.length === 0
|
|
554
|
+
? t('in sync')
|
|
555
|
+
: changes.length === 1
|
|
556
|
+
? t('1 diff · y to apply')
|
|
557
|
+
: t('{n} diffs · y to apply', { n: changes.length }) })] }), changes.map(c => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: c.from === undefined ? 'green' : 'yellow', children: c.from === undefined ? '+ ' : '~ ' }), c.key, ": ", c.from !== undefined && _jsxs(Text, { dimColor: true, children: [c.from, " \u2192 "] }), c.to] }, c.key)))] }), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderDimColor: true, borderLeft: false, borderRight: false, borderBottom: false, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { bold: true, color: ACCENT, children: t(current.label) }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t(current.help)] })] }) }), _jsx(Text, { color: "yellow", children: note })] }));
|
|
499
558
|
}
|
package/dist/shell/Frame.js
CHANGED
|
@@ -25,9 +25,7 @@ export function typeGlyph(manifest) {
|
|
|
25
25
|
return type ? TYPE_GLYPH[type] : '???';
|
|
26
26
|
}
|
|
27
27
|
export function appStatus(project) {
|
|
28
|
-
const sync = project.
|
|
29
|
-
? getPackageJsonSyncChanges(project.root, project.devProperties).length
|
|
30
|
-
: 0;
|
|
28
|
+
const sync = getPackageJsonSyncChanges(project.root).length;
|
|
31
29
|
return {
|
|
32
30
|
deps: project.hasNodeModules,
|
|
33
31
|
config: Boolean(project.devProperties),
|
package/dist/shell/Shell.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
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
|
-
import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, } from '../utils/project-detection.js';
|
|
6
|
+
import { detectProject, appTypeOf, localizedText, readWorkspaceDevProperties, readSvcConfig, writeSvcConfig, writeDevProperties, syncDevPropertiesToPackageJson, } from '../utils/project-detection.js';
|
|
7
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';
|
|
@@ -157,10 +157,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
157
157
|
: rawProject.devProperties);
|
|
158
158
|
if (!base)
|
|
159
159
|
return;
|
|
160
|
-
writeDevProperties(targetRoot, {
|
|
161
|
-
...base,
|
|
162
|
-
environments: { ...base.environments, [clean]: {} },
|
|
163
|
-
});
|
|
160
|
+
writeDevProperties(targetRoot, { ...base, environments: { ...base.environments, [clean]: {} } }, { complete: !workspaceRoot });
|
|
164
161
|
reload();
|
|
165
162
|
setEnvChoice(clean);
|
|
166
163
|
writeSvcConfig(configRoot, { environment: clean });
|
|
@@ -327,9 +324,20 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
327
324
|
return;
|
|
328
325
|
}
|
|
329
326
|
if (settings && focus === 'content') {
|
|
330
|
-
// Settings pane: the form owns everything but q and Tab/Esc above.
|
|
327
|
+
// Settings pane: the form owns everything but q, y and Tab/Esc above.
|
|
331
328
|
if (input === 'q')
|
|
332
329
|
quit();
|
|
330
|
+
else if (input === 'y') {
|
|
331
|
+
try {
|
|
332
|
+
if (syncDevPropertiesToPackageJson(workspaceRoot)) {
|
|
333
|
+
reload();
|
|
334
|
+
notify(t('package.json updated'), 'ok');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
notify(error instanceof Error ? error.message : String(error), 'error');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
333
341
|
return;
|
|
334
342
|
}
|
|
335
343
|
if (tab === 'versions') {
|
|
@@ -386,6 +394,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
386
394
|
? h([
|
|
387
395
|
['↑↓', 'field'],
|
|
388
396
|
['Enter', 'edit'],
|
|
397
|
+
['y', 'sync'],
|
|
389
398
|
['Esc', 'back'],
|
|
390
399
|
['q', 'quit'],
|
|
391
400
|
])
|
|
@@ -490,6 +499,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
490
499
|
devProperties: project.devProperties,
|
|
491
500
|
base: rawProject.devProperties,
|
|
492
501
|
environment: env,
|
|
502
|
+
workspaceRoot,
|
|
493
503
|
}, active: formActive, width: narrow ? columns : columns - sidebar, height: contentHeight, pickAddon: pickAddon, onSaved: () => {
|
|
494
504
|
reload();
|
|
495
505
|
notify(t('config saved'), 'ok');
|
package/dist/shell/Tabs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
|
-
import { appTypeOf, getPackageJsonSyncChanges, localizedText, } from '../utils/project-detection.js';
|
|
4
|
+
import { appTypeOf, getPackageJsonSyncChanges, readAncestorDevProperties, localizedText, } from '../utils/project-detection.js';
|
|
5
5
|
import { checkSitevisionScriptsCompatibility } from '../utils/sitevision-scripts-runner.js';
|
|
6
6
|
import { ACCENT, elapsed } from './Frame.js';
|
|
7
7
|
import { t } from '../utils/i18n.js';
|
|
@@ -39,9 +39,14 @@ function time(ms, seconds = false) {
|
|
|
39
39
|
export function Overview({ project, tasks, height, }) {
|
|
40
40
|
const dev = project.devProperties;
|
|
41
41
|
const inherited = new Set(project.inheritedKeys);
|
|
42
|
-
const
|
|
42
|
+
const ancestors = readAncestorDevProperties(project.root);
|
|
43
|
+
const src = (key) => inherited.has(key)
|
|
44
|
+
? Object.hasOwn(ancestors, key)
|
|
45
|
+
? t('↑ root')
|
|
46
|
+
: 'package.json'
|
|
47
|
+
: undefined;
|
|
43
48
|
const notSet = t('not set');
|
|
44
|
-
const sync =
|
|
49
|
+
const sync = getPackageJsonSyncChanges(project.root).length;
|
|
45
50
|
const scripts = checkSitevisionScriptsCompatibility(project.root);
|
|
46
51
|
const recent = tasks
|
|
47
52
|
.filter(task => task.appRoot === project.root && task.status !== 'running')
|
package/dist/shell/actions.js
CHANGED
|
Binary file
|
package/dist/utils/i18n.js
CHANGED
|
@@ -187,6 +187,8 @@ const sv = {
|
|
|
187
187
|
SOURCE: 'KÄLLA',
|
|
188
188
|
"Shared by every app below {root}. An app's own value wins.": 'Delas av alla appar under {root}. Appens eget värde vinner.',
|
|
189
189
|
'PACKAGE.JSON SYNC': 'PACKAGE.JSON-SYNK',
|
|
190
|
+
'no workspace package.json · y to set up': 'ingen package.json i arbetsytan · y för att skapa',
|
|
191
|
+
'Not saved: {error}': 'Inte sparat: {error}',
|
|
190
192
|
'←→ choose · Enter confirm · Esc cancel': '←→ välj · Enter bekräfta · Esc avbryt',
|
|
191
193
|
'Enter save · Esc cancel': 'Enter spara · Esc avbryt',
|
|
192
194
|
'↑↓ field · Enter edit · ^O pick addon': '↑↓ fält · Enter redigera · ^O välj tillägg',
|
|
@@ -30,7 +30,14 @@ export declare function normalizeDomain(value: string): string;
|
|
|
30
30
|
* app's would be. Used to edit shared config from the shell.
|
|
31
31
|
*/
|
|
32
32
|
export declare function readWorkspaceDevProperties(root: string): Partial<DevProperties>;
|
|
33
|
-
/**
|
|
33
|
+
/** Ancestor directories' .dev_properties.json merged, nearest wins (no own file). */
|
|
34
|
+
export declare function readAncestorDevProperties(root: string): Partial<DevProperties>;
|
|
35
|
+
/** Shared defaults from package.json: the workspace root's first, the app's on top. */
|
|
36
|
+
export declare function readPackageDefaultsChain(root: string): Partial<DevProperties>;
|
|
37
|
+
/**
|
|
38
|
+
* Everything an app's own .dev_properties.json sits on top of: package.json
|
|
39
|
+
* defaults (root, then app), then ancestor .dev_properties.json files.
|
|
40
|
+
*/
|
|
34
41
|
export declare function readInheritedDevProperties(root: string): Partial<DevProperties>;
|
|
35
42
|
/**
|
|
36
43
|
* Get the full app ID including any prefix/suffix from environment
|
|
@@ -125,7 +132,9 @@ export declare function readDevProperties(projectRoot: string): DevProperties |
|
|
|
125
132
|
* `accessToken` and `sessionCookie` are held in the OS keychain / resolved at
|
|
126
133
|
* runtime instead.
|
|
127
134
|
*/
|
|
128
|
-
export declare function writeDevProperties(projectRoot: string, properties: DevProperties
|
|
135
|
+
export declare function writeDevProperties(projectRoot: string, properties: DevProperties, { complete }?: {
|
|
136
|
+
complete?: boolean;
|
|
137
|
+
}): void;
|
|
129
138
|
/**
|
|
130
139
|
* CLI preferences stored in .svcconfig at the project root. Unknown keys are
|
|
131
140
|
* preserved on write so hand-edited entries survive.
|
|
@@ -137,22 +146,35 @@ export interface SvcConfig {
|
|
|
137
146
|
}
|
|
138
147
|
export declare function readSvcConfig(projectRoot: string): SvcConfig;
|
|
139
148
|
export declare function writeSvcConfig(projectRoot: string, updates: SvcConfig): void;
|
|
149
|
+
/**
|
|
150
|
+
* Values tied to the person running svc. They stay in .dev_properties.json and
|
|
151
|
+
* never go into package.json.
|
|
152
|
+
*/
|
|
153
|
+
export declare const USER_KEYS: string[];
|
|
140
154
|
export interface PackageJsonSyncChange {
|
|
141
155
|
key: string;
|
|
142
156
|
from?: string;
|
|
143
157
|
to: string;
|
|
144
158
|
}
|
|
159
|
+
/** The shared defaults one directory's package.json provides. */
|
|
160
|
+
export declare function readPackageDefaults(dir: string): Partial<DevProperties>;
|
|
161
|
+
/** What an unset field means, so spelling out the default is not a change. */
|
|
162
|
+
export declare const IMPLICIT_VALUES: Record<string, unknown>;
|
|
163
|
+
/** What syncing this directory would change in its package.json. */
|
|
164
|
+
export declare function getPackageJsonSyncChanges(dir: string): PackageJsonSyncChange[];
|
|
165
|
+
export declare function hasPackageJson(dir: string): boolean;
|
|
145
166
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
167
|
+
* Copy the pending shared values from .dev_properties.json into package.json,
|
|
168
|
+
* creating package.json when the directory has none. Throws if it cannot be
|
|
169
|
+
* read or written.
|
|
149
170
|
*/
|
|
150
|
-
export declare function
|
|
171
|
+
export declare function syncDevPropertiesToPackageJson(dir: string): boolean;
|
|
151
172
|
/**
|
|
152
|
-
*
|
|
153
|
-
* file
|
|
173
|
+
* Edit package.json in place, keeping its indentation and trailing newline. A
|
|
174
|
+
* missing file is created; an unreadable or invalid one throws, so the caller
|
|
175
|
+
* can warn instead of dropping the change silently.
|
|
154
176
|
*/
|
|
155
|
-
export declare function
|
|
177
|
+
export declare function updatePackageJson(dir: string, mutate: (packageJson: Record<string, unknown>) => void): void;
|
|
156
178
|
/**
|
|
157
179
|
* Move a plaintext password from .dev_properties.json into the OS keychain and
|
|
158
180
|
* strip it from the file. Returns true if the password was migrated.
|
|
@@ -123,14 +123,32 @@ export function readWorkspaceDevProperties(root) {
|
|
|
123
123
|
}
|
|
124
124
|
return merged;
|
|
125
125
|
}
|
|
126
|
-
/**
|
|
127
|
-
export function
|
|
126
|
+
/** Ancestor directories' .dev_properties.json merged, nearest wins (no own file). */
|
|
127
|
+
export function readAncestorDevProperties(root) {
|
|
128
128
|
let merged = {};
|
|
129
129
|
for (const dir of ancestorDirs(root)) {
|
|
130
130
|
merged = { ...merged, ...readDevPropertiesFile(dir) };
|
|
131
131
|
}
|
|
132
132
|
return merged;
|
|
133
133
|
}
|
|
134
|
+
/** Shared defaults from package.json: the workspace root's first, the app's on top. */
|
|
135
|
+
export function readPackageDefaultsChain(root) {
|
|
136
|
+
let merged = {};
|
|
137
|
+
for (const dir of [...ancestorDirs(root), root]) {
|
|
138
|
+
merged = { ...merged, ...readPackageDefaults(dir) };
|
|
139
|
+
}
|
|
140
|
+
return merged;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Everything an app's own .dev_properties.json sits on top of: package.json
|
|
144
|
+
* defaults (root, then app), then ancestor .dev_properties.json files.
|
|
145
|
+
*/
|
|
146
|
+
export function readInheritedDevProperties(root) {
|
|
147
|
+
return {
|
|
148
|
+
...readPackageDefaultsChain(root),
|
|
149
|
+
...readAncestorDevProperties(root),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
134
152
|
/**
|
|
135
153
|
* Get app ID configuration from environment or defaults
|
|
136
154
|
*/
|
|
@@ -427,7 +445,9 @@ export function readDevProperties(projectRoot) {
|
|
|
427
445
|
* `accessToken` and `sessionCookie` are held in the OS keychain / resolved at
|
|
428
446
|
* runtime instead.
|
|
429
447
|
*/
|
|
430
|
-
export function writeDevProperties(projectRoot, properties
|
|
448
|
+
export function writeDevProperties(projectRoot, properties,
|
|
449
|
+
// App mode: write every value, so plain sitevision-scripts finds them all.
|
|
450
|
+
{ complete = false } = {}) {
|
|
431
451
|
const devPropertiesPath = findDevPropertiesPath(projectRoot) ||
|
|
432
452
|
getDefaultDevPropertiesPath(projectRoot);
|
|
433
453
|
const { password: _password, accessToken: _accessToken, sessionCookie: _sessionCookie, environmentName: _environmentName, productionEnvironment: _productionEnvironment, ...persisted } = properties;
|
|
@@ -435,7 +455,9 @@ export function writeDevProperties(projectRoot, properties) {
|
|
|
435
455
|
// at the workspace root instead of being copied into every app. An empty
|
|
436
456
|
// string means "unset", so it is dropped rather than written as an override
|
|
437
457
|
// that would shadow the inherited value.
|
|
438
|
-
const inherited =
|
|
458
|
+
const inherited = complete
|
|
459
|
+
? {}
|
|
460
|
+
: readInheritedDevProperties(projectRoot);
|
|
439
461
|
const own = Object.fromEntries(Object.entries(persisted).filter(([key, value]) => value !== '' &&
|
|
440
462
|
(!Object.hasOwn(inherited, key) ||
|
|
441
463
|
JSON.stringify(inherited[key]) !== JSON.stringify(value))));
|
|
@@ -457,13 +479,24 @@ export function writeSvcConfig(projectRoot, updates) {
|
|
|
457
479
|
// PACKAGE.JSON SYNC
|
|
458
480
|
// =============================================================================
|
|
459
481
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*/
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
482
|
+
* Values tied to the person running svc. They stay in .dev_properties.json and
|
|
483
|
+
* never go into package.json.
|
|
484
|
+
*/
|
|
485
|
+
export const USER_KEYS = ['username', 'signingUsername', 'certificateName'];
|
|
486
|
+
// Shared values package.json can hold: three top-level fields, the rest under "svc".
|
|
487
|
+
const PACKAGE_TOP_KEYS = {
|
|
488
|
+
domain: 'developmentDomain',
|
|
489
|
+
siteName: 'siteName',
|
|
490
|
+
addonName: 'addonName',
|
|
491
|
+
};
|
|
492
|
+
const PACKAGE_SVC_KEYS = [
|
|
493
|
+
'authMethod',
|
|
494
|
+
'oauth2',
|
|
495
|
+
'sessionLoginUrl',
|
|
496
|
+
'useHTTPForDevDeploy',
|
|
497
|
+
'baseEnvironment',
|
|
498
|
+
'production',
|
|
499
|
+
'environments',
|
|
467
500
|
];
|
|
468
501
|
function readPackageJson(projectRoot) {
|
|
469
502
|
try {
|
|
@@ -473,59 +506,122 @@ function readPackageJson(projectRoot) {
|
|
|
473
506
|
return null;
|
|
474
507
|
}
|
|
475
508
|
}
|
|
509
|
+
/** The shared defaults one directory's package.json provides. */
|
|
510
|
+
export function readPackageDefaults(dir) {
|
|
511
|
+
const packageJson = readPackageJson(dir);
|
|
512
|
+
if (!packageJson)
|
|
513
|
+
return {};
|
|
514
|
+
const defaults = {};
|
|
515
|
+
for (const [devKey, packageKey] of Object.entries(PACKAGE_TOP_KEYS)) {
|
|
516
|
+
const value = packageJson[packageKey];
|
|
517
|
+
if (typeof value === 'string' && value !== '')
|
|
518
|
+
defaults[devKey] = value;
|
|
519
|
+
}
|
|
520
|
+
const svc = packageJson['svc'];
|
|
521
|
+
for (const key of PACKAGE_SVC_KEYS) {
|
|
522
|
+
if (svc?.[key] !== undefined)
|
|
523
|
+
defaults[key] = svc[key];
|
|
524
|
+
}
|
|
525
|
+
if (typeof defaults['domain'] === 'string') {
|
|
526
|
+
defaults['domain'] = normalizeDomain(defaults['domain']);
|
|
527
|
+
}
|
|
528
|
+
return defaults;
|
|
529
|
+
}
|
|
476
530
|
/**
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* `npm install` in the same session may have rewritten it.
|
|
531
|
+
* Shared values in this directory's own .dev_properties.json that package.json
|
|
532
|
+
* does not already provide, here or further up. User values never qualify.
|
|
480
533
|
*/
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
if (!
|
|
534
|
+
function pendingSync(dir) {
|
|
535
|
+
const own = readDevPropertiesFile(dir);
|
|
536
|
+
if (!own)
|
|
484
537
|
return [];
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
: { key: packageKey, from, to });
|
|
538
|
+
const defaults = readPackageDefaultsChain(dir);
|
|
539
|
+
const pending = [];
|
|
540
|
+
for (const key of [...Object.keys(PACKAGE_TOP_KEYS), ...PACKAGE_SVC_KEYS]) {
|
|
541
|
+
let value = own[key];
|
|
542
|
+
if (key === 'environments' && value) {
|
|
543
|
+
value = Object.fromEntries(Object.entries(value).map(([name, override]) => [
|
|
544
|
+
name,
|
|
545
|
+
Object.fromEntries(Object.entries(override).filter(([field]) => !USER_KEYS.includes(field))),
|
|
546
|
+
]));
|
|
495
547
|
}
|
|
548
|
+
if (value === undefined || value === '')
|
|
549
|
+
continue;
|
|
550
|
+
if (JSON.stringify(value) ===
|
|
551
|
+
JSON.stringify(defaults[key] ?? IMPLICIT_VALUES[key]))
|
|
552
|
+
continue;
|
|
553
|
+
pending.push({ key, value });
|
|
496
554
|
}
|
|
497
|
-
return
|
|
555
|
+
return pending;
|
|
556
|
+
}
|
|
557
|
+
/** What an unset field means, so spelling out the default is not a change. */
|
|
558
|
+
export const IMPLICIT_VALUES = {
|
|
559
|
+
authMethod: 'basic',
|
|
560
|
+
useHTTPForDevDeploy: false,
|
|
561
|
+
production: false,
|
|
562
|
+
};
|
|
563
|
+
const packageLabel = (key) => PACKAGE_TOP_KEYS[key] ?? `svc.${key}`;
|
|
564
|
+
const display = (value) => typeof value === 'string' ? value : JSON.stringify(value);
|
|
565
|
+
/** What syncing this directory would change in its package.json. */
|
|
566
|
+
export function getPackageJsonSyncChanges(dir) {
|
|
567
|
+
const current = readPackageDefaults(dir);
|
|
568
|
+
return pendingSync(dir).map(({ key, value }) => current[key] === undefined
|
|
569
|
+
? { key: packageLabel(key), to: display(value) }
|
|
570
|
+
: {
|
|
571
|
+
key: packageLabel(key),
|
|
572
|
+
from: display(current[key]),
|
|
573
|
+
to: display(value),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
export function hasPackageJson(dir) {
|
|
577
|
+
return fs.existsSync(path.join(dir, 'package.json'));
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Copy the pending shared values from .dev_properties.json into package.json,
|
|
581
|
+
* creating package.json when the directory has none. Throws if it cannot be
|
|
582
|
+
* read or written.
|
|
583
|
+
*/
|
|
584
|
+
export function syncDevPropertiesToPackageJson(dir) {
|
|
585
|
+
const pending = pendingSync(dir);
|
|
586
|
+
if (pending.length === 0 && hasPackageJson(dir))
|
|
587
|
+
return false;
|
|
588
|
+
updatePackageJson(dir, packageJson => {
|
|
589
|
+
for (const { key, value } of pending) {
|
|
590
|
+
const topKey = PACKAGE_TOP_KEYS[key];
|
|
591
|
+
if (topKey) {
|
|
592
|
+
packageJson[topKey] = value;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
packageJson['svc'] = {
|
|
596
|
+
...packageJson['svc'],
|
|
597
|
+
[key]: value,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return true;
|
|
498
603
|
}
|
|
499
604
|
/**
|
|
500
|
-
*
|
|
501
|
-
* file
|
|
605
|
+
* Edit package.json in place, keeping its indentation and trailing newline. A
|
|
606
|
+
* missing file is created; an unreadable or invalid one throws, so the caller
|
|
607
|
+
* can warn instead of dropping the change silently.
|
|
502
608
|
*/
|
|
503
|
-
export function
|
|
504
|
-
const packageJsonPath = path.join(
|
|
505
|
-
|
|
609
|
+
export function updatePackageJson(dir, mutate) {
|
|
610
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
611
|
+
// A new file is private, so a workspace root is never published by accident.
|
|
612
|
+
let raw = '{\n\t"private": true\n}\n';
|
|
506
613
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
packageJson = JSON.parse(raw);
|
|
515
|
-
}
|
|
516
|
-
catch {
|
|
517
|
-
return false;
|
|
614
|
+
if (hasPackageJson(dir))
|
|
615
|
+
raw = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
616
|
+
const packageJson = JSON.parse(raw);
|
|
617
|
+
mutate(packageJson);
|
|
618
|
+
const indent = /^(?<indent>[\t ]+)/m.exec(raw)?.groups?.['indent'] ?? '\t';
|
|
619
|
+
const newline = raw.endsWith('\n') ? '\n' : '';
|
|
620
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, indent) + newline);
|
|
518
621
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (typeof value === 'string' && value !== '') {
|
|
522
|
-
packageJson[packageKey] = value;
|
|
523
|
-
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
throw new Error(`Could not update ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
524
624
|
}
|
|
525
|
-
const indent = /^(?<indent>[\t ]+)/m.exec(raw)?.groups?.['indent'] ?? '\t';
|
|
526
|
-
const newline = raw.endsWith('\n') ? '\n' : '';
|
|
527
|
-
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, indent) + newline);
|
|
528
|
-
return true;
|
|
529
625
|
}
|
|
530
626
|
/**
|
|
531
627
|
* Move a plaintext password from .dev_properties.json into the OS keychain and
|
|
@@ -542,8 +638,22 @@ export function migrateLegacyPassword(project) {
|
|
|
542
638
|
return false;
|
|
543
639
|
if (!setDeployPassword(domain, username, password))
|
|
544
640
|
return false;
|
|
545
|
-
//
|
|
546
|
-
|
|
641
|
+
// Remove only the password, from whichever file holds it; every other value
|
|
642
|
+
// stays as written.
|
|
643
|
+
for (const dir of [project.root, ...ancestorDirs(project.root)]) {
|
|
644
|
+
const file = findDevPropertiesPath(dir);
|
|
645
|
+
if (!file)
|
|
646
|
+
continue;
|
|
647
|
+
try {
|
|
648
|
+
const { password: stored, ...rest } = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
649
|
+
if (stored !== undefined) {
|
|
650
|
+
fs.writeFileSync(file, JSON.stringify(rest, null, 2));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Unreadable file: nothing to migrate there.
|
|
655
|
+
}
|
|
656
|
+
}
|
|
547
657
|
project.hasLegacyPassword = false;
|
|
548
658
|
return true;
|
|
549
659
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,233 +1,139 @@
|
|
|
1
1
|
# Sitevision CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
`svc` builds, signs and deploys Sitevision apps (WebApp, Widget, RESTApp,
|
|
4
|
+
MCPServer) from a full-screen terminal shell or as plain commands.
|
|
5
|
+
|
|
6
|
+
- **One shell for one app or a whole repo.** Run it inside an app, or at the
|
|
7
|
+
root of a repo with many apps and switch between them. Dev and watch keep
|
|
8
|
+
running in the background.
|
|
9
|
+
- **Three ways to authenticate deploys:** username and password, OAuth2 (PKCE,
|
|
10
|
+
works with SSO), or a captured browser session for SAML-only sites.
|
|
11
|
+
- **No secrets on disk.** Passwords, tokens and cookies live in the OS keychain.
|
|
12
|
+
- **Environments.** dev, test and prod in one config; production deploys use
|
|
13
|
+
the signed zip, confirm and activate.
|
|
14
|
+
- **Shared config in git.** Site and auth settings for the whole team live in
|
|
15
|
+
`package.json`, once at the repo root; your username stays in a local
|
|
16
|
+
`.dev_properties.json`. Compatible with plain sitevision-scripts.
|
|
17
|
+
- **Builds the way Sitevision does.** Bundled apps without their own webpack
|
|
18
|
+
config are built by `@sitevision/sitevision-scripts`.
|
|
19
|
+
- English and Swedish UI.
|
|
20
|
+
|
|
21
|
+
📖 **[User guide](docs/user-guide.md)** · **[Användarguide (svenska)](docs/anvandarguide.md)**
|
|
17
22
|
|
|
18
23
|
## Install
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
npm install --global sitevision-cli
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Usage
|
|
25
|
-
|
|
26
|
-
The CLI must be run inside a Sitevision project directory (containing a `manifest.json`).
|
|
27
|
-
|
|
28
|
-
### Interactive shell
|
|
29
|
-
|
|
30
|
-
Run `svc` with no arguments to open the full-screen shell. It works in two
|
|
31
|
-
places:
|
|
32
|
-
|
|
33
|
-
- **Inside an app** (a directory with `manifest.json`): single-app mode.
|
|
34
|
-
- **At the root of a repo** that contains apps in subfolders such as
|
|
35
|
-
`webapps/*`, `restapps/*` or `widgets/*`: workspace mode, with every app in
|
|
36
|
-
the left navigator and per-app status dots (dependencies, config,
|
|
37
|
-
package.json sync, signing).
|
|
38
|
-
|
|
39
|
-
The right pane has four tabs: **Overview**, **Config** (the whole
|
|
40
|
-
`.dev_properties.json` as one form, plus signing and keychain secrets; `Tab`
|
|
41
|
-
moves between fields, `Enter` saves, `Ctrl+O` on the addon field picks an
|
|
42
|
-
addon from the site's Addon Repository), **Versions** (the versions uploaded
|
|
43
|
-
to the site, `a` activates one) and **Log** (streaming build and deploy
|
|
44
|
-
output). Dev and watch keep running in the background while you navigate
|
|
45
|
-
between apps.
|
|
46
|
-
|
|
47
|
-
Single-letter keys drive everything; the bottom bar shows the ones that apply.
|
|
48
|
-
`/` opens the command palette with every action, `Tab` switches between the
|
|
49
|
-
navigator and the content pane, `1`–`4` pick a tab, `q` quits.
|
|
50
|
-
|
|
51
|
-
| Key | Action |
|
|
52
|
-
| --------------- | ----------------------------------------------------------------- |
|
|
53
|
-
| `d` / `w` | Dev (build, sign, deploy on change) / Watch (build and sign only) |
|
|
54
|
-
| `b` / `s` | Build / Sign |
|
|
55
|
-
| `p` / `P` | Deploy to dev / force deploy |
|
|
56
|
-
| `a` | Versions tab: list and activate remote versions |
|
|
57
|
-
| `e` / `y` / `l` | Edit dev properties / apply package.json sync / log in |
|
|
58
|
-
| `K` | Stop the running task for the selected app |
|
|
59
|
-
|
|
60
|
-
### Settings
|
|
61
|
-
|
|
62
|
-
`,` (or "Settings" in the palette) opens the global preferences, stored in
|
|
63
|
-
`~/.config/sitevision-cli/config.json`: the UI language (English or Swedish,
|
|
64
|
-
which also picks the manifest name language) and whether the intro animation
|
|
65
|
-
plays. In workspace mode the same screen has a row that jumps to the shared
|
|
66
|
-
workspace config.
|
|
67
|
-
|
|
68
|
-
### Environments
|
|
69
|
-
|
|
70
|
-
The top-level fields of `.dev_properties.json` are one environment, called
|
|
71
|
-
**dev** unless `baseEnvironment` says otherwise (a repo that only has a
|
|
72
|
-
production site can set `"baseEnvironment": "prod"`). Add more under
|
|
73
|
-
`environments`, overriding only what differs:
|
|
74
|
-
|
|
75
|
-
```json
|
|
76
|
-
{
|
|
77
|
-
"domain": "acme-use.sitevision-cloud.se",
|
|
78
|
-
"siteName": "Intranet",
|
|
79
|
-
"username": "me@acme.se",
|
|
80
|
-
"environments": {
|
|
81
|
-
"test": {"domain": "acme-tse.sitevision-cloud.se"},
|
|
82
|
-
"prod": {"domain": "acme.sitevision-cloud.se", "authMethod": "oauth2"}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
`E` cycles the active environment (also "Switch environment" and "Add
|
|
88
|
-
environment" in the palette); the choice is remembered in `.svcconfig`. The
|
|
89
|
-
top bar shows a badge, green for dev, yellow for others, red for production.
|
|
90
|
-
Versions, deploy, login state and the Config tab all follow the active
|
|
91
|
-
environment; on a non-dev environment the Config tab edits that environment's
|
|
92
|
-
overrides. Override names containing `prod`, or any environment with `"production":
|
|
93
|
-
true`, are production: deploy needs the signed zip, confirms, and activates,
|
|
94
|
-
and dev or watch refuse to run against them. The base environment is never
|
|
95
|
-
production by name, only by the flag, so a prod-only repo keeps its dev loop.
|
|
96
|
-
Both settings have rows in the Config tab under ENVIRONMENT.
|
|
97
|
-
|
|
98
|
-
### Shared configuration in a workspace
|
|
99
|
-
|
|
100
|
-
`.dev_properties.json` is resolved by merging every ancestor directory's file
|
|
101
|
-
(up to the repo root) under the app's own file, nearest wins. Put the shared
|
|
102
|
-
fields (`domain`, `siteName`, `username`, `authMethod`, `oauth2`,
|
|
103
|
-
`signingUsername`, ...) once at the repo root and keep only `addonName` in each
|
|
104
|
-
app. The Config tab marks inherited values with `↑ root`, and saving an app's
|
|
105
|
-
config never copies inherited values into the app file. Keychain entries are
|
|
106
|
-
keyed by domain and username, so one login covers every app on the site.
|
|
107
|
-
|
|
108
|
-
### Direct Commands
|
|
109
|
-
|
|
110
|
-
You can also run commands directly:
|
|
111
|
-
|
|
112
|
-
#### Development
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
# Start development server with watch mode
|
|
116
|
-
svc dev
|
|
117
|
-
|
|
118
|
-
# Start development server with automatic signing
|
|
119
|
-
svc dev --signed
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
#### Building
|
|
25
|
+
Requires Node.js 22+.
|
|
123
26
|
|
|
124
27
|
```bash
|
|
125
|
-
|
|
126
|
-
svc build
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
#### Signing
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
# Sign the app for production deployment
|
|
133
|
-
svc sign
|
|
28
|
+
npm install --global sitevision-cli
|
|
134
29
|
```
|
|
135
30
|
|
|
136
|
-
|
|
31
|
+
## Quick start
|
|
137
32
|
|
|
138
33
|
```bash
|
|
139
|
-
#
|
|
140
|
-
svc
|
|
141
|
-
|
|
142
|
-
# Force deploy (overwrite existing)
|
|
143
|
-
svc deploy --force
|
|
144
|
-
|
|
145
|
-
# Deploy to production (requires signed app)
|
|
146
|
-
svc deploy --production
|
|
34
|
+
cd my-repo # or cd into a single app
|
|
35
|
+
svc
|
|
147
36
|
```
|
|
148
37
|
|
|
149
|
-
|
|
38
|
+
1. Pick an app in the navigator and press `Enter`. (In a new repo the shell
|
|
39
|
+
opens on **Workspace settings** first.)
|
|
40
|
+
2. Press `2` for the **Config** tab and fill in domain, site name, addon name,
|
|
41
|
+
username and auth method. Each field saves on `Enter`.
|
|
42
|
+
3. Press `i` to install dependencies if needed, then `d` to start dev: build on
|
|
43
|
+
every change and deploy. Output is in the **Log** tab (`4`).
|
|
44
|
+
|
|
45
|
+
For production: switch environment with `E`, press `b` to build, `s` to sign
|
|
46
|
+
and `p` to deploy and activate.
|
|
47
|
+
|
|
48
|
+
## The shell
|
|
49
|
+
|
|
50
|
+
| Key | Action |
|
|
51
|
+
| --------------- | --------------------------------------------------- |
|
|
52
|
+
| `d` / `w` | Dev (build + deploy on change) / Watch (build only) |
|
|
53
|
+
| `b` / `s` | Build / Sign |
|
|
54
|
+
| `p` / `P` | Deploy / force deploy to the active environment |
|
|
55
|
+
| `a` | Versions: list and activate uploaded versions |
|
|
56
|
+
| `E` | Switch environment |
|
|
57
|
+
| `e` / `y` / `i` | Config tab / sync `package.json` / `npm install` |
|
|
58
|
+
| `l` | Log in again |
|
|
59
|
+
| `K` | Stop running tasks |
|
|
60
|
+
| `1`–`4` | Overview · Config · Versions · Log |
|
|
61
|
+
| `/` | Command palette |
|
|
62
|
+
| `,` | Settings (language, intro animation) |
|
|
63
|
+
| `Tab` / `Esc` | Switch pane / back |
|
|
64
|
+
| `q` | Quit |
|
|
65
|
+
|
|
66
|
+
In the navigator, typing filters the app list; action keys work once `Enter` or
|
|
67
|
+
`Tab` has moved focus to the content pane. The bottom bar always shows the keys
|
|
68
|
+
that apply. `svc --minimal` gives a compact layout for small panes.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
150
71
|
|
|
151
72
|
```bash
|
|
152
|
-
#
|
|
153
|
-
svc
|
|
73
|
+
svc # interactive shell
|
|
74
|
+
svc build # build to dist/<id>.zip
|
|
75
|
+
svc sign # sign to dist/<id>-signed.zip
|
|
76
|
+
svc deploy [--force] # deploy the zip
|
|
77
|
+
svc deploy --production [--activate] # deploy the signed zip
|
|
78
|
+
svc dev [--signed] # build + deploy on change
|
|
79
|
+
svc watch [--signed] # build on change, no deploy
|
|
80
|
+
svc info # project information
|
|
154
81
|
```
|
|
155
82
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
# Show project information and configuration
|
|
160
|
-
svc info
|
|
161
|
-
```
|
|
83
|
+
Direct commands use the base environment. `--token` and `--cookie` pass an
|
|
84
|
+
OAuth2 token or session cookie for one run.
|
|
162
85
|
|
|
163
86
|
## Configuration
|
|
164
87
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
```json
|
|
170
|
-
{
|
|
171
|
-
"domain": "your-site.sitevision.se",
|
|
172
|
-
"siteName": "YourSite",
|
|
173
|
-
"addonName": "your-addon",
|
|
174
|
-
"username": "your-email@example.com",
|
|
175
|
-
"useHTTPForDevDeploy": false,
|
|
176
|
-
"signingUsername": "your-developer-account@example.com",
|
|
177
|
-
"certificateName": "optional-certificate-name"
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Keeping `package.json` in sync
|
|
182
|
-
|
|
183
|
-
`sitevision-scripts` reads `developmentDomain`, `siteName` and `addonName`
|
|
184
|
-
from `package.json`, which duplicates three fields of
|
|
185
|
-
`.dev_properties.json`. When they disagree — or when a fresh setup has just
|
|
186
|
-
written `.dev_properties.json` — `svc` shows the differences and offers to
|
|
187
|
-
update `package.json` from `.dev_properties.json`. Nothing is written without
|
|
188
|
-
confirmation, and `.dev_properties.json` is always the source of truth for
|
|
189
|
-
the copy. Existing indentation and unrelated fields are left alone.
|
|
190
|
-
|
|
191
|
-
After answering, `svc` offers to remember the choice in a `.svcconfig` file
|
|
192
|
-
in the project root:
|
|
88
|
+
`.dev_properties.json` is your local config and the main source; keep it out of
|
|
89
|
+
git. Shared values are committed in `package.json` and act as defaults
|
|
90
|
+
underneath it:
|
|
193
91
|
|
|
194
92
|
```json
|
|
195
93
|
{
|
|
196
|
-
"
|
|
94
|
+
"developmentDomain": "acme-use.sitevision-cloud.se",
|
|
95
|
+
"siteName": "Intranet",
|
|
96
|
+
"addonName": "my-addon",
|
|
97
|
+
"svc": {
|
|
98
|
+
"authMethod": "oauth2",
|
|
99
|
+
"environments": {"prod": {"domain": "acme.sitevision-cloud.se"}}
|
|
100
|
+
}
|
|
197
101
|
}
|
|
198
102
|
```
|
|
199
103
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
104
|
+
`username`, `signingUsername` and `certificateName` are user-specific and only
|
|
105
|
+
live in `.dev_properties.json`. `y` copies shared values from
|
|
106
|
+
`.dev_properties.json` into `package.json`. In a workspace, shared values go in
|
|
107
|
+
the root `package.json` and each app's `package.json` only needs `addonName`.
|
|
203
108
|
|
|
204
|
-
|
|
109
|
+
## Authentication in short
|
|
205
110
|
|
|
206
|
-
|
|
207
|
-
Credential Manager, Linux libsecret) under the `sitevision-cli` service —
|
|
208
|
-
never in `.dev_properties.json`. Run `svc` and complete the setup form (or
|
|
209
|
-
enter the password when prompted at deploy/sign time and toggle "save to
|
|
210
|
-
keychain") to populate it.
|
|
111
|
+
There are two separate credentials:
|
|
211
112
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
113
|
+
- **Deploy**: your account on the site. `authMethod` is `basic` (password),
|
|
114
|
+
`oauth2` (browser login against the site's OAuth2 provider, refreshed
|
|
115
|
+
silently afterwards) or `cookie` (log in with SSO in a Chrome window, the
|
|
116
|
+
session is captured).
|
|
117
|
+
- **Signing**: your developer.sitevision.se account, always username and
|
|
118
|
+
password.
|
|
217
119
|
|
|
218
|
-
|
|
219
|
-
`SITEVISION_SIGNING_PASSWORD
|
|
220
|
-
|
|
120
|
+
Everything secret goes in the OS keychain under `sitevision-cli`. For CI, set
|
|
121
|
+
`SITEVISION_DEPLOY_PASSWORD`, `SITEVISION_SIGNING_PASSWORD`,
|
|
122
|
+
`SITEVISION_ACCESS_TOKEN` or `SITEVISION_SESSION_COOKIE`.
|
|
221
123
|
|
|
222
|
-
|
|
124
|
+
OAuth2 needs a client registered on the site with the redirect URI
|
|
125
|
+
`http://127.0.0.1:8137/callback`. The [user guide](docs/user-guide.md#5-authentication)
|
|
126
|
+
covers the setup, the pitfalls, and which method works with which command.
|
|
223
127
|
|
|
224
|
-
|
|
128
|
+
## Development
|
|
225
129
|
|
|
226
|
-
|
|
227
|
-
|
|
130
|
+
```bash
|
|
131
|
+
npm install
|
|
132
|
+
npm run build # tsc → dist/
|
|
133
|
+
npm test # prettier, xo, ava
|
|
134
|
+
```
|
|
228
135
|
|
|
229
|
-
|
|
230
|
-
the OS keychain for future runs.
|
|
136
|
+
Releasing: see [RELEASING.md](RELEASING.md).
|
|
231
137
|
|
|
232
138
|
## License
|
|
233
139
|
|