sitevision-cli 1.0.0-beta.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/LICENSE +21 -0
- package/dist/cli.js +194 -70
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +95 -212
- package/dist/commands/dev.js +87 -416
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +50 -33
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +5 -31
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.js +33 -15
- package/dist/components/PasswordInput.js +2 -1
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.js +44 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.js +703 -0
- package/dist/shell/Frame.js +138 -0
- package/dist/shell/Help.js +96 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.js +149 -0
- package/dist/shell/Shell.js +953 -0
- package/dist/shell/Tabs.js +96 -0
- package/dist/shell/actions.js +493 -0
- package/dist/utils/config.js +62 -2
- package/dist/utils/create-app.js +231 -0
- package/dist/utils/debug.js +32 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.js +369 -0
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +54 -6
- package/dist/utils/oauth2-auth.js +146 -43
- package/dist/utils/password-prompt.js +37 -3
- package/dist/utils/process-runner.js +77 -38
- package/dist/utils/project-detection.js +457 -112
- package/dist/utils/session-cookie-auth.js +10 -6
- package/dist/utils/sitevision-api.js +162 -32
- package/dist/utils/sitevision-scripts-runner.js +50 -22
- package/dist/utils/tasks.js +510 -0
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +9 -5
- package/dist/utils/workspace.js +105 -0
- package/dist/utils/zip.js +18 -19
- package/package.json +27 -4
- package/readme.md +113 -141
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -307
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -16
- package/dist/commands/dev.d.ts +0 -16
- package/dist/commands/index.d.ts +0 -4
- package/dist/commands/info.d.ts +0 -2
- package/dist/commands/setup-signing.d.ts +0 -2
- package/dist/commands/sign.d.ts +0 -14
- package/dist/commands/types.d.ts +0 -18
- package/dist/commands/watch.d.ts +0 -2
- package/dist/components/AnimatedLogo.d.ts +0 -10
- package/dist/components/AuthLoginScreen.d.ts +0 -21
- package/dist/components/DevPropertiesForm.d.ts +0 -10
- package/dist/components/DevPropertiesForm.js +0 -210
- package/dist/components/InfoScreen.d.ts +0 -7
- package/dist/components/InfoScreen.js +0 -14
- package/dist/components/KeychainPasswordChoice.d.ts +0 -8
- package/dist/components/KeychainPasswordChoice.js +0 -30
- package/dist/components/MainMenu.d.ts +0 -7
- package/dist/components/MainMenu.js +0 -83
- package/dist/components/PasswordInput.d.ts +0 -10
- package/dist/components/ProcessOutput.d.ts +0 -6
- package/dist/components/ProcessOutput.js +0 -30
- package/dist/components/SetupFlow.d.ts +0 -8
- package/dist/components/SetupFlow.js +0 -233
- package/dist/components/SigningPropertiesForm.d.ts +0 -7
- package/dist/components/SigningPropertiesForm.js +0 -38
- package/dist/components/StatusIndicator.d.ts +0 -8
- package/dist/components/TextInput.d.ts +0 -10
- package/dist/components/TextInput.js +0 -29
- package/dist/components/WelcomeScreen.d.ts +0 -14
- package/dist/types/index.d.ts +0 -284
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -17
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -27
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -22
- package/dist/utils/project-detection.d.ts +0 -151
- package/dist/utils/session-cookie-auth.d.ts +0 -34
- package/dist/utils/signing-password.d.ts +0 -12
- package/dist/utils/signing-step.d.ts +0 -15
- package/dist/utils/signing-step.js +0 -16
- package/dist/utils/sitevision-api.d.ts +0 -112
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/zip.d.ts +0 -71
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { setSessionCookie } from './keychain.js';
|
|
2
|
+
import { debug } from './debug.js';
|
|
2
3
|
function bareDomain(domain) {
|
|
3
4
|
return domain.replace(/^\./, '');
|
|
4
5
|
}
|
|
@@ -15,14 +16,15 @@ function domainRelated(a, b) {
|
|
|
15
16
|
* browser — so it's unit-testable.
|
|
16
17
|
*/
|
|
17
18
|
export function selectSessionCookie(all, siteDomain) {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// Only a session on the deploy site counts: a Java IdP sets a JSESSIONID of
|
|
20
|
+
// its own, and that one must never be sent to the site.
|
|
21
|
+
const chosen = all.find(c => c.name === 'JSESSIONID' && domainRelated(c.domain, siteDomain));
|
|
22
|
+
if (!chosen) {
|
|
20
23
|
const domains = [...new Set(all.map(c => bareDomain(c.domain)))];
|
|
21
24
|
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.`,
|
|
25
|
+
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
26
|
};
|
|
24
27
|
}
|
|
25
|
-
const chosen = sessions.find(c => domainRelated(c.domain, siteDomain)) ?? sessions[0];
|
|
26
28
|
const cookies = all.filter(c => domainRelated(c.domain, chosen.domain));
|
|
27
29
|
return {
|
|
28
30
|
cookie: cookies.map(c => `${c.name}=${c.value}`).join('; '),
|
|
@@ -56,7 +58,7 @@ async function readAllCookies(browser, page) {
|
|
|
56
58
|
*/
|
|
57
59
|
export async function beginCookieLogin(dev) {
|
|
58
60
|
const { domain, username } = dev;
|
|
59
|
-
if (!domain
|
|
61
|
+
if (!domain)
|
|
60
62
|
return null;
|
|
61
63
|
let puppeteer;
|
|
62
64
|
try {
|
|
@@ -70,12 +72,14 @@ export async function beginCookieLogin(dev) {
|
|
|
70
72
|
browser = await puppeteer.launch({ headless: false, channel: 'chrome' });
|
|
71
73
|
const page = await browser.newPage();
|
|
72
74
|
const loginUrl = dev.sessionLoginUrl || `https://${domain}/`;
|
|
75
|
+
debug('auth', `cookie login ${loginUrl}`);
|
|
73
76
|
await page.goto(loginUrl, { waitUntil: 'domcontentloaded' }).catch(() => {
|
|
74
77
|
// A SAML redirect may abort the initial navigation — that's fine.
|
|
75
78
|
});
|
|
76
79
|
const capture = async () => {
|
|
77
80
|
const all = await readAllCookies(browser, page);
|
|
78
81
|
const result = selectSessionCookie(all, domain);
|
|
82
|
+
debug('auth', `cookie capture ${domain}: ${result.cookie ? 'session found' : 'no session'} (${all.length} cookies)`);
|
|
79
83
|
if (result.cookie) {
|
|
80
84
|
setSessionCookie(domain, username, result.cookie);
|
|
81
85
|
}
|
|
@@ -86,7 +90,7 @@ export async function beginCookieLogin(dev) {
|
|
|
86
90
|
// Best-effort close.
|
|
87
91
|
});
|
|
88
92
|
};
|
|
89
|
-
return { capture, close };
|
|
93
|
+
return { capture, close, loginUrl };
|
|
90
94
|
}
|
|
91
95
|
catch {
|
|
92
96
|
if (browser) {
|
|
@@ -12,7 +12,8 @@ import fs from 'fs';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import https from 'https';
|
|
14
14
|
import http from 'http';
|
|
15
|
-
import { buildImportEndpointUrl, buildAddonEndpointUrl, } from './project-detection.js';
|
|
15
|
+
import { buildImportEndpointUrl, buildAddonEndpointUrl, buildApiBaseUrl, } from './project-detection.js';
|
|
16
|
+
import { debug } from './debug.js';
|
|
16
17
|
// =============================================================================
|
|
17
18
|
// CONSTANTS
|
|
18
19
|
// =============================================================================
|
|
@@ -89,6 +90,9 @@ function createMultipartFormData(filePath, fieldName, boundary) {
|
|
|
89
90
|
export function makeRequest(url, options) {
|
|
90
91
|
return new Promise((resolve, reject) => {
|
|
91
92
|
const parsedUrl = new URL(url);
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
const describe = `${options.method} ${parsedUrl.host}${parsedUrl.pathname}`;
|
|
95
|
+
debug('http', describe);
|
|
92
96
|
const isHttps = parsedUrl.protocol === 'https:';
|
|
93
97
|
const transport = isHttps ? https : http;
|
|
94
98
|
const headers = {
|
|
@@ -121,18 +125,28 @@ export function makeRequest(url, options) {
|
|
|
121
125
|
chunks.push(chunk);
|
|
122
126
|
});
|
|
123
127
|
res.on('end', () => {
|
|
128
|
+
debug('http', `${describe} -> ${res.statusCode} ${res.headers['content-type'] ?? ''} ${Date.now() - startedAt}ms`);
|
|
124
129
|
resolve({
|
|
125
130
|
statusCode: res.statusCode || 0,
|
|
126
131
|
body: Buffer.concat(chunks),
|
|
127
132
|
headers: res.headers,
|
|
128
133
|
});
|
|
129
134
|
});
|
|
135
|
+
// A connection cut mid-body never emits 'end'.
|
|
136
|
+
res.on('error', reject);
|
|
137
|
+
res.on('close', () => {
|
|
138
|
+
if (!res.complete)
|
|
139
|
+
reject(new Error('Response was cut off'));
|
|
140
|
+
});
|
|
130
141
|
});
|
|
131
142
|
// Abort hung connections instead of blocking the CLI indefinitely.
|
|
132
143
|
req.setTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => {
|
|
133
144
|
req.destroy(new Error(`Request timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`));
|
|
134
145
|
});
|
|
135
|
-
req.on('error',
|
|
146
|
+
req.on('error', error => {
|
|
147
|
+
debug('http', `${describe} failed: ${error.message}`);
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
136
150
|
if (options.body) {
|
|
137
151
|
req.write(options.body);
|
|
138
152
|
}
|
|
@@ -160,16 +174,36 @@ async function delay(ms) {
|
|
|
160
174
|
*/
|
|
161
175
|
export function summarizeErrorBody(body, headers) {
|
|
162
176
|
const contentType = headers['content-type'] ?? 'unknown';
|
|
163
|
-
const
|
|
177
|
+
const declaredText = contentType.includes('text') ||
|
|
164
178
|
contentType.includes('json') ||
|
|
165
179
|
contentType.includes('xml');
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
const text = body.toString('utf8');
|
|
181
|
+
// Show the body when the server declares it text OR the bytes decode to
|
|
182
|
+
// mostly-printable text. Sitevision sometimes returns a text/JSON error with
|
|
183
|
+
// a missing or non-text content-type, and hiding it obscures the real cause.
|
|
184
|
+
if (declaredText || isMostlyPrintable(text)) {
|
|
185
|
+
const collapsed = text.replaceAll(/\s+/g, ' ').trim();
|
|
186
|
+
const max = 300;
|
|
187
|
+
const summary = collapsed.length > max ? collapsed.slice(0, max) + '…' : collapsed;
|
|
188
|
+
if (summary.length > 0)
|
|
189
|
+
return summary;
|
|
190
|
+
}
|
|
191
|
+
return `(${contentType}, ${body.length} bytes)`;
|
|
192
|
+
}
|
|
193
|
+
/** Heuristic: a decoded string is text if <10% of chars are control/undecodable. */
|
|
194
|
+
function isMostlyPrintable(text) {
|
|
195
|
+
if (text.length === 0)
|
|
196
|
+
return false;
|
|
197
|
+
let bad = 0;
|
|
198
|
+
for (const ch of text) {
|
|
199
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
200
|
+
// Undecodable byte (replacement char), or a control char other than
|
|
201
|
+
// tab/newline/vertical-tab/form-feed/carriage-return.
|
|
202
|
+
if (code === 0xff_fd || code < 9 || (code > 13 && code < 32)) {
|
|
203
|
+
bad++;
|
|
204
|
+
}
|
|
168
205
|
}
|
|
169
|
-
|
|
170
|
-
const max = 300;
|
|
171
|
-
const summary = text.length > max ? text.slice(0, max) + '…' : text;
|
|
172
|
-
return summary.length > 0 ? summary : `(${contentType}, empty body)`;
|
|
206
|
+
return bad / text.length < 0.1;
|
|
173
207
|
}
|
|
174
208
|
/** ZIP local-file-header magic bytes: "PK\x03\x04". */
|
|
175
209
|
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
@@ -316,9 +350,11 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
316
350
|
if (force) {
|
|
317
351
|
url += '?force=true';
|
|
318
352
|
}
|
|
319
|
-
//
|
|
353
|
+
// The import endpoints read the archive from a multipart part named "data"
|
|
354
|
+
// (per the webAppImport/restAppImport docs, and confirmed against a live
|
|
355
|
+
// server). Signing uses "file" against a different endpoint.
|
|
320
356
|
const boundary = generateBoundary();
|
|
321
|
-
const { body, contentType } = createMultipartFormData(zipPath, '
|
|
357
|
+
const { body, contentType } = createMultipartFormData(zipPath, 'data', boundary);
|
|
322
358
|
const { auth, kind } = configAuth(config);
|
|
323
359
|
try {
|
|
324
360
|
const response = await makeRequest(url, {
|
|
@@ -369,6 +405,14 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
369
405
|
error: 'Conflict. Addon already exists. Use --force to overwrite.',
|
|
370
406
|
};
|
|
371
407
|
}
|
|
408
|
+
if (response.statusCode === 400 &&
|
|
409
|
+
response.body.toString().includes('could not resolve context node')) {
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: `Deployment failed with status 400: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
413
|
+
contextNodeMissing: true,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
372
416
|
return {
|
|
373
417
|
success: false,
|
|
374
418
|
error: `Deployment failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
@@ -394,23 +438,24 @@ export async function deployProduction(signedZipPath, config, appType) {
|
|
|
394
438
|
if (!deployResult.success) {
|
|
395
439
|
return deployResult;
|
|
396
440
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (!activationResult.success) {
|
|
401
|
-
return {
|
|
402
|
-
success: true,
|
|
403
|
-
executableId: deployResult.executableId,
|
|
404
|
-
message: `Deployed successfully but activation failed: ${activationResult.error}`,
|
|
405
|
-
};
|
|
406
|
-
}
|
|
441
|
+
if (!config.activate)
|
|
442
|
+
return deployResult;
|
|
443
|
+
if (!deployResult.executableId) {
|
|
407
444
|
return {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
message: 'Deployed
|
|
445
|
+
...deployResult,
|
|
446
|
+
activated: false,
|
|
447
|
+
message: 'Deployed, but not activated: the server did not return an executable id.',
|
|
411
448
|
};
|
|
412
449
|
}
|
|
413
|
-
|
|
450
|
+
const activationResult = await activateApp(deployResult.executableId, config, appType);
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
executableId: deployResult.executableId,
|
|
454
|
+
activated: activationResult.success,
|
|
455
|
+
message: activationResult.success
|
|
456
|
+
? 'Deployed and activated successfully'
|
|
457
|
+
: `Deployed successfully but activation failed: ${activationResult.error}`,
|
|
458
|
+
};
|
|
414
459
|
}
|
|
415
460
|
// =============================================================================
|
|
416
461
|
// ADDON MANAGEMENT API
|
|
@@ -423,10 +468,11 @@ export async function deployProduction(signedZipPath, config, appType) {
|
|
|
423
468
|
*/
|
|
424
469
|
export async function createAddon(config, appType) {
|
|
425
470
|
const url = buildAddonEndpointUrl(config.domain, config.siteName, appType, config.useHTTP);
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
471
|
+
const payload = { name: config.addonName };
|
|
472
|
+
// Only the custommodule endpoints take (and require) a category.
|
|
473
|
+
if (appType === 'web' || appType === 'widget')
|
|
474
|
+
payload['category'] = 'Other';
|
|
475
|
+
const body = JSON.stringify(payload);
|
|
430
476
|
const { auth, kind } = configAuth(config);
|
|
431
477
|
try {
|
|
432
478
|
const response = await makeRequest(url, {
|
|
@@ -487,9 +533,8 @@ export async function createAddon(config, appType) {
|
|
|
487
533
|
export async function activateApp(executableId, config, _appType) {
|
|
488
534
|
const protocol = config.useHTTP ? 'http' : 'https';
|
|
489
535
|
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
});
|
|
536
|
+
// Property name per the ActivateCustomModuleExecutable PUT docs.
|
|
537
|
+
const body = JSON.stringify({ customModuleExecutableId: executableId });
|
|
493
538
|
const { auth, kind } = configAuth(config);
|
|
494
539
|
try {
|
|
495
540
|
const response = await makeRequest(url, {
|
|
@@ -523,6 +568,91 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
523
568
|
};
|
|
524
569
|
}
|
|
525
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* List the executables (uploaded versions) of the configured addon.
|
|
573
|
+
*/
|
|
574
|
+
export async function listExecutables(config) {
|
|
575
|
+
const protocol = config.useHTTP ? 'http' : 'https';
|
|
576
|
+
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
577
|
+
const { auth, kind } = configAuth(config);
|
|
578
|
+
try {
|
|
579
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
580
|
+
if (response.statusCode === 401) {
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: unauthorizedMessage(kind),
|
|
584
|
+
authExpired: true,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (response.statusCode !== 200) {
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
error: `Listing versions failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const data = JSON.parse(response.body.toString());
|
|
594
|
+
return { success: true, executables: data.executables ?? [] };
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
return {
|
|
598
|
+
success: false,
|
|
599
|
+
error: `Listing versions failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const ADDON_TYPES = {
|
|
604
|
+
'sv:customModule': 'web',
|
|
605
|
+
'sv:marketplaceCustomModule': 'web',
|
|
606
|
+
'sv:widgetCustomModule': 'widget',
|
|
607
|
+
'sv:marketplaceWidgetCustomModule': 'widget',
|
|
608
|
+
'sv:headlessCustomModule': 'rest',
|
|
609
|
+
'sv:marketplaceHeadlessCustomModule': 'rest',
|
|
610
|
+
'sv:mcpServerCustomModule': 'mcp',
|
|
611
|
+
};
|
|
612
|
+
/**
|
|
613
|
+
* List the addons (custom modules) in the site's Addon Repository.
|
|
614
|
+
*/
|
|
615
|
+
export async function listAddons(config) {
|
|
616
|
+
const url = `${buildApiBaseUrl(config.domain, config.siteName, config.useHTTP)}/Addon%20Repository/nodes`;
|
|
617
|
+
const { auth, kind } = configAuth(config);
|
|
618
|
+
try {
|
|
619
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
620
|
+
if (response.statusCode === 401) {
|
|
621
|
+
return { success: false, error: unauthorizedMessage(kind) };
|
|
622
|
+
}
|
|
623
|
+
if (response.statusCode !== 200) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
error: `Listing addons failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const nodes = JSON.parse(response.body.toString());
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
addons: nodes
|
|
633
|
+
.filter(node => Object.hasOwn(ADDON_TYPES, node.type))
|
|
634
|
+
.map(node => ({ ...node, appType: ADDON_TYPES[node.type] })),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: `Listing addons failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Whether an addon exists, judged from a listAddons result. A failed or empty
|
|
646
|
+
* listing is "unknown": a dead session can look exactly like an empty site.
|
|
647
|
+
*/
|
|
648
|
+
export function classifyAddon(listed, name) {
|
|
649
|
+
if (!listed.success || !listed.addons?.length)
|
|
650
|
+
return 'unknown';
|
|
651
|
+
const wanted = name.toLowerCase();
|
|
652
|
+
return listed.addons.some(addon => addon.name.toLowerCase() === wanted)
|
|
653
|
+
? 'present'
|
|
654
|
+
: 'missing';
|
|
655
|
+
}
|
|
526
656
|
// =============================================================================
|
|
527
657
|
// HELPER EXPORTS
|
|
528
658
|
// =============================================================================
|
|
@@ -17,14 +17,33 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import fs from 'fs';
|
|
20
|
-
import {
|
|
20
|
+
import { killChild, spawnChild } from './process-runner.js';
|
|
21
|
+
import { getAppIdConfig, getFullAppId } from './project-detection.js';
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
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
|
|
27
|
-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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
|
|
84
|
+
const dir = findInstalledPackage(projectRoot, PACKAGE);
|
|
85
|
+
if (!dir)
|
|
86
|
+
return null;
|
|
72
87
|
try {
|
|
73
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
74
89
|
return parsed.version ?? null;
|
|
75
90
|
}
|
|
76
91
|
catch {
|
|
@@ -145,8 +160,9 @@ const MAX_OUTPUT_CHARS = 50_000;
|
|
|
145
160
|
*
|
|
146
161
|
* @param projectRoot - Project root directory (used as cwd)
|
|
147
162
|
* @param onOutput - Optional callback for streaming output chunks
|
|
163
|
+
* @param signal - Aborting kills the build and everything it started
|
|
148
164
|
*/
|
|
149
|
-
export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
165
|
+
export async function runSitevisionScriptsBuild(projectRoot, onOutput, signal) {
|
|
150
166
|
const bin = getSitevisionScriptsBin(projectRoot);
|
|
151
167
|
if (!bin) {
|
|
152
168
|
return {
|
|
@@ -157,10 +173,18 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
157
173
|
}
|
|
158
174
|
return new Promise(resolve => {
|
|
159
175
|
let output = '';
|
|
160
|
-
const
|
|
176
|
+
const { prefix, suffix } = getAppIdConfig();
|
|
177
|
+
const child = spawnChild(process.execPath, [bin, 'build'], {
|
|
161
178
|
cwd: projectRoot,
|
|
179
|
+
env: { ...process.env, APP_ID_PREFIX: prefix, APP_ID_SUFFIX: suffix },
|
|
162
180
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
181
|
});
|
|
182
|
+
const abort = () => {
|
|
183
|
+
killChild(child);
|
|
184
|
+
};
|
|
185
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
186
|
+
if (signal?.aborted)
|
|
187
|
+
abort();
|
|
164
188
|
const handleData = (data) => {
|
|
165
189
|
const text = data.toString();
|
|
166
190
|
output += text;
|
|
@@ -175,12 +199,16 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
175
199
|
resolve({ success: false, output, error: error.message });
|
|
176
200
|
});
|
|
177
201
|
child.on('close', code => {
|
|
202
|
+
signal?.removeEventListener('abort', abort);
|
|
203
|
+
const stopped = signal?.aborted ?? false;
|
|
178
204
|
resolve({
|
|
179
|
-
success: code === 0,
|
|
205
|
+
success: code === 0 && !stopped,
|
|
180
206
|
output,
|
|
181
|
-
error:
|
|
182
|
-
?
|
|
183
|
-
:
|
|
207
|
+
error: stopped
|
|
208
|
+
? 'Build stopped.'
|
|
209
|
+
: code === 0
|
|
210
|
+
? undefined
|
|
211
|
+
: `sitevision-scripts build exited with code ${code}`,
|
|
184
212
|
});
|
|
185
213
|
});
|
|
186
214
|
});
|