sitevision-cli 1.0.0-beta.3 → 1.0.0-beta.31
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 +57 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +59 -8
- package/dist/cli.js +125 -40
- package/dist/commands/build.js +1 -1
- package/dist/commands/deploy.d.ts +2 -2
- package/dist/commands/deploy.js +137 -28
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +70 -371
- package/dist/commands/watch.js +5 -23
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.d.ts +21 -0
- package/dist/components/AuthLoginScreen.js +90 -0
- package/dist/components/DevPropertiesForm.d.ts +2 -1
- package/dist/components/DevPropertiesForm.js +198 -33
- package/dist/components/MainMenu.js +5 -0
- package/dist/components/PasswordInput.js +2 -1
- package/dist/components/SetupFlow.d.ts +2 -1
- package/dist/components/SetupFlow.js +99 -10
- package/dist/shell/AddonPicker.d.ts +14 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.d.ts +10 -0
- package/dist/shell/Changelog.js +44 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.d.ts +36 -0
- package/dist/shell/ConfigForm.js +558 -0
- package/dist/shell/Frame.d.ts +59 -0
- package/dist/shell/Frame.js +135 -0
- package/dist/shell/Help.d.ts +9 -0
- package/dist/shell/Help.js +95 -0
- package/dist/shell/Popover.d.ts +12 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +10 -0
- package/dist/shell/Shell.js +622 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +90 -0
- package/dist/shell/actions.d.ts +48 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +37 -3
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.js +14 -0
- package/dist/utils/environments.d.ts +20 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.d.ts +12 -0
- package/dist/utils/i18n.js +314 -0
- package/dist/utils/jsonc.d.ts +19 -0
- package/dist/utils/jsonc.js +74 -0
- package/dist/utils/keychain.d.ts +9 -0
- package/dist/utils/keychain.js +54 -0
- package/dist/utils/oauth2-auth.d.ts +64 -0
- package/dist/utils/oauth2-auth.js +242 -0
- package/dist/utils/password-prompt.d.ts +5 -0
- package/dist/utils/password-prompt.js +28 -0
- package/dist/utils/process-runner.d.ts +13 -4
- package/dist/utils/process-runner.js +63 -37
- package/dist/utils/project-detection.d.ts +90 -4
- package/dist/utils/project-detection.js +389 -63
- package/dist/utils/session-cookie-auth.d.ts +35 -0
- package/dist/utils/session-cookie-auth.js +99 -0
- package/dist/utils/sitevision-api.d.ts +72 -5
- package/dist/utils/sitevision-api.js +220 -38
- package/dist/utils/sitevision-scripts-runner.d.ts +2 -1
- package/dist/utils/sitevision-scripts-runner.js +18 -7
- package/dist/utils/tasks.d.ts +50 -0
- package/dist/utils/tasks.js +439 -0
- package/dist/utils/workspace.d.ts +17 -0
- package/dist/utils/workspace.js +67 -0
- package/package.json +5 -2
- package/readme.md +103 -121
|
@@ -13,6 +13,27 @@ import type { SigningCredentials, DeployConfig, ProductionDeployConfig, SigningR
|
|
|
13
13
|
* Create Basic Auth header value
|
|
14
14
|
*/
|
|
15
15
|
declare function createBasicAuth(username: string, password: string): string;
|
|
16
|
+
type RequestAuth = {
|
|
17
|
+
username: string;
|
|
18
|
+
password: string;
|
|
19
|
+
} | {
|
|
20
|
+
token: string;
|
|
21
|
+
} | {
|
|
22
|
+
cookie: string;
|
|
23
|
+
};
|
|
24
|
+
type AuthKind = 'basic' | 'bearer' | 'cookie';
|
|
25
|
+
/** Single source of the 401 message, worded for the auth kind actually used. */
|
|
26
|
+
declare function unauthorizedMessage(kind: AuthKind): string;
|
|
27
|
+
/** Pick cookie > bearer > basic based on what the deploy config carries. */
|
|
28
|
+
declare function configAuth(config: {
|
|
29
|
+
username: string;
|
|
30
|
+
password?: string;
|
|
31
|
+
accessToken?: string;
|
|
32
|
+
sessionCookie?: string;
|
|
33
|
+
}): {
|
|
34
|
+
auth: RequestAuth;
|
|
35
|
+
kind: AuthKind;
|
|
36
|
+
};
|
|
16
37
|
/**
|
|
17
38
|
* Make an HTTP/HTTPS request
|
|
18
39
|
*/
|
|
@@ -20,10 +41,7 @@ export declare function makeRequest(url: string, options: {
|
|
|
20
41
|
method: string;
|
|
21
42
|
headers?: Record<string, string>;
|
|
22
43
|
body?: Buffer;
|
|
23
|
-
auth?:
|
|
24
|
-
username: string;
|
|
25
|
-
password: string;
|
|
26
|
-
};
|
|
44
|
+
auth?: RequestAuth;
|
|
27
45
|
timeoutMs?: number;
|
|
28
46
|
}): Promise<{
|
|
29
47
|
statusCode: number;
|
|
@@ -45,6 +63,12 @@ export declare function summarizeErrorBody(body: Buffer, headers: Record<string,
|
|
|
45
63
|
* the signing endpoint returns an error page with HTTP 200.
|
|
46
64
|
*/
|
|
47
65
|
export declare function looksLikeZip(body: Buffer): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A stale Sitevision session usually answers with a redirect to the login page
|
|
68
|
+
* or a 200 carrying an HTML login form — not a clean 401. Detect both so cookie
|
|
69
|
+
* auth can drop the dead session and re-login instead of showing a generic error.
|
|
70
|
+
*/
|
|
71
|
+
export declare function looksLikeAuthExpired(statusCode: number, body: Buffer, headers: Record<string, string>): boolean;
|
|
48
72
|
/**
|
|
49
73
|
* Sign an app via developer.sitevision.se
|
|
50
74
|
*
|
|
@@ -85,4 +109,47 @@ export declare function createAddon(config: DeployConfig, appType: SimpleAppType
|
|
|
85
109
|
* @param appType - The app type (web, widget, rest)
|
|
86
110
|
*/
|
|
87
111
|
export declare function activateApp(executableId: string, config: DeployConfig, _appType: SimpleAppType): Promise<ActivationResponse>;
|
|
88
|
-
|
|
112
|
+
/**
|
|
113
|
+
* A version uploaded to a custom module, as reported by the
|
|
114
|
+
* ActivateCustomModuleExecutable GET endpoint.
|
|
115
|
+
*/
|
|
116
|
+
export interface Executable {
|
|
117
|
+
id: string;
|
|
118
|
+
name: string;
|
|
119
|
+
appIdentifier: string;
|
|
120
|
+
appVersion: string;
|
|
121
|
+
active: boolean;
|
|
122
|
+
}
|
|
123
|
+
export interface ListExecutablesResponse {
|
|
124
|
+
success: boolean;
|
|
125
|
+
executables?: Executable[];
|
|
126
|
+
error?: string;
|
|
127
|
+
authExpired?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* List the executables (uploaded versions) of the configured addon.
|
|
131
|
+
*/
|
|
132
|
+
export declare function listExecutables(config: DeployConfig): Promise<ListExecutablesResponse>;
|
|
133
|
+
export interface AddonNode {
|
|
134
|
+
id: string;
|
|
135
|
+
name: string;
|
|
136
|
+
type: string;
|
|
137
|
+
appType?: SimpleAppType;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* List the addons (custom modules) in the site's Addon Repository.
|
|
141
|
+
*/
|
|
142
|
+
export declare function listAddons(config: DeployConfig): Promise<{
|
|
143
|
+
success: boolean;
|
|
144
|
+
addons?: AddonNode[];
|
|
145
|
+
error?: string;
|
|
146
|
+
}>;
|
|
147
|
+
/**
|
|
148
|
+
* Whether an addon exists, judged from a listAddons result. A failed or empty
|
|
149
|
+
* listing is "unknown": a dead session can look exactly like an empty site.
|
|
150
|
+
*/
|
|
151
|
+
export declare function classifyAddon(listed: {
|
|
152
|
+
success: boolean;
|
|
153
|
+
addons?: AddonNode[];
|
|
154
|
+
}, name: string): 'missing' | 'present' | 'unknown';
|
|
155
|
+
export { createBasicAuth, configAuth, unauthorizedMessage };
|
|
@@ -12,7 +12,7 @@ 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
16
|
// =============================================================================
|
|
17
17
|
// CONSTANTS
|
|
18
18
|
// =============================================================================
|
|
@@ -33,6 +33,30 @@ const RETRY_BASE_DELAY_MS = 1000;
|
|
|
33
33
|
function createBasicAuth(username, password) {
|
|
34
34
|
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
35
35
|
}
|
|
36
|
+
/** Single source of the 401 message, worded for the auth kind actually used. */
|
|
37
|
+
function unauthorizedMessage(kind) {
|
|
38
|
+
switch (kind) {
|
|
39
|
+
case 'bearer':
|
|
40
|
+
return 'Unauthorized. The access token was rejected or has expired.';
|
|
41
|
+
case 'cookie':
|
|
42
|
+
return 'Unauthorized. The session cookie was rejected or has expired — log in again.';
|
|
43
|
+
default:
|
|
44
|
+
return 'Unauthorized. Check username and password.';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Pick cookie > bearer > basic based on what the deploy config carries. */
|
|
48
|
+
function configAuth(config) {
|
|
49
|
+
if (config.sessionCookie) {
|
|
50
|
+
return { auth: { cookie: config.sessionCookie }, kind: 'cookie' };
|
|
51
|
+
}
|
|
52
|
+
if (config.accessToken) {
|
|
53
|
+
return { auth: { token: config.accessToken }, kind: 'bearer' };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
auth: { username: config.username, password: config.password ?? '' },
|
|
57
|
+
kind: 'basic',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
36
60
|
/**
|
|
37
61
|
* Generate a random boundary for multipart form data
|
|
38
62
|
*/
|
|
@@ -71,7 +95,18 @@ export function makeRequest(url, options) {
|
|
|
71
95
|
...options.headers,
|
|
72
96
|
};
|
|
73
97
|
if (options.auth) {
|
|
74
|
-
|
|
98
|
+
if ('cookie' in options.auth) {
|
|
99
|
+
headers['Cookie'] = options.auth.cookie;
|
|
100
|
+
// Session-authenticated state-changing calls typically need this to
|
|
101
|
+
// pass Sitevision's CSRF guard, unlike Basic-auth requests.
|
|
102
|
+
headers['X-Requested-With'] ??= 'XMLHttpRequest';
|
|
103
|
+
}
|
|
104
|
+
else if ('token' in options.auth) {
|
|
105
|
+
headers['Authorization'] = `Bearer ${options.auth.token}`;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
headers['Authorization'] = createBasicAuth(options.auth.username, options.auth.password);
|
|
109
|
+
}
|
|
75
110
|
}
|
|
76
111
|
const requestOptions = {
|
|
77
112
|
hostname: parsedUrl.hostname,
|
|
@@ -125,16 +160,36 @@ async function delay(ms) {
|
|
|
125
160
|
*/
|
|
126
161
|
export function summarizeErrorBody(body, headers) {
|
|
127
162
|
const contentType = headers['content-type'] ?? 'unknown';
|
|
128
|
-
const
|
|
163
|
+
const declaredText = contentType.includes('text') ||
|
|
129
164
|
contentType.includes('json') ||
|
|
130
165
|
contentType.includes('xml');
|
|
131
|
-
|
|
132
|
-
|
|
166
|
+
const text = body.toString('utf8');
|
|
167
|
+
// Show the body when the server declares it text OR the bytes decode to
|
|
168
|
+
// mostly-printable text. Sitevision sometimes returns a text/JSON error with
|
|
169
|
+
// a missing or non-text content-type, and hiding it obscures the real cause.
|
|
170
|
+
if (declaredText || isMostlyPrintable(text)) {
|
|
171
|
+
const collapsed = text.replaceAll(/\s+/g, ' ').trim();
|
|
172
|
+
const max = 300;
|
|
173
|
+
const summary = collapsed.length > max ? collapsed.slice(0, max) + '…' : collapsed;
|
|
174
|
+
if (summary.length > 0)
|
|
175
|
+
return summary;
|
|
133
176
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
177
|
+
return `(${contentType}, ${body.length} bytes)`;
|
|
178
|
+
}
|
|
179
|
+
/** Heuristic: a decoded string is text if <10% of chars are control/undecodable. */
|
|
180
|
+
function isMostlyPrintable(text) {
|
|
181
|
+
if (text.length === 0)
|
|
182
|
+
return false;
|
|
183
|
+
let bad = 0;
|
|
184
|
+
for (const ch of text) {
|
|
185
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
186
|
+
// Undecodable byte (replacement char), or a control char other than
|
|
187
|
+
// tab/newline/vertical-tab/form-feed/carriage-return.
|
|
188
|
+
if (code === 0xff_fd || code < 9 || (code > 13 && code < 32)) {
|
|
189
|
+
bad++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return bad / text.length < 0.1;
|
|
138
193
|
}
|
|
139
194
|
/** ZIP local-file-header magic bytes: "PK\x03\x04". */
|
|
140
195
|
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
@@ -145,6 +200,31 @@ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
|
145
200
|
export function looksLikeZip(body) {
|
|
146
201
|
return body.length >= 4 && body.subarray(0, 4).equals(ZIP_MAGIC);
|
|
147
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* A stale Sitevision session usually answers with a redirect to the login page
|
|
205
|
+
* or a 200 carrying an HTML login form — not a clean 401. Detect both so cookie
|
|
206
|
+
* auth can drop the dead session and re-login instead of showing a generic error.
|
|
207
|
+
*/
|
|
208
|
+
export function looksLikeAuthExpired(statusCode, body, headers) {
|
|
209
|
+
if (statusCode === 401)
|
|
210
|
+
return true;
|
|
211
|
+
if (statusCode >= 300 && statusCode < 400)
|
|
212
|
+
return true;
|
|
213
|
+
if (statusCode === 200) {
|
|
214
|
+
const contentType = headers['content-type'] ?? '';
|
|
215
|
+
if (contentType.includes('html'))
|
|
216
|
+
return true;
|
|
217
|
+
const head = body
|
|
218
|
+
.subarray(0, 64)
|
|
219
|
+
.toString('utf8')
|
|
220
|
+
.trimStart()
|
|
221
|
+
.toLowerCase();
|
|
222
|
+
if (head.startsWith('<!doctype html') || head.startsWith('<html')) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
148
228
|
// =============================================================================
|
|
149
229
|
// SIGNING API
|
|
150
230
|
// =============================================================================
|
|
@@ -211,7 +291,7 @@ export async function signApp(zipPath, credentials, outputPath) {
|
|
|
211
291
|
// Auth failures will not resolve on retry.
|
|
212
292
|
return {
|
|
213
293
|
success: false,
|
|
214
|
-
error: '
|
|
294
|
+
error: unauthorizedMessage('basic'),
|
|
215
295
|
};
|
|
216
296
|
}
|
|
217
297
|
lastError = `Signing failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`;
|
|
@@ -256,9 +336,12 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
256
336
|
if (force) {
|
|
257
337
|
url += '?force=true';
|
|
258
338
|
}
|
|
259
|
-
//
|
|
339
|
+
// The import endpoints read the archive from a multipart part named "data"
|
|
340
|
+
// (per the webAppImport/restAppImport docs, and confirmed against a live
|
|
341
|
+
// server). Signing uses "file" against a different endpoint.
|
|
260
342
|
const boundary = generateBoundary();
|
|
261
|
-
const { body, contentType } = createMultipartFormData(zipPath, '
|
|
343
|
+
const { body, contentType } = createMultipartFormData(zipPath, 'data', boundary);
|
|
344
|
+
const { auth, kind } = configAuth(config);
|
|
262
345
|
try {
|
|
263
346
|
const response = await makeRequest(url, {
|
|
264
347
|
method: 'POST',
|
|
@@ -267,11 +350,25 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
267
350
|
'Content-Length': String(body.length),
|
|
268
351
|
},
|
|
269
352
|
body,
|
|
270
|
-
auth
|
|
271
|
-
username: config.username,
|
|
272
|
-
password: config.password,
|
|
273
|
-
},
|
|
353
|
+
auth,
|
|
274
354
|
});
|
|
355
|
+
if (response.statusCode === 401) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: unauthorizedMessage(kind),
|
|
359
|
+
authExpired: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// A cookie session fails without a clean 401: a redirect to login or a
|
|
363
|
+
// 200 carrying an HTML login page. Flag it so the caller re-authenticates.
|
|
364
|
+
if (kind === 'cookie' &&
|
|
365
|
+
looksLikeAuthExpired(response.statusCode, response.body, response.headers)) {
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
error: unauthorizedMessage('cookie'),
|
|
369
|
+
authExpired: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
275
372
|
if (response.statusCode === 200) {
|
|
276
373
|
// Try to parse response for executable ID
|
|
277
374
|
let executableId;
|
|
@@ -288,16 +385,18 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
288
385
|
message: 'Deployment successful',
|
|
289
386
|
};
|
|
290
387
|
}
|
|
291
|
-
if (response.statusCode ===
|
|
388
|
+
if (response.statusCode === 409) {
|
|
292
389
|
return {
|
|
293
390
|
success: false,
|
|
294
|
-
error: '
|
|
391
|
+
error: 'Conflict. Addon already exists. Use --force to overwrite.',
|
|
295
392
|
};
|
|
296
393
|
}
|
|
297
|
-
if (response.statusCode ===
|
|
394
|
+
if (response.statusCode === 400 &&
|
|
395
|
+
response.body.toString().includes('could not resolve context node')) {
|
|
298
396
|
return {
|
|
299
397
|
success: false,
|
|
300
|
-
error:
|
|
398
|
+
error: `Deployment failed with status 400: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
399
|
+
contextNodeMissing: true,
|
|
301
400
|
};
|
|
302
401
|
}
|
|
303
402
|
return {
|
|
@@ -354,10 +453,12 @@ export async function deployProduction(signedZipPath, config, appType) {
|
|
|
354
453
|
*/
|
|
355
454
|
export async function createAddon(config, appType) {
|
|
356
455
|
const url = buildAddonEndpointUrl(config.domain, config.siteName, appType, config.useHTTP);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
456
|
+
const payload = { name: config.addonName };
|
|
457
|
+
// Only the custommodule endpoints take (and require) a category.
|
|
458
|
+
if (appType === 'web' || appType === 'widget')
|
|
459
|
+
payload['category'] = 'Other';
|
|
460
|
+
const body = JSON.stringify(payload);
|
|
461
|
+
const { auth, kind } = configAuth(config);
|
|
361
462
|
try {
|
|
362
463
|
const response = await makeRequest(url, {
|
|
363
464
|
method: 'POST',
|
|
@@ -366,10 +467,7 @@ export async function createAddon(config, appType) {
|
|
|
366
467
|
'Content-Length': String(Buffer.byteLength(body)),
|
|
367
468
|
},
|
|
368
469
|
body: Buffer.from(body),
|
|
369
|
-
auth
|
|
370
|
-
username: config.username,
|
|
371
|
-
password: config.password,
|
|
372
|
-
},
|
|
470
|
+
auth,
|
|
373
471
|
});
|
|
374
472
|
if (response.statusCode === 200 || response.statusCode === 201) {
|
|
375
473
|
let addonId;
|
|
@@ -388,7 +486,8 @@ export async function createAddon(config, appType) {
|
|
|
388
486
|
if (response.statusCode === 401) {
|
|
389
487
|
return {
|
|
390
488
|
success: false,
|
|
391
|
-
error:
|
|
489
|
+
error: unauthorizedMessage(kind),
|
|
490
|
+
authExpired: true,
|
|
392
491
|
};
|
|
393
492
|
}
|
|
394
493
|
if (response.statusCode === 409) {
|
|
@@ -419,9 +518,9 @@ export async function createAddon(config, appType) {
|
|
|
419
518
|
export async function activateApp(executableId, config, _appType) {
|
|
420
519
|
const protocol = config.useHTTP ? 'http' : 'https';
|
|
421
520
|
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
});
|
|
521
|
+
// Property name per the ActivateCustomModuleExecutable PUT docs.
|
|
522
|
+
const body = JSON.stringify({ customModuleExecutableId: executableId });
|
|
523
|
+
const { auth, kind } = configAuth(config);
|
|
425
524
|
try {
|
|
426
525
|
const response = await makeRequest(url, {
|
|
427
526
|
method: 'PUT',
|
|
@@ -430,10 +529,7 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
430
529
|
'Content-Length': String(Buffer.byteLength(body)),
|
|
431
530
|
},
|
|
432
531
|
body: Buffer.from(body),
|
|
433
|
-
auth
|
|
434
|
-
username: config.username,
|
|
435
|
-
password: config.password,
|
|
436
|
-
},
|
|
532
|
+
auth,
|
|
437
533
|
});
|
|
438
534
|
if (response.statusCode === 200) {
|
|
439
535
|
return { success: true };
|
|
@@ -441,7 +537,8 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
441
537
|
if (response.statusCode === 401) {
|
|
442
538
|
return {
|
|
443
539
|
success: false,
|
|
444
|
-
error:
|
|
540
|
+
error: unauthorizedMessage(kind),
|
|
541
|
+
authExpired: true,
|
|
445
542
|
};
|
|
446
543
|
}
|
|
447
544
|
return {
|
|
@@ -456,7 +553,92 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
456
553
|
};
|
|
457
554
|
}
|
|
458
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* List the executables (uploaded versions) of the configured addon.
|
|
558
|
+
*/
|
|
559
|
+
export async function listExecutables(config) {
|
|
560
|
+
const protocol = config.useHTTP ? 'http' : 'https';
|
|
561
|
+
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
562
|
+
const { auth, kind } = configAuth(config);
|
|
563
|
+
try {
|
|
564
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
565
|
+
if (response.statusCode === 401) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
error: unauthorizedMessage(kind),
|
|
569
|
+
authExpired: true,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
if (response.statusCode !== 200) {
|
|
573
|
+
return {
|
|
574
|
+
success: false,
|
|
575
|
+
error: `Listing versions failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const data = JSON.parse(response.body.toString());
|
|
579
|
+
return { success: true, executables: data.executables ?? [] };
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: `Listing versions failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const ADDON_TYPES = {
|
|
589
|
+
'sv:customModule': 'web',
|
|
590
|
+
'sv:marketplaceCustomModule': 'web',
|
|
591
|
+
'sv:widgetCustomModule': 'widget',
|
|
592
|
+
'sv:marketplaceWidgetCustomModule': 'widget',
|
|
593
|
+
'sv:headlessCustomModule': 'rest',
|
|
594
|
+
'sv:marketplaceHeadlessCustomModule': 'rest',
|
|
595
|
+
'sv:mcpServerCustomModule': 'mcp',
|
|
596
|
+
};
|
|
597
|
+
/**
|
|
598
|
+
* List the addons (custom modules) in the site's Addon Repository.
|
|
599
|
+
*/
|
|
600
|
+
export async function listAddons(config) {
|
|
601
|
+
const url = `${buildApiBaseUrl(config.domain, config.siteName, config.useHTTP)}/Addon%20Repository/nodes`;
|
|
602
|
+
const { auth, kind } = configAuth(config);
|
|
603
|
+
try {
|
|
604
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
605
|
+
if (response.statusCode === 401) {
|
|
606
|
+
return { success: false, error: unauthorizedMessage(kind) };
|
|
607
|
+
}
|
|
608
|
+
if (response.statusCode !== 200) {
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
error: `Listing addons failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const nodes = JSON.parse(response.body.toString());
|
|
615
|
+
return {
|
|
616
|
+
success: true,
|
|
617
|
+
addons: nodes
|
|
618
|
+
.filter(node => Object.hasOwn(ADDON_TYPES, node.type))
|
|
619
|
+
.map(node => ({ ...node, appType: ADDON_TYPES[node.type] })),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
return {
|
|
624
|
+
success: false,
|
|
625
|
+
error: `Listing addons failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Whether an addon exists, judged from a listAddons result. A failed or empty
|
|
631
|
+
* listing is "unknown": a dead session can look exactly like an empty site.
|
|
632
|
+
*/
|
|
633
|
+
export function classifyAddon(listed, name) {
|
|
634
|
+
if (!listed.success || !listed.addons?.length)
|
|
635
|
+
return 'unknown';
|
|
636
|
+
const wanted = name.toLowerCase();
|
|
637
|
+
return listed.addons.some(addon => addon.name.toLowerCase() === wanted)
|
|
638
|
+
? 'present'
|
|
639
|
+
: 'missing';
|
|
640
|
+
}
|
|
459
641
|
// =============================================================================
|
|
460
642
|
// HELPER EXPORTS
|
|
461
643
|
// =============================================================================
|
|
462
|
-
export { createBasicAuth };
|
|
644
|
+
export { createBasicAuth, configAuth, unauthorizedMessage };
|
|
@@ -79,5 +79,6 @@ export interface SitevisionBuildResult {
|
|
|
79
79
|
*
|
|
80
80
|
* @param projectRoot - Project root directory (used as cwd)
|
|
81
81
|
* @param onOutput - Optional callback for streaming output chunks
|
|
82
|
+
* @param signal - Aborting kills the build and everything it started
|
|
82
83
|
*/
|
|
83
|
-
export declare function runSitevisionScriptsBuild(projectRoot: string, onOutput?: (chunk: string) => void): Promise<SitevisionBuildResult>;
|
|
84
|
+
export declare function runSitevisionScriptsBuild(projectRoot: string, onOutput?: (chunk: string) => void, signal?: AbortSignal): Promise<SitevisionBuildResult>;
|
|
@@ -17,7 +17,7 @@
|
|
|
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
21
|
/**
|
|
22
22
|
* Resolve the path to the sitevision-scripts CLI entry inside a project.
|
|
23
23
|
* Returns null if the package is not installed.
|
|
@@ -145,8 +145,9 @@ const MAX_OUTPUT_CHARS = 50_000;
|
|
|
145
145
|
*
|
|
146
146
|
* @param projectRoot - Project root directory (used as cwd)
|
|
147
147
|
* @param onOutput - Optional callback for streaming output chunks
|
|
148
|
+
* @param signal - Aborting kills the build and everything it started
|
|
148
149
|
*/
|
|
149
|
-
export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
150
|
+
export async function runSitevisionScriptsBuild(projectRoot, onOutput, signal) {
|
|
150
151
|
const bin = getSitevisionScriptsBin(projectRoot);
|
|
151
152
|
if (!bin) {
|
|
152
153
|
return {
|
|
@@ -157,10 +158,16 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
157
158
|
}
|
|
158
159
|
return new Promise(resolve => {
|
|
159
160
|
let output = '';
|
|
160
|
-
const child =
|
|
161
|
+
const child = spawnChild(process.execPath, [bin, 'build'], {
|
|
161
162
|
cwd: projectRoot,
|
|
162
163
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
164
|
});
|
|
165
|
+
const abort = () => {
|
|
166
|
+
killChild(child);
|
|
167
|
+
};
|
|
168
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
169
|
+
if (signal?.aborted)
|
|
170
|
+
abort();
|
|
164
171
|
const handleData = (data) => {
|
|
165
172
|
const text = data.toString();
|
|
166
173
|
output += text;
|
|
@@ -175,12 +182,16 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
175
182
|
resolve({ success: false, output, error: error.message });
|
|
176
183
|
});
|
|
177
184
|
child.on('close', code => {
|
|
185
|
+
signal?.removeEventListener('abort', abort);
|
|
186
|
+
const stopped = signal?.aborted ?? false;
|
|
178
187
|
resolve({
|
|
179
|
-
success: code === 0,
|
|
188
|
+
success: code === 0 && !stopped,
|
|
180
189
|
output,
|
|
181
|
-
error:
|
|
182
|
-
?
|
|
183
|
-
:
|
|
190
|
+
error: stopped
|
|
191
|
+
? 'Build stopped.'
|
|
192
|
+
: code === 0
|
|
193
|
+
? undefined
|
|
194
|
+
: `sitevision-scripts build exited with code ${code}`,
|
|
184
195
|
});
|
|
185
196
|
});
|
|
186
197
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ProjectInfo, SigningCredentials, DeployConfig } from '../types/index.js';
|
|
2
|
+
export type TaskKind = 'dev' | 'watch' | 'build' | 'sign' | 'deploy' | 'activate' | 'install';
|
|
3
|
+
export type TaskStatus = 'running' | 'success' | 'error' | 'stopped';
|
|
4
|
+
export type LogLevel = 'info' | 'ok' | 'warn' | 'error';
|
|
5
|
+
export interface LogLine {
|
|
6
|
+
time: number;
|
|
7
|
+
tag: string;
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Task {
|
|
12
|
+
id: number;
|
|
13
|
+
kind: TaskKind;
|
|
14
|
+
appRoot: string;
|
|
15
|
+
appName: string;
|
|
16
|
+
label: string;
|
|
17
|
+
status: TaskStatus;
|
|
18
|
+
phase: string;
|
|
19
|
+
startedAt: number;
|
|
20
|
+
endedAt?: number;
|
|
21
|
+
lines: LogLine[];
|
|
22
|
+
error?: string;
|
|
23
|
+
stop: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function getTasks(): Task[];
|
|
26
|
+
export declare function useTasks(): Task[];
|
|
27
|
+
export declare function runningTasks(appRoot?: string): Task[];
|
|
28
|
+
export declare function clearFinished(): void;
|
|
29
|
+
export interface DeployOptions {
|
|
30
|
+
force?: boolean;
|
|
31
|
+
production?: boolean;
|
|
32
|
+
activate?: boolean;
|
|
33
|
+
onAddonMissing?: (addonName: string) => Promise<boolean>;
|
|
34
|
+
}
|
|
35
|
+
export declare function startBuild(project: ProjectInfo): Task;
|
|
36
|
+
export declare function startSign(project: ProjectInfo, credentials: SigningCredentials): Task;
|
|
37
|
+
export declare function startDeploy(project: ProjectInfo, config: DeployConfig, options: DeployOptions): Task;
|
|
38
|
+
export declare function startActivate(project: ProjectInfo, config: DeployConfig, executableId: string, versionLabel: string): Task;
|
|
39
|
+
export declare function startInstall(project: ProjectInfo): Task;
|
|
40
|
+
export interface DevOptions {
|
|
41
|
+
deploy: boolean;
|
|
42
|
+
signingCredentials?: SigningCredentials;
|
|
43
|
+
deployConfig?: DeployConfig;
|
|
44
|
+
onAddonMissing?: DeployOptions['onAddonMissing'];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Dev / watch loop: build on every source change, then optionally sign and
|
|
48
|
+
* deploy. Runs until `task.stop()`; the task stays in the registry meanwhile.
|
|
49
|
+
*/
|
|
50
|
+
export declare function startDev(project: ProjectInfo, options: DevOptions): Task;
|