sitevision-cli 1.0.0-beta.31 → 1.0.0-beta.33

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +73 -34
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +85 -238
  7. package/dist/commands/dev.js +34 -37
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +43 -28
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +0 -8
  13. package/dist/components/AuthLoginScreen.js +27 -12
  14. package/dist/scaffold/inquirer-stub.js +119 -0
  15. package/dist/scaffold/register.js +15 -0
  16. package/dist/shell/ConfigForm.js +174 -29
  17. package/dist/shell/Frame.js +4 -1
  18. package/dist/shell/Help.js +2 -1
  19. package/dist/shell/Settings.js +59 -6
  20. package/dist/shell/Shell.js +361 -43
  21. package/dist/shell/Tabs.js +8 -2
  22. package/dist/shell/actions.js +0 -0
  23. package/dist/utils/config.js +46 -3
  24. package/dist/utils/create-app.js +226 -0
  25. package/dist/utils/i18n.js +68 -13
  26. package/dist/utils/jsonc.js +2 -1
  27. package/dist/utils/keychain.js +44 -4
  28. package/dist/utils/oauth2-auth.js +35 -7
  29. package/dist/utils/password-prompt.js +23 -3
  30. package/dist/utils/process-runner.js +8 -3
  31. package/dist/utils/project-detection.js +172 -44
  32. package/dist/utils/session-cookie-auth.js +5 -4
  33. package/dist/utils/sitevision-api.js +21 -14
  34. package/dist/utils/sitevision-scripts-runner.js +32 -15
  35. package/dist/utils/tasks.js +132 -66
  36. package/dist/utils/version-check.js +3 -3
  37. package/dist/utils/webpack-runner.js +6 -5
  38. package/dist/utils/workspace.js +41 -3
  39. package/dist/utils/zip.js +18 -19
  40. package/package.json +24 -3
  41. package/readme.md +11 -4
  42. package/dist/app.d.ts +0 -6
  43. package/dist/app.js +0 -321
  44. package/dist/cli.d.ts +0 -2
  45. package/dist/commands/build.d.ts +0 -12
  46. package/dist/commands/deploy.d.ts +0 -17
  47. package/dist/commands/dev.d.ts +0 -14
  48. package/dist/commands/index.d.ts +0 -4
  49. package/dist/commands/info.d.ts +0 -2
  50. package/dist/commands/setup-signing.d.ts +0 -2
  51. package/dist/commands/sign.d.ts +0 -14
  52. package/dist/commands/types.d.ts +0 -18
  53. package/dist/commands/watch.d.ts +0 -2
  54. package/dist/components/AnimatedLogo.d.ts +0 -10
  55. package/dist/components/AuthLoginScreen.d.ts +0 -21
  56. package/dist/components/DevPropertiesForm.d.ts +0 -11
  57. package/dist/components/DevPropertiesForm.js +0 -246
  58. package/dist/components/InfoScreen.d.ts +0 -7
  59. package/dist/components/InfoScreen.js +0 -14
  60. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  61. package/dist/components/KeychainPasswordChoice.js +0 -30
  62. package/dist/components/MainMenu.d.ts +0 -7
  63. package/dist/components/MainMenu.js +0 -88
  64. package/dist/components/PasswordInput.d.ts +0 -10
  65. package/dist/components/ProcessOutput.d.ts +0 -6
  66. package/dist/components/ProcessOutput.js +0 -30
  67. package/dist/components/SetupFlow.d.ts +0 -8
  68. package/dist/components/SetupFlow.js +0 -233
  69. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  70. package/dist/components/SigningPropertiesForm.js +0 -38
  71. package/dist/components/StatusIndicator.d.ts +0 -8
  72. package/dist/components/TextInput.d.ts +0 -10
  73. package/dist/components/TextInput.js +0 -29
  74. package/dist/components/WelcomeScreen.d.ts +0 -14
  75. package/dist/shell/AddonPicker.d.ts +0 -14
  76. package/dist/shell/Changelog.d.ts +0 -10
  77. package/dist/shell/CommandPalette.d.ts +0 -12
  78. package/dist/shell/ConfigForm.d.ts +0 -36
  79. package/dist/shell/Frame.d.ts +0 -59
  80. package/dist/shell/Help.d.ts +0 -9
  81. package/dist/shell/Popover.d.ts +0 -12
  82. package/dist/shell/Settings.d.ts +0 -6
  83. package/dist/shell/Shell.d.ts +0 -10
  84. package/dist/shell/Tabs.d.ts +0 -36
  85. package/dist/shell/actions.d.ts +0 -48
  86. package/dist/types/index.d.ts +0 -295
  87. package/dist/utils/branding.d.ts +0 -28
  88. package/dist/utils/config.d.ts +0 -27
  89. package/dist/utils/environments.d.ts +0 -20
  90. package/dist/utils/i18n.d.ts +0 -12
  91. package/dist/utils/jsonc.d.ts +0 -19
  92. package/dist/utils/keychain.d.ts +0 -15
  93. package/dist/utils/oauth2-auth.d.ts +0 -64
  94. package/dist/utils/password-prompt.d.ts +0 -14
  95. package/dist/utils/process-runner.d.ts +0 -31
  96. package/dist/utils/project-detection.d.ts +0 -202
  97. package/dist/utils/session-cookie-auth.d.ts +0 -35
  98. package/dist/utils/signing-password.d.ts +0 -12
  99. package/dist/utils/signing-step.d.ts +0 -15
  100. package/dist/utils/signing-step.js +0 -16
  101. package/dist/utils/sitevision-api.d.ts +0 -155
  102. package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
  103. package/dist/utils/tasks.d.ts +0 -50
  104. package/dist/utils/version-check.d.ts +0 -9
  105. package/dist/utils/webpack-runner.d.ts +0 -76
  106. package/dist/utils/workspace.d.ts +0 -17
  107. package/dist/utils/zip.d.ts +0 -71
@@ -1,8 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getDeployPassword, setDeployPassword, getSessionCookie, } from './keychain.js';
4
- import { parseJsonc } from './jsonc.js';
4
+ import { parseJsonc, stripJsonComments } from './jsonc.js';
5
5
  import { getLanguage } from './i18n.js';
6
+ import { getGlobalSigning } from './config.js';
6
7
  // =============================================================================
7
8
  // LOCALIZED TEXT
8
9
  // =============================================================================
@@ -22,6 +23,20 @@ export function localizedText(value, preferred = getLanguage()) {
22
23
  return value;
23
24
  return value[preferred] ?? value['en'] ?? Object.values(value)[0] ?? '';
24
25
  }
26
+ /**
27
+ * The manifest's names when the addon name is none of them. The two are
28
+ * separate things (the addon is an object on the site, the name a label in the
29
+ * zip), so a difference is only ever pointed out, never "fixed".
30
+ */
31
+ export function addonNameDrift(addonName, manifest) {
32
+ const same = (text) => text.trim().toLowerCase();
33
+ const names = (typeof manifest?.name === 'string'
34
+ ? [manifest.name]
35
+ : Object.values(manifest?.name ?? {})).filter(name => name.trim() !== '');
36
+ if (!addonName?.trim() || names.length === 0)
37
+ return undefined;
38
+ return names.some(name => same(name) === same(addonName)) ? undefined : names;
39
+ }
25
40
  // =============================================================================
26
41
  // PATH UTILITIES
27
42
  // =============================================================================
@@ -105,16 +120,33 @@ function readDevPropertiesFile(dir) {
105
120
  return null;
106
121
  }
107
122
  }
123
+ /**
124
+ * Merge config layers, later ones winning. `environments` merges per name, so a
125
+ * layer that overrides one field of an environment keeps the rest of it.
126
+ */
127
+ export function mergeDevLayers(...layers) {
128
+ let merged = {};
129
+ for (const layer of layers) {
130
+ if (!layer)
131
+ continue;
132
+ const environments = { ...merged.environments };
133
+ for (const [name, override] of Object.entries(layer.environments ?? {})) {
134
+ environments[name] = { ...environments[name], ...override };
135
+ }
136
+ merged = { ...merged, ...layer };
137
+ if (Object.keys(environments).length > 0) {
138
+ merged.environments = environments;
139
+ }
140
+ }
141
+ return merged;
142
+ }
108
143
  /**
109
144
  * The dev properties a workspace root defines (its own file merged over any
110
145
  * ancestors'), with the deploy password resolved from the keychain like an
111
146
  * app's would be. Used to edit shared config from the shell.
112
147
  */
113
148
  export function readWorkspaceDevProperties(root) {
114
- const merged = {
115
- ...readInheritedDevProperties(root),
116
- ...readDevPropertiesFile(root),
117
- };
149
+ const merged = mergeDevLayers(readInheritedDevProperties(root), readDevPropertiesFile(root));
118
150
  if (merged.domain && merged.username && !merged.password) {
119
151
  merged.password =
120
152
  process.env['SITEVISION_DEPLOY_PASSWORD'] ??
@@ -125,37 +157,31 @@ export function readWorkspaceDevProperties(root) {
125
157
  }
126
158
  /** Ancestor directories' .dev_properties.json merged, nearest wins (no own file). */
127
159
  export function readAncestorDevProperties(root) {
128
- let merged = {};
129
- for (const dir of ancestorDirs(root)) {
130
- merged = { ...merged, ...readDevPropertiesFile(dir) };
131
- }
132
- return merged;
160
+ return mergeDevLayers(...ancestorDirs(root).map(dir => readDevPropertiesFile(dir)));
133
161
  }
134
162
  /** Shared defaults from package.json: the workspace root's first, the app's on top. */
135
163
  export function readPackageDefaultsChain(root) {
136
- let merged = {};
137
- for (const dir of [...ancestorDirs(root), root]) {
138
- merged = { ...merged, ...readPackageDefaults(dir) };
139
- }
140
- return merged;
164
+ return mergeDevLayers(...[...ancestorDirs(root), root].map(dir => readPackageDefaults(dir)));
141
165
  }
142
166
  /**
143
167
  * Everything an app's own .dev_properties.json sits on top of: package.json
144
168
  * defaults (root, then app), then ancestor .dev_properties.json files.
145
169
  */
146
170
  export function readInheritedDevProperties(root) {
147
- return {
148
- ...readPackageDefaultsChain(root),
149
- ...readAncestorDevProperties(root),
150
- };
171
+ return mergeDevLayers(readPackageDefaultsChain(root), readAncestorDevProperties(root));
151
172
  }
152
173
  /**
153
174
  * Get app ID configuration from environment or defaults
154
175
  */
155
- function getAppIdConfig() {
176
+ export function getAppIdConfig() {
156
177
  return {
157
- prefix: process.env['SITEVISION_APP_ID_PREFIX'] || '',
158
- suffix: process.env['SITEVISION_APP_ID_SUFFIX'] || '',
178
+ // svc's own names first, then the ones sitevision-scripts reads.
179
+ prefix: process.env['SITEVISION_APP_ID_PREFIX'] ||
180
+ process.env['APP_ID_PREFIX'] ||
181
+ '',
182
+ suffix: process.env['SITEVISION_APP_ID_SUFFIX'] ||
183
+ process.env['APP_ID_SUFFIX'] ||
184
+ '',
159
185
  };
160
186
  }
161
187
  /**
@@ -270,7 +296,7 @@ export function buildImportEndpointUrl(domain, siteName, addonName, appType, use
270
296
  export class ManifestParseError extends Error {
271
297
  constructor(manifestPath, cause) {
272
298
  const reason = cause instanceof Error ? cause.message : String(cause);
273
- super(`${manifestPath} is not valid JSON: ${reason}`);
299
+ super(`${manifestPath} ${reason}`);
274
300
  this.name = 'ManifestParseError';
275
301
  }
276
302
  }
@@ -290,18 +316,65 @@ export function readManifest(cwd) {
290
316
  }
291
317
  // Manifests may contain comments (Sitevision's own docs show them), so
292
318
  // parse as JSONC.
319
+ let manifest;
293
320
  try {
294
- return {
295
- manifestPath,
296
- manifest: parseJsonc(fs.readFileSync(manifestPath, 'utf-8')),
297
- };
321
+ manifest = parseJsonc(fs.readFileSync(manifestPath, 'utf-8'));
298
322
  }
299
323
  catch (error) {
300
- throw new ManifestParseError(manifestPath, error);
324
+ throw new ManifestParseError(manifestPath, `is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
325
+ }
326
+ if (typeof manifest !== 'object' || manifest === null) {
327
+ throw new ManifestParseError(manifestPath, 'must be a JSON object');
301
328
  }
329
+ // Zip names, endpoints and the build all derive from these.
330
+ for (const key of ['id', 'version', 'type']) {
331
+ if (typeof manifest[key] !== 'string' || manifest[key] === '') {
332
+ throw new ManifestParseError(manifestPath, `is missing "${key}"`);
333
+ }
334
+ }
335
+ return { manifestPath, manifest };
302
336
  }
303
337
  return null;
304
338
  }
339
+ const JSON_STRING = String.raw `"(?:[^"\\]|\\.)*"`;
340
+ const escapeRegExp = (s) => s.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`);
341
+ /**
342
+ * Set one text field of manifest.json, or one language of a localized field.
343
+ * An empty value removes the key. A value that is already a string is replaced
344
+ * in the text, so comments and formatting survive; anything else rewrites the
345
+ * file, which is refused when that would drop comments.
346
+ */
347
+ export function writeManifestField(manifestPath, key, value, lang) {
348
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
349
+ const wanted = parseJsonc(raw);
350
+ const existing = wanted[key];
351
+ if (lang && typeof existing !== 'object') {
352
+ // A plain string becomes the English text of a multilingual value.
353
+ wanted[key] = existing ? { en: existing } : {};
354
+ }
355
+ const holder = lang ? wanted[key] : wanted;
356
+ const leaf = lang ?? key;
357
+ // JSON.stringify drops an undefined key.
358
+ holder[leaf] = value || undefined;
359
+ const prefix = lang
360
+ ? String.raw `"${escapeRegExp(key)}"\s*:\s*\{[^}]*?"${escapeRegExp(lang)}"\s*:\s*`
361
+ : String.raw `"${escapeRegExp(key)}"\s*:\s*`;
362
+ const patched = raw.replace(new RegExp(`(${prefix})${JSON_STRING}`), (_, before) => before + JSON.stringify(value));
363
+ let patchedOk = false;
364
+ try {
365
+ patchedOk = JSON.stringify(parseJsonc(patched)) === JSON.stringify(wanted);
366
+ }
367
+ catch { }
368
+ if (patchedOk) {
369
+ fs.writeFileSync(manifestPath, patched);
370
+ return;
371
+ }
372
+ if (stripJsonComments(raw) !== raw) {
373
+ throw new Error(`${manifestPath} has comments; add or remove "${leaf}" by hand`);
374
+ }
375
+ const indent = /^(?<indent>[\t ]+)/m.exec(raw)?.groups?.['indent'] ?? '\t';
376
+ fs.writeFileSync(manifestPath, JSON.stringify(wanted, null, indent) + (raw.endsWith('\n') ? '\n' : ''));
377
+ }
305
378
  /**
306
379
  * Detect if the current directory is a Sitevision project
307
380
  */
@@ -335,7 +408,9 @@ export function detectProject(cwd = process.cwd()) {
335
408
  let hasLegacyPassword = false;
336
409
  if (hasDevProperties) {
337
410
  try {
338
- const parsed = { ...inherited, ...own };
411
+ // ponytail: the global signing identity only reaches a project that
412
+ // has some config of its own, so "configured?" checks stay truthful.
413
+ const parsed = mergeDevLayers(getGlobalSigning(), inherited, own);
339
414
  hasLegacyPassword =
340
415
  typeof parsed.password === 'string' && parsed.password.length > 0;
341
416
  devProperties = parsed;
@@ -412,7 +487,7 @@ export function requireProject(cwd) {
412
487
  * the whole shell down.
413
488
  */
414
489
  export function appTypeOf(manifest) {
415
- const type = manifest.type.toLowerCase();
490
+ const type = String(manifest.type ?? '').toLowerCase();
416
491
  if (type.startsWith('web'))
417
492
  return 'web';
418
493
  if (type.startsWith('widget'))
@@ -471,10 +546,18 @@ export function writeDevProperties(projectRoot, properties,
471
546
  // that would shadow the inherited value.
472
547
  const inherited = complete
473
548
  ? {}
474
- : readInheritedDevProperties(projectRoot);
549
+ : mergeDevLayers(getGlobalSigning(), readInheritedDevProperties(projectRoot));
475
550
  const own = Object.fromEntries(Object.entries(persisted).filter(([key, value]) => value !== '' &&
476
551
  (!Object.hasOwn(inherited, key) ||
477
552
  JSON.stringify(inherited[key]) !== JSON.stringify(value))));
553
+ // A plaintext password already in the file (sitevision-scripts puts it
554
+ // there) moves to the keychain; if that fails it stays rather than be lost.
555
+ const legacy = readDevPropertiesFile(projectRoot)?.password;
556
+ if (legacy) {
557
+ const { domain, username } = { ...inherited, ...persisted };
558
+ if (!setDeployPassword(domain, username, legacy))
559
+ own['password'] = legacy;
560
+ }
478
561
  fs.writeFileSync(devPropertiesPath, JSON.stringify(own, null, 2));
479
562
  }
480
563
  export function readSvcConfig(projectRoot) {
@@ -568,6 +651,38 @@ function pendingSync(dir) {
568
651
  }
569
652
  return pending;
570
653
  }
654
+ // Manifest fields package.json mirrors under the same name; the manifest wins.
655
+ const MANIFEST_PACKAGE_KEYS = ['version', 'description', 'author'];
656
+ /**
657
+ * Manifest values the app's own package.json does not mirror yet. Nothing when
658
+ * either file is missing, so syncing never creates a package.json for this.
659
+ */
660
+ function pendingManifestSync(dir) {
661
+ const packageJson = readPackageJson(dir);
662
+ if (!packageJson)
663
+ return [];
664
+ let manifest;
665
+ try {
666
+ manifest = readManifest(dir)?.manifest;
667
+ }
668
+ catch { }
669
+ if (!manifest)
670
+ return [];
671
+ const pending = [];
672
+ for (const key of MANIFEST_PACKAGE_KEYS) {
673
+ const value = key === 'description'
674
+ ? localizedText(manifest.description, 'en')
675
+ : manifest[key];
676
+ const current = packageJson[key];
677
+ // ponytail: an author object ({name, email}) is left alone.
678
+ if (!value || typeof value !== 'string' || value === current)
679
+ continue;
680
+ if (current !== undefined && typeof current !== 'string')
681
+ continue;
682
+ pending.push({ key, from: current, value });
683
+ }
684
+ return pending;
685
+ }
571
686
  /** What an unset field means, so spelling out the default is not a change. */
572
687
  export const IMPLICIT_VALUES = {
573
688
  authMethod: 'basic',
@@ -579,27 +694,38 @@ const display = (value) => typeof value === 'string' ? value : JSON.stringify(va
579
694
  /** What syncing this directory would change in its package.json. */
580
695
  export function getPackageJsonSyncChanges(dir) {
581
696
  const current = readPackageDefaults(dir);
582
- return pendingSync(dir).map(({ key, value }) => current[key] === undefined
583
- ? { key: packageLabel(key), to: display(value) }
584
- : {
585
- key: packageLabel(key),
586
- from: display(current[key]),
587
- to: display(value),
588
- });
697
+ return [
698
+ ...pendingSync(dir).map(({ key, value }) => current[key] === undefined
699
+ ? { key: packageLabel(key), to: display(value) }
700
+ : {
701
+ key: packageLabel(key),
702
+ from: display(current[key]),
703
+ to: display(value),
704
+ }),
705
+ ...pendingManifestSync(dir).map(({ key, from, value }) => ({
706
+ key,
707
+ ...(from !== undefined && { from }),
708
+ to: value,
709
+ })),
710
+ ];
589
711
  }
590
712
  export function hasPackageJson(dir) {
591
713
  return fs.existsSync(path.join(dir, 'package.json'));
592
714
  }
593
715
  /**
594
- * Copy the pending shared values from .dev_properties.json into package.json,
595
- * creating package.json when the directory has none. Throws if it cannot be
716
+ * Copy the pending shared values from .dev_properties.json, and the manifest
717
+ * fields package.json mirrors, into package.json, creating package.json when
718
+ * the directory has none. Throws if it cannot be
596
719
  * read or written.
597
720
  */
598
721
  export function syncDevPropertiesToPackageJson(dir) {
599
722
  const pending = pendingSync(dir);
600
- if (pending.length === 0 && hasPackageJson(dir))
723
+ const mirrored = pendingManifestSync(dir);
724
+ if (pending.length + mirrored.length === 0 && hasPackageJson(dir))
601
725
  return false;
602
726
  updatePackageJson(dir, packageJson => {
727
+ for (const { key, value } of mirrored)
728
+ packageJson[key] = value;
603
729
  for (const { key, value } of pending) {
604
730
  const topKey = PACKAGE_TOP_KEYS[key];
605
731
  if (topKey) {
@@ -665,11 +791,13 @@ export function migrateLegacyPassword(project) {
665
791
  }
666
792
  }
667
793
  catch {
668
- // Unreadable file: nothing to migrate there.
794
+ // Unreadable file: nothing to migrate there. Unwritable: checked below.
669
795
  }
670
796
  }
671
- project.hasLegacyPassword = false;
672
- return true;
797
+ // Moved only if no file still holds it.
798
+ const left = [project.root, ...ancestorDirs(project.root)].some(dir => readDevPropertiesFile(dir)?.password);
799
+ project.hasLegacyPassword = left;
800
+ return !left;
673
801
  }
674
802
  /**
675
803
  * Ensure the dist directory exists
@@ -15,14 +15,15 @@ function domainRelated(a, b) {
15
15
  * browser — so it's unit-testable.
16
16
  */
17
17
  export function selectSessionCookie(all, siteDomain) {
18
- const sessions = all.filter(c => c.name === 'JSESSIONID');
19
- if (sessions.length === 0) {
18
+ // Only a session on the deploy site counts: a Java IdP sets a JSESSIONID of
19
+ // its own, and that one must never be sent to the site.
20
+ const chosen = all.find(c => c.name === 'JSESSIONID' && domainRelated(c.domain, siteDomain));
21
+ if (!chosen) {
20
22
  const domains = [...new Set(all.map(c => bareDomain(c.domain)))];
21
23
  return {
22
- error: `No JSESSIONID among ${all.length} cookies. Domains seen: ${domains.join(', ') || 'none'}. If these are only your IdP, open a Sitevision page in the browser, then press Enter again.`,
24
+ error: `No JSESSIONID for ${siteDomain} among ${all.length} cookies. Domains seen: ${domains.join(', ') || 'none'}. If these are only your IdP, open a Sitevision page in the browser, then press Enter again.`,
23
25
  };
24
26
  }
25
- const chosen = sessions.find(c => domainRelated(c.domain, siteDomain)) ?? sessions[0];
26
27
  const cookies = all.filter(c => domainRelated(c.domain, chosen.domain));
27
28
  return {
28
29
  cookie: cookies.map(c => `${c.name}=${c.value}`).join('; '),
@@ -127,6 +127,12 @@ export function makeRequest(url, options) {
127
127
  headers: res.headers,
128
128
  });
129
129
  });
130
+ // A connection cut mid-body never emits 'end'.
131
+ res.on('error', reject);
132
+ res.on('close', () => {
133
+ if (!res.complete)
134
+ reject(new Error('Response was cut off'));
135
+ });
130
136
  });
131
137
  // Abort hung connections instead of blocking the CLI indefinitely.
132
138
  req.setTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => {
@@ -424,23 +430,24 @@ export async function deployProduction(signedZipPath, config, appType) {
424
430
  if (!deployResult.success) {
425
431
  return deployResult;
426
432
  }
427
- // If activation requested and we have an executable ID
428
- if (config.activate && deployResult.executableId) {
429
- const activationResult = await activateApp(deployResult.executableId, config, appType);
430
- if (!activationResult.success) {
431
- return {
432
- success: true,
433
- executableId: deployResult.executableId,
434
- message: `Deployed successfully but activation failed: ${activationResult.error}`,
435
- };
436
- }
433
+ if (!config.activate)
434
+ return deployResult;
435
+ if (!deployResult.executableId) {
437
436
  return {
438
- success: true,
439
- executableId: deployResult.executableId,
440
- message: 'Deployed and activated successfully',
437
+ ...deployResult,
438
+ activated: false,
439
+ message: 'Deployed, but not activated: the server did not return an executable id.',
441
440
  };
442
441
  }
443
- return deployResult;
442
+ const activationResult = await activateApp(deployResult.executableId, config, appType);
443
+ return {
444
+ success: true,
445
+ executableId: deployResult.executableId,
446
+ activated: activationResult.success,
447
+ message: activationResult.success
448
+ ? 'Deployed and activated successfully'
449
+ : `Deployed successfully but activation failed: ${activationResult.error}`,
450
+ };
444
451
  }
445
452
  // =============================================================================
446
453
  // ADDON MANAGEMENT API
@@ -18,13 +18,32 @@
18
18
  import path from 'path';
19
19
  import fs from 'fs';
20
20
  import { killChild, spawnChild } from './process-runner.js';
21
+ import { getAppIdConfig, getFullAppId } from './project-detection.js';
21
22
  /**
22
- * Resolve the path to the sitevision-scripts CLI entry inside a project.
23
+ * Directory of an installed package, looked up the way Node does: the project's
24
+ * node_modules, then each parent's, since workspaces hoist to the repo root.
25
+ */
26
+ export function findInstalledPackage(projectRoot, name) {
27
+ let dir = path.resolve(projectRoot);
28
+ for (;;) {
29
+ const candidate = path.join(dir, 'node_modules', name);
30
+ if (fs.existsSync(candidate))
31
+ return candidate;
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir)
34
+ return null;
35
+ dir = parent;
36
+ }
37
+ }
38
+ const PACKAGE = '@sitevision/sitevision-scripts';
39
+ /**
40
+ * Resolve the path to the sitevision-scripts CLI entry for a project.
23
41
  * Returns null if the package is not installed.
24
42
  */
25
43
  export function getSitevisionScriptsBin(projectRoot) {
26
- const bin = path.join(projectRoot, 'node_modules', '@sitevision', 'sitevision-scripts', 'bin', 'sitevision-scripts.js');
27
- return fs.existsSync(bin) ? bin : null;
44
+ const dir = findInstalledPackage(projectRoot, PACKAGE);
45
+ const bin = dir && path.join(dir, 'bin', 'sitevision-scripts.js');
46
+ return bin && fs.existsSync(bin) ? bin : null;
28
47
  }
29
48
  /**
30
49
  * Whether the sitevision-scripts package is available in the project.
@@ -35,18 +54,12 @@ export function hasSitevisionScripts(projectRoot) {
35
54
  /**
36
55
  * Path of the zip that `sitevision-scripts build` writes.
37
56
  *
38
- * IMPORTANT: this mirrors sitevision-scripts' own app-id convention
39
- * (`APP_ID_PREFIX`/`APP_ID_SUFFIX` env vars + `dist/<appId>.zip`), which differs
40
- * from the CLI's own `getZipPath` env vars (`SITEVISION_APP_ID_*`). For delegated
41
- * builds the package is the one writing the file, so its convention is the source
42
- * of truth — using `getZipPath` here would look for the wrong filename whenever a
43
- * prefix/suffix is configured.
57
+ * sitevision-scripts names it from `APP_ID_PREFIX`/`APP_ID_SUFFIX`; the build is
58
+ * started with those set from `getAppIdConfig`, so this is the same file that
59
+ * sign and deploy look for.
44
60
  */
45
61
  export function getDelegatedZipPath(projectRoot, manifestId) {
46
- const prefix = process.env['APP_ID_PREFIX'] ?? '';
47
- const suffix = process.env['APP_ID_SUFFIX'] ?? '';
48
- const appId = `${prefix}${manifestId}${suffix}`;
49
- return path.join(projectRoot, 'dist', `${appId}.zip`);
62
+ return path.join(projectRoot, 'dist', `${getFullAppId(manifestId)}.zip`);
50
63
  }
51
64
  // =============================================================================
52
65
  // VERSION COMPATIBILITY
@@ -68,9 +81,11 @@ export const SUPPORTED_SITEVISION_SCRIPTS_RANGE = `>=${SUPPORTED_SITEVISION_SCRI
68
81
  * is not installed / unreadable.
69
82
  */
70
83
  export function getSitevisionScriptsVersion(projectRoot) {
71
- const packageJsonPath = path.join(projectRoot, 'node_modules', '@sitevision', 'sitevision-scripts', 'package.json');
84
+ const dir = findInstalledPackage(projectRoot, PACKAGE);
85
+ if (!dir)
86
+ return null;
72
87
  try {
73
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
88
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
74
89
  return parsed.version ?? null;
75
90
  }
76
91
  catch {
@@ -158,8 +173,10 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput, signal) {
158
173
  }
159
174
  return new Promise(resolve => {
160
175
  let output = '';
176
+ const { prefix, suffix } = getAppIdConfig();
161
177
  const child = spawnChild(process.execPath, [bin, 'build'], {
162
178
  cwd: projectRoot,
179
+ env: { ...process.env, APP_ID_PREFIX: prefix, APP_ID_SUFFIX: suffix },
163
180
  stdio: ['ignore', 'pipe', 'pipe'],
164
181
  });
165
182
  const abort = () => {