sitevision-cli 1.0.0-beta.4 → 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.
@@ -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
- }, [projectRoot, manifest, devProperties, signed, deploy, signingCredentials]);
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", manifest.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" }))] })] }));
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,7 @@
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';
@@ -11,6 +11,8 @@ 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, onReload, onComplete }) {
28
30
  setStep('confirm-password-migration');
29
31
  }
30
32
  else {
31
- setStep('check-signing-properties');
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');
@@ -87,6 +108,26 @@ export function SetupFlow({ project, onReload, onComplete }) {
87
108
  // The file was rewritten (plaintext stripped, password moved to
88
109
  // keychain) — re-detect so hasLegacyPassword/password reflect that.
89
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 });
90
131
  setStep('check-signing-properties');
91
132
  }
92
133
  else if (input === 'n' || input === 'N') {
@@ -109,7 +150,7 @@ export function SetupFlow({ project, onReload, onComplete }) {
109
150
  // password) populate in memory — otherwise the rest of this flow and
110
151
  // the menu would see stale state until the CLI is restarted.
111
152
  onReload();
112
- setStep('check-signing-properties');
153
+ setStep('check-package-sync');
113
154
  }, onCancel: () => setStep('check-signing-properties') }));
114
155
  }
115
156
  // Setup Signing Properties Form
@@ -137,6 +178,16 @@ export function SetupFlow({ project, onReload, onComplete }) {
137
178
  if (step === 'confirm-password-migration') {
138
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." })] })] }));
139
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
+ }
140
191
  // Confirm signing setup
141
192
  if (step === 'confirm-signing-setup') {
142
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)" }) })] }));
@@ -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.
@@ -167,38 +167,44 @@ export class ManifestParseError extends Error {
167
167
  this.name = 'ManifestParseError';
168
168
  }
169
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
+ }
170
198
  /**
171
199
  * Detect if the current directory is a Sitevision project
172
200
  */
173
201
  export function detectProject(cwd = process.cwd()) {
174
202
  try {
175
- // Look for manifest.json in multiple locations (current, static/, src/)
176
- const manifestPaths = [
177
- path.join(cwd, 'manifest.json'),
178
- path.join(cwd, 'static', 'manifest.json'),
179
- path.join(cwd, 'src', 'manifest.json'),
180
- ];
181
- let manifestPath = null;
182
- let manifest = null;
183
- for (const p of manifestPaths) {
184
- if (fs.existsSync(p)) {
185
- manifestPath = p;
186
- // Manifests may contain comments (Sitevision's own docs show them), so
187
- // parse as JSONC. A still-unparseable manifest is a real, fixable
188
- // error — surface it rather than silently reporting "Not a Sitevision
189
- // project".
190
- try {
191
- manifest = parseJsonc(fs.readFileSync(p, 'utf-8'));
192
- }
193
- catch (error) {
194
- throw new ManifestParseError(p, error);
195
- }
196
- break;
197
- }
198
- }
199
- if (!manifest || !manifestPath) {
203
+ const found = readManifest(cwd);
204
+ if (!found) {
200
205
  return null;
201
206
  }
207
+ const { manifestPath, manifest } = found;
202
208
  // Check for package.json
203
209
  const packageJsonPath = path.join(cwd, 'package.json');
204
210
  if (!fs.existsSync(packageJsonPath)) {
@@ -326,6 +332,92 @@ export function writeDevProperties(projectRoot, properties) {
326
332
  const { password: _password, ...persisted } = properties;
327
333
  fs.writeFileSync(devPropertiesPath, JSON.stringify(persisted, null, 2));
328
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
+ }
329
421
  /**
330
422
  * Move a plaintext password from .dev_properties.json into the OS keychain and
331
423
  * strip it from the file. Returns true if the password was migrated.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"
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