sitevision-cli 1.0.0-beta.3 → 1.0.0-beta.5
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/app.d.ts +1 -1
- package/dist/app.js +22 -6
- package/dist/commands/dev.js +23 -3
- package/dist/components/SetupFlow.d.ts +2 -1
- package/dist/components/SetupFlow.js +65 -9
- package/dist/utils/jsonc.d.ts +19 -0
- package/dist/utils/jsonc.js +74 -0
- package/dist/utils/project-detection.d.ts +34 -0
- package/dist/utils/project-detection.js +118 -25
- package/package.json +1 -1
- package/readme.md +23 -0
package/dist/app.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import { type ProjectInfo } from './utils/project-detection.js';
|
|
|
2
2
|
type Props = {
|
|
3
3
|
project: ProjectInfo;
|
|
4
4
|
};
|
|
5
|
-
export default function App({ project }: Props): import("react").JSX.Element | null;
|
|
5
|
+
export default function App({ project: initialProject }: Props): import("react").JSX.Element | null;
|
|
6
6
|
export {};
|
package/dist/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { detectProject } from './utils/project-detection.js';
|
|
3
4
|
import { MainMenu } from './components/MainMenu.js';
|
|
4
5
|
import { InfoScreen } from './components/InfoScreen.js';
|
|
5
6
|
import { SetupFlow } from './components/SetupFlow.js';
|
|
@@ -12,7 +13,22 @@ import { DeployScreen } from './commands/deploy.js';
|
|
|
12
13
|
import { SignScreen } from './commands/sign.js';
|
|
13
14
|
import { SigningPropertiesForm } from './components/SigningPropertiesForm.js';
|
|
14
15
|
import { getSigningPassword, setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword, } from './utils/keychain.js';
|
|
15
|
-
export default function App({ project }) {
|
|
16
|
+
export default function App({ project: initialProject }) {
|
|
17
|
+
// The project is loaded once at startup, but setup flows write new values to
|
|
18
|
+
// disk and the OS keychain. Hold it in state so we can re-detect after setup
|
|
19
|
+
// and pick up those changes (e.g. saved passwords) without restarting the CLI.
|
|
20
|
+
const [project, setProject] = useState(initialProject);
|
|
21
|
+
const reloadProject = useCallback(() => {
|
|
22
|
+
try {
|
|
23
|
+
const refreshed = detectProject(initialProject.root);
|
|
24
|
+
if (refreshed)
|
|
25
|
+
setProject(refreshed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Re-detection failed (e.g. manifest became unparseable mid-session) —
|
|
29
|
+
// keep the existing in-memory project rather than crashing.
|
|
30
|
+
}
|
|
31
|
+
}, [initialProject.root]);
|
|
16
32
|
const [state, setState] = useState('setup');
|
|
17
33
|
const [currentCommand, setCurrentCommand] = useState('');
|
|
18
34
|
const [signingPassword, setSigningPassword] = useState('');
|
|
@@ -185,7 +201,7 @@ export default function App({ project }) {
|
|
|
185
201
|
}
|
|
186
202
|
};
|
|
187
203
|
if (state === 'setup') {
|
|
188
|
-
return _jsx(SetupFlow, { project: project, onComplete: () => setState('menu') });
|
|
204
|
+
return (_jsx(SetupFlow, { project: project, onReload: reloadProject, onComplete: () => setState('menu') }));
|
|
189
205
|
}
|
|
190
206
|
if (state === 'menu') {
|
|
191
207
|
return _jsx(MainMenu, { project: project, onSelect: handleCommandSelect });
|
|
@@ -260,9 +276,9 @@ export default function App({ project }) {
|
|
|
260
276
|
}
|
|
261
277
|
if (state === 'setup-signing') {
|
|
262
278
|
return (_jsx(SigningPropertiesForm, { projectRoot: project.root, onComplete: () => {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
279
|
+
// Re-detect so the newly written signing credentials are reflected
|
|
280
|
+
// in memory (hasSigningProperties, keychain password) without a restart.
|
|
281
|
+
reloadProject();
|
|
266
282
|
setState('menu');
|
|
267
283
|
}, onCancel: () => setState('menu') }));
|
|
268
284
|
}
|
package/dist/commands/dev.js
CHANGED
|
@@ -11,9 +11,10 @@ import { signApp, deployApp } from '../utils/sitevision-api.js';
|
|
|
11
11
|
import { setDeployPassword } from '../utils/keychain.js';
|
|
12
12
|
import { resolveSigningPassword } from '../utils/signing-password.js';
|
|
13
13
|
import { copyStaticToBuild, createBuildZip, cleanBuild } from '../utils/zip.js';
|
|
14
|
-
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, } from '../utils/project-detection.js';
|
|
14
|
+
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, readManifest, } from '../utils/project-detection.js';
|
|
15
15
|
export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy = true, signingCredentials, onBack, onRetryCredentials, }) {
|
|
16
16
|
const { exit } = useApp();
|
|
17
|
+
const [version, setVersion] = React.useState(manifest.version);
|
|
17
18
|
const [state, setState] = React.useState({
|
|
18
19
|
status: 'initializing',
|
|
19
20
|
message: 'Starting webpack watch...',
|
|
@@ -34,6 +35,16 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
|
|
|
34
35
|
const isBuildingRef = React.useRef(false);
|
|
35
36
|
const pendingRebuildRef = React.useRef(false);
|
|
36
37
|
// Sign (if needed) and deploy an already-built zip, updating UI state.
|
|
38
|
+
const refreshVersion = React.useCallback(() => {
|
|
39
|
+
try {
|
|
40
|
+
const fresh = readManifest(projectRoot)?.manifest.version;
|
|
41
|
+
if (fresh)
|
|
42
|
+
setVersion(fresh);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// keep last known version
|
|
46
|
+
}
|
|
47
|
+
}, [projectRoot]);
|
|
37
48
|
const signAndDeploy = React.useCallback(async (zipPath, buildTime) => {
|
|
38
49
|
try {
|
|
39
50
|
let deployZipPath = zipPath;
|
|
@@ -57,6 +68,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
|
|
|
57
68
|
}
|
|
58
69
|
deployZipPath = signedZipPath;
|
|
59
70
|
}
|
|
71
|
+
refreshVersion();
|
|
60
72
|
// Watch/build-only mode: stop after building (and signing).
|
|
61
73
|
if (!deploy || !devProperties) {
|
|
62
74
|
setState(prev => ({
|
|
@@ -113,7 +125,15 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
|
|
|
113
125
|
error: error instanceof Error ? error.message : String(error),
|
|
114
126
|
}));
|
|
115
127
|
}
|
|
116
|
-
}, [
|
|
128
|
+
}, [
|
|
129
|
+
projectRoot,
|
|
130
|
+
manifest,
|
|
131
|
+
devProperties,
|
|
132
|
+
signed,
|
|
133
|
+
deploy,
|
|
134
|
+
signingCredentials,
|
|
135
|
+
refreshVersion,
|
|
136
|
+
]);
|
|
117
137
|
// In-house webpack path: copy static, zip, then sign + deploy.
|
|
118
138
|
const handleBuildComplete = React.useCallback(async (result) => {
|
|
119
139
|
if (!result.success) {
|
|
@@ -353,7 +373,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
|
|
|
353
373
|
};
|
|
354
374
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
|
|
355
375
|
? ` | Last build: ${state.lastBuildTime}ms`
|
|
356
|
-
: '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [localizedText(manifest.name), " v",
|
|
376
|
+
: '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [localizedText(manifest.name), " v", version] }) }), deploy && devProperties && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Target: ", devProperties.domain, "/", devProperties.siteName, "/", devProperties.addonName] }) })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack ? (_jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu (Ctrl+C to stop process)" })) : (_jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop" }))] })] }));
|
|
357
377
|
}
|
|
358
378
|
export const devCommand = {
|
|
359
379
|
name: 'dev',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ProjectInfo } from '../utils/project-detection.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
project: ProjectInfo;
|
|
4
|
+
onReload: () => void;
|
|
4
5
|
onComplete: () => void;
|
|
5
6
|
}
|
|
6
|
-
export declare function SetupFlow({ project, onComplete }: Props): import("react").JSX.Element | null;
|
|
7
|
+
export declare function SetupFlow({ project, onReload, onComplete }: Props): import("react").JSX.Element | null;
|
|
7
8
|
export {};
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { getAppType, localizedText, migrateLegacyPassword, } from '../utils/project-detection.js';
|
|
4
|
+
import { getAppType, localizedText, migrateLegacyPassword, getPackageJsonSyncChanges, syncDevPropertiesToPackageJson, readSvcConfig, writeSvcConfig, } from '../utils/project-detection.js';
|
|
5
5
|
import { ProcessRunner } from '../utils/process-runner.js';
|
|
6
6
|
import { ProcessOutputComponent } from './ProcessOutput.js';
|
|
7
7
|
import { StatusIndicator } from './StatusIndicator.js';
|
|
8
8
|
import { DevPropertiesForm } from './DevPropertiesForm.js';
|
|
9
9
|
import { SigningPropertiesForm } from './SigningPropertiesForm.js';
|
|
10
|
-
export function SetupFlow({ project, onComplete }) {
|
|
10
|
+
export function SetupFlow({ project, onReload, onComplete }) {
|
|
11
11
|
const [step, setStep] = useState('check-node-modules');
|
|
12
12
|
const [runner, setRunner] = useState(null);
|
|
13
13
|
const [commandStatus, setCommandStatus] = useState('running');
|
|
14
|
+
const [syncChanges, setSyncChanges] = useState([]);
|
|
15
|
+
const [syncDecision, setSyncDecision] = useState(false);
|
|
14
16
|
const appType = getAppType(project.manifest);
|
|
15
17
|
// Auto-advance through checks
|
|
16
18
|
useEffect(() => {
|
|
@@ -28,13 +30,32 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
28
30
|
setStep('confirm-password-migration');
|
|
29
31
|
}
|
|
30
32
|
else {
|
|
31
|
-
setStep('check-
|
|
33
|
+
setStep('check-package-sync');
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
else {
|
|
35
37
|
setStep('confirm-dev-setup');
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
else if (step === 'check-package-sync') {
|
|
41
|
+
const preference = readSvcConfig(project.root).syncPackageJson;
|
|
42
|
+
const properties = project.devProperties;
|
|
43
|
+
const changes = preference !== false && properties
|
|
44
|
+
? getPackageJsonSyncChanges(project.root, properties)
|
|
45
|
+
: [];
|
|
46
|
+
if (changes.length === 0 || !properties) {
|
|
47
|
+
setStep('check-signing-properties');
|
|
48
|
+
}
|
|
49
|
+
else if (preference === true) {
|
|
50
|
+
syncDevPropertiesToPackageJson(project.root, properties);
|
|
51
|
+
onReload();
|
|
52
|
+
setStep('check-signing-properties');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
setSyncChanges(changes);
|
|
56
|
+
setStep('confirm-package-sync');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
38
59
|
else if (step === 'check-signing-properties') {
|
|
39
60
|
if (project.hasSigningProperties) {
|
|
40
61
|
setStep('show-info');
|
|
@@ -84,6 +105,29 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
84
105
|
else if (step === 'confirm-password-migration') {
|
|
85
106
|
if (input === 'y' || input === 'Y') {
|
|
86
107
|
migrateLegacyPassword(project);
|
|
108
|
+
// The file was rewritten (plaintext stripped, password moved to
|
|
109
|
+
// keychain) — re-detect so hasLegacyPassword/password reflect that.
|
|
110
|
+
onReload();
|
|
111
|
+
setStep('check-package-sync');
|
|
112
|
+
}
|
|
113
|
+
else if (input === 'n' || input === 'N') {
|
|
114
|
+
setStep('check-package-sync');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (step === 'confirm-package-sync') {
|
|
118
|
+
if (['y', 'Y', 'n', 'N'].includes(input)) {
|
|
119
|
+
const accepted = input.toLowerCase() === 'y';
|
|
120
|
+
if (accepted && project.devProperties) {
|
|
121
|
+
syncDevPropertiesToPackageJson(project.root, project.devProperties);
|
|
122
|
+
onReload();
|
|
123
|
+
}
|
|
124
|
+
setSyncDecision(accepted);
|
|
125
|
+
setStep('confirm-save-sync-choice');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (step === 'confirm-save-sync-choice') {
|
|
129
|
+
if (input === 'y' || input === 'Y') {
|
|
130
|
+
writeSvcConfig(project.root, { syncPackageJson: syncDecision });
|
|
87
131
|
setStep('check-signing-properties');
|
|
88
132
|
}
|
|
89
133
|
else if (input === 'n' || input === 'N') {
|
|
@@ -102,17 +146,19 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
102
146
|
// Setup Dev Properties Form
|
|
103
147
|
if (step === 'setup-dev-properties') {
|
|
104
148
|
return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties: project.devProperties, packageJson: project.packageJson, onComplete: () => {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
setStep('check-
|
|
149
|
+
// Re-detect from disk/keychain so devProperties (incl. the keychain
|
|
150
|
+
// password) populate in memory — otherwise the rest of this flow and
|
|
151
|
+
// the menu would see stale state until the CLI is restarted.
|
|
152
|
+
onReload();
|
|
153
|
+
setStep('check-package-sync');
|
|
110
154
|
}, onCancel: () => setStep('check-signing-properties') }));
|
|
111
155
|
}
|
|
112
156
|
// Setup Signing Properties Form
|
|
113
157
|
if (step === 'setup-signing-properties') {
|
|
114
158
|
return (_jsx(SigningPropertiesForm, { projectRoot: project.root, onComplete: () => {
|
|
115
|
-
|
|
159
|
+
// Re-detect so signing credentials are reflected in memory before
|
|
160
|
+
// the info screen / menu render.
|
|
161
|
+
onReload();
|
|
116
162
|
setStep('show-info');
|
|
117
163
|
}, onCancel: () => setStep('show-info') }));
|
|
118
164
|
}
|
|
@@ -132,6 +178,16 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
132
178
|
if (step === 'confirm-password-migration') {
|
|
133
179
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 Plaintext password found in .dev_properties.json" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Move it to the OS keychain and remove it from the file? (y/n)" }), _jsx(Text, { dimColor: true, children: "Recommended \u2014 storing passwords in project files is insecure." })] })] }));
|
|
134
180
|
}
|
|
181
|
+
// Confirm package.json sync
|
|
182
|
+
if (step === 'confirm-package-sync') {
|
|
183
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 package.json is out of sync with .dev_properties.json" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", marginLeft: 2, children: syncChanges.map(change => (_jsxs(Box, { children: [_jsx(Text, { color: change.from === undefined ? 'green' : 'yellow', children: change.from === undefined ? '+ ' : '~ ' }), _jsxs(Text, { bold: true, children: [change.key, ": "] }), change.from !== undefined && (_jsxs(Text, { dimColor: true, children: [change.from, " \u2192 "] })), _jsx(Text, { children: change.to })] }, change.key))) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Update package.json from .dev_properties.json? (y/n)" }), _jsx(Text, { dimColor: true, children: "sitevision-scripts reads these fields from package.json." })] })] }));
|
|
184
|
+
}
|
|
185
|
+
// Offer to persist the sync decision in .svcconfig
|
|
186
|
+
if (step === 'confirm-save-sync-choice') {
|
|
187
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Remember this choice in .svcconfig? (y/n)" }), _jsx(Text, { dimColor: true, children: syncDecision
|
|
188
|
+
? 'svc will update package.json automatically from now on.'
|
|
189
|
+
: 'svc will stop asking about package.json sync.' })] })] }));
|
|
190
|
+
}
|
|
135
191
|
// Confirm signing setup
|
|
136
192
|
if (step === 'confirm-signing-setup') {
|
|
137
193
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 signing credentials not configured" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Signing credentials are required for signing apps on developer.sitevision.se" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Would you like to set up signing credentials? (y/n)" }) })] }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONC helpers.
|
|
3
|
+
*
|
|
4
|
+
* Sitevision's own manifest documentation shows manifest.json with line and
|
|
5
|
+
* block comments (e.g. `"name": { // Multilingual-manifest requires SV 10.1`),
|
|
6
|
+
* so real-world manifests copied from the docs contain them. Strict JSON.parse
|
|
7
|
+
* rejects those, so we strip comments before parsing.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Remove line comments (//...) and block comments from a JSON string, leaving
|
|
11
|
+
* everything inside string literals untouched (so values like
|
|
12
|
+
* `"https://example.com"` survive).
|
|
13
|
+
*/
|
|
14
|
+
export declare function stripJsonComments(input: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a JSON string that may contain comments (JSONC). Throws the underlying
|
|
17
|
+
* SyntaxError if the content is invalid even after comments are removed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseJsonc<T>(input: string): T;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONC helpers.
|
|
3
|
+
*
|
|
4
|
+
* Sitevision's own manifest documentation shows manifest.json with line and
|
|
5
|
+
* block comments (e.g. `"name": { // Multilingual-manifest requires SV 10.1`),
|
|
6
|
+
* so real-world manifests copied from the docs contain them. Strict JSON.parse
|
|
7
|
+
* rejects those, so we strip comments before parsing.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Remove line comments (//...) and block comments from a JSON string, leaving
|
|
11
|
+
* everything inside string literals untouched (so values like
|
|
12
|
+
* `"https://example.com"` survive).
|
|
13
|
+
*/
|
|
14
|
+
export function stripJsonComments(input) {
|
|
15
|
+
let result = '';
|
|
16
|
+
let inString = false;
|
|
17
|
+
let inLineComment = false;
|
|
18
|
+
let inBlockComment = false;
|
|
19
|
+
for (let i = 0; i < input.length; i++) {
|
|
20
|
+
const char = input[i];
|
|
21
|
+
const next = input[i + 1];
|
|
22
|
+
if (inLineComment) {
|
|
23
|
+
if (char === '\n') {
|
|
24
|
+
inLineComment = false;
|
|
25
|
+
result += char;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (inBlockComment) {
|
|
30
|
+
if (char === '*' && next === '/') {
|
|
31
|
+
inBlockComment = false;
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (inString) {
|
|
37
|
+
result += char;
|
|
38
|
+
// Copy escaped characters verbatim so an escaped quote (\") does not
|
|
39
|
+
// end the string early.
|
|
40
|
+
if (char === '\\') {
|
|
41
|
+
result += next ?? '';
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
else if (char === '"') {
|
|
45
|
+
inString = false;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"') {
|
|
50
|
+
inString = true;
|
|
51
|
+
result += char;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '/' && next === '/') {
|
|
55
|
+
inLineComment = true;
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (char === '/' && next === '*') {
|
|
60
|
+
inBlockComment = true;
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
result += char;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse a JSON string that may contain comments (JSONC). Throws the underlying
|
|
70
|
+
* SyntaxError if the content is invalid even after comments are removed.
|
|
71
|
+
*/
|
|
72
|
+
export function parseJsonc(input) {
|
|
73
|
+
return JSON.parse(stripJsonComments(input));
|
|
74
|
+
}
|
|
@@ -69,6 +69,14 @@ export declare function buildImportEndpointUrl(domain: string, siteName: string,
|
|
|
69
69
|
export declare class ManifestParseError extends Error {
|
|
70
70
|
constructor(manifestPath: string, cause: unknown);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Read manifest.json from its supported locations (root, static/, src/).
|
|
74
|
+
* Throws ManifestParseError on malformed JSON.
|
|
75
|
+
*/
|
|
76
|
+
export declare function readManifest(cwd: string): {
|
|
77
|
+
manifestPath: string;
|
|
78
|
+
manifest: SitevisionManifest;
|
|
79
|
+
} | null;
|
|
72
80
|
/**
|
|
73
81
|
* Detect if the current directory is a Sitevision project
|
|
74
82
|
*/
|
|
@@ -94,6 +102,32 @@ export declare function readDevProperties(projectRoot: string): DevProperties |
|
|
|
94
102
|
* it is held in the OS keychain instead.
|
|
95
103
|
*/
|
|
96
104
|
export declare function writeDevProperties(projectRoot: string, properties: DevProperties): void;
|
|
105
|
+
/**
|
|
106
|
+
* CLI preferences stored in .svcconfig at the project root. Unknown keys are
|
|
107
|
+
* preserved on write so hand-edited entries survive.
|
|
108
|
+
*/
|
|
109
|
+
export interface SvcConfig {
|
|
110
|
+
syncPackageJson?: boolean;
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
113
|
+
export declare function readSvcConfig(projectRoot: string): SvcConfig;
|
|
114
|
+
export declare function writeSvcConfig(projectRoot: string, updates: SvcConfig): void;
|
|
115
|
+
export interface PackageJsonSyncChange {
|
|
116
|
+
key: string;
|
|
117
|
+
from?: string;
|
|
118
|
+
to: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Which of the shared fields package.json is missing or disagrees on, relative
|
|
122
|
+
* to the given dev properties. Reads package.json from disk — an earlier
|
|
123
|
+
* `npm install` in the same session may have rewritten it.
|
|
124
|
+
*/
|
|
125
|
+
export declare function getPackageJsonSyncChanges(projectRoot: string, properties: DevProperties): PackageJsonSyncChange[];
|
|
126
|
+
/**
|
|
127
|
+
* Copy the shared fields from dev properties into package.json, preserving the
|
|
128
|
+
* file's existing indentation and trailing newline.
|
|
129
|
+
*/
|
|
130
|
+
export declare function syncDevPropertiesToPackageJson(projectRoot: string, properties: DevProperties): boolean;
|
|
97
131
|
/**
|
|
98
132
|
* Move a plaintext password from .dev_properties.json into the OS keychain and
|
|
99
133
|
* strip it from the file. Returns true if the password was migrated.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getDeployPassword, setDeployPassword } from './keychain.js';
|
|
4
|
+
import { parseJsonc } from './jsonc.js';
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// LOCALIZED TEXT
|
|
6
7
|
// =============================================================================
|
|
@@ -166,38 +167,44 @@ export class ManifestParseError extends Error {
|
|
|
166
167
|
this.name = 'ManifestParseError';
|
|
167
168
|
}
|
|
168
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Read manifest.json from its supported locations (root, static/, src/).
|
|
172
|
+
* Throws ManifestParseError on malformed JSON.
|
|
173
|
+
*/
|
|
174
|
+
export function readManifest(cwd) {
|
|
175
|
+
const manifestPaths = [
|
|
176
|
+
path.join(cwd, 'manifest.json'),
|
|
177
|
+
path.join(cwd, 'static', 'manifest.json'),
|
|
178
|
+
path.join(cwd, 'src', 'manifest.json'),
|
|
179
|
+
];
|
|
180
|
+
for (const manifestPath of manifestPaths) {
|
|
181
|
+
if (!fs.existsSync(manifestPath)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Manifests may contain comments (Sitevision's own docs show them), so
|
|
185
|
+
// parse as JSONC.
|
|
186
|
+
try {
|
|
187
|
+
return {
|
|
188
|
+
manifestPath,
|
|
189
|
+
manifest: parseJsonc(fs.readFileSync(manifestPath, 'utf-8')),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw new ManifestParseError(manifestPath, error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
169
198
|
/**
|
|
170
199
|
* Detect if the current directory is a Sitevision project
|
|
171
200
|
*/
|
|
172
201
|
export function detectProject(cwd = process.cwd()) {
|
|
173
202
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
path.join(cwd, 'manifest.json'),
|
|
177
|
-
path.join(cwd, 'static', 'manifest.json'),
|
|
178
|
-
path.join(cwd, 'src', 'manifest.json'),
|
|
179
|
-
];
|
|
180
|
-
let manifestPath = null;
|
|
181
|
-
let manifest = null;
|
|
182
|
-
for (const p of manifestPaths) {
|
|
183
|
-
if (fs.existsSync(p)) {
|
|
184
|
-
manifestPath = p;
|
|
185
|
-
// A present-but-unparseable manifest is a real, fixable error (a stray
|
|
186
|
-
// comment, a trailing comma, …). Surface it rather than silently
|
|
187
|
-
// reporting "Not a Sitevision project". JSON has no comments — strip
|
|
188
|
-
// any `//` annotations from manifest.json.
|
|
189
|
-
try {
|
|
190
|
-
manifest = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
throw new ManifestParseError(p, error);
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (!manifest || !manifestPath) {
|
|
203
|
+
const found = readManifest(cwd);
|
|
204
|
+
if (!found) {
|
|
199
205
|
return null;
|
|
200
206
|
}
|
|
207
|
+
const { manifestPath, manifest } = found;
|
|
201
208
|
// Check for package.json
|
|
202
209
|
const packageJsonPath = path.join(cwd, 'package.json');
|
|
203
210
|
if (!fs.existsSync(packageJsonPath)) {
|
|
@@ -325,6 +332,92 @@ export function writeDevProperties(projectRoot, properties) {
|
|
|
325
332
|
const { password: _password, ...persisted } = properties;
|
|
326
333
|
fs.writeFileSync(devPropertiesPath, JSON.stringify(persisted, null, 2));
|
|
327
334
|
}
|
|
335
|
+
export function readSvcConfig(projectRoot) {
|
|
336
|
+
try {
|
|
337
|
+
return parseJsonc(fs.readFileSync(path.join(projectRoot, '.svcconfig'), 'utf-8'));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export function writeSvcConfig(projectRoot, updates) {
|
|
344
|
+
const merged = { ...readSvcConfig(projectRoot), ...updates };
|
|
345
|
+
fs.writeFileSync(path.join(projectRoot, '.svcconfig'), JSON.stringify(merged, null, 2) + '\n');
|
|
346
|
+
}
|
|
347
|
+
// =============================================================================
|
|
348
|
+
// PACKAGE.JSON SYNC
|
|
349
|
+
// =============================================================================
|
|
350
|
+
/**
|
|
351
|
+
* Fields duplicated between .dev_properties.json and package.json, where
|
|
352
|
+
* sitevision-scripts reads them under different names.
|
|
353
|
+
*/
|
|
354
|
+
const PACKAGE_JSON_SYNC_KEYS = [
|
|
355
|
+
{ packageKey: 'developmentDomain', devKey: 'domain' },
|
|
356
|
+
{ packageKey: 'siteName', devKey: 'siteName' },
|
|
357
|
+
{ packageKey: 'addonName', devKey: 'addonName' },
|
|
358
|
+
];
|
|
359
|
+
function readPackageJson(projectRoot) {
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Which of the shared fields package.json is missing or disagrees on, relative
|
|
369
|
+
* to the given dev properties. Reads package.json from disk — an earlier
|
|
370
|
+
* `npm install` in the same session may have rewritten it.
|
|
371
|
+
*/
|
|
372
|
+
export function getPackageJsonSyncChanges(projectRoot, properties) {
|
|
373
|
+
const packageJson = readPackageJson(projectRoot);
|
|
374
|
+
if (!packageJson)
|
|
375
|
+
return [];
|
|
376
|
+
const changes = [];
|
|
377
|
+
for (const { packageKey, devKey } of PACKAGE_JSON_SYNC_KEYS) {
|
|
378
|
+
const to = properties[devKey];
|
|
379
|
+
if (typeof to !== 'string' || to === '')
|
|
380
|
+
continue;
|
|
381
|
+
const from = packageJson[packageKey];
|
|
382
|
+
if (from !== to) {
|
|
383
|
+
changes.push(from === undefined
|
|
384
|
+
? { key: packageKey, to }
|
|
385
|
+
: { key: packageKey, from, to });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return changes;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Copy the shared fields from dev properties into package.json, preserving the
|
|
392
|
+
* file's existing indentation and trailing newline.
|
|
393
|
+
*/
|
|
394
|
+
export function syncDevPropertiesToPackageJson(projectRoot, properties) {
|
|
395
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
396
|
+
let raw;
|
|
397
|
+
try {
|
|
398
|
+
raw = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
let packageJson;
|
|
404
|
+
try {
|
|
405
|
+
packageJson = JSON.parse(raw);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
for (const { packageKey, devKey } of PACKAGE_JSON_SYNC_KEYS) {
|
|
411
|
+
const value = properties[devKey];
|
|
412
|
+
if (typeof value === 'string' && value !== '') {
|
|
413
|
+
packageJson[packageKey] = value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const indent = /^(?<indent>[\t ]+)/m.exec(raw)?.groups?.['indent'] ?? '\t';
|
|
417
|
+
const newline = raw.endsWith('\n') ? '\n' : '';
|
|
418
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, indent) + newline);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
328
421
|
/**
|
|
329
422
|
* Move a plaintext password from .dev_properties.json into the OS keychain and
|
|
330
423
|
* strip it from the file. Returns true if the password was migrated.
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -126,6 +126,29 @@ Create this file in your project root for deployment configuration:
|
|
|
126
126
|
}
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
### Keeping `package.json` in sync
|
|
130
|
+
|
|
131
|
+
`sitevision-scripts` reads `developmentDomain`, `siteName` and `addonName`
|
|
132
|
+
from `package.json`, which duplicates three fields of
|
|
133
|
+
`.dev_properties.json`. When they disagree — or when a fresh setup has just
|
|
134
|
+
written `.dev_properties.json` — `svc` shows the differences and offers to
|
|
135
|
+
update `package.json` from `.dev_properties.json`. Nothing is written without
|
|
136
|
+
confirmation, and `.dev_properties.json` is always the source of truth for
|
|
137
|
+
the copy. Existing indentation and unrelated fields are left alone.
|
|
138
|
+
|
|
139
|
+
After answering, `svc` offers to remember the choice in a `.svcconfig` file
|
|
140
|
+
in the project root:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"syncPackageJson": true
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
With `true`, `svc` updates `package.json` automatically without asking; with
|
|
149
|
+
`false`, the check is skipped entirely. Delete the key (or the file) to be
|
|
150
|
+
asked again. The file contains no secrets, so it is safe to commit.
|
|
151
|
+
|
|
129
152
|
### Password storage
|
|
130
153
|
|
|
131
154
|
Passwords are stored in the OS-native secret store (macOS Keychain, Windows
|