sitevision-cli 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +1 -1
- package/dist/app.js +59 -8
- package/dist/cli.js +96 -39
- package/dist/commands/build.js +1 -1
- package/dist/commands/deploy.d.ts +2 -2
- package/dist/commands/deploy.js +135 -25
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +77 -366
- package/dist/commands/info.js +2 -2
- 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/InfoScreen.js +2 -2
- package/dist/components/MainMenu.js +7 -2
- package/dist/components/PasswordInput.js +2 -1
- package/dist/components/SetupFlow.d.ts +2 -1
- package/dist/components/SetupFlow.js +100 -11
- package/dist/shell/AddonPicker.d.ts +14 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/CommandPalette.d.ts +8 -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 +134 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +9 -0
- package/dist/shell/Shell.js +586 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +90 -0
- package/dist/shell/actions.d.ts +45 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +44 -5
- 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 +279 -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/project-detection.d.ts +105 -6
- package/dist/utils/project-detection.js +411 -54
- 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 +64 -5
- package/dist/utils/sitevision-api.js +195 -33
- package/dist/utils/tasks.d.ts +48 -0
- package/dist/utils/tasks.js +371 -0
- package/dist/utils/workspace.d.ts +17 -0
- package/dist/utils/workspace.js +67 -0
- package/package.json +3 -1
- package/readme.md +102 -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,39 @@ 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
|
+
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,12 +385,6 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
288
385
|
message: 'Deployment successful',
|
|
289
386
|
};
|
|
290
387
|
}
|
|
291
|
-
if (response.statusCode === 401) {
|
|
292
|
-
return {
|
|
293
|
-
success: false,
|
|
294
|
-
error: 'Unauthorized. Check username and password.',
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
388
|
if (response.statusCode === 409) {
|
|
298
389
|
return {
|
|
299
390
|
success: false,
|
|
@@ -358,6 +449,7 @@ export async function createAddon(config, appType) {
|
|
|
358
449
|
name: config.addonName,
|
|
359
450
|
category: 'Other',
|
|
360
451
|
});
|
|
452
|
+
const { auth, kind } = configAuth(config);
|
|
361
453
|
try {
|
|
362
454
|
const response = await makeRequest(url, {
|
|
363
455
|
method: 'POST',
|
|
@@ -366,10 +458,7 @@ export async function createAddon(config, appType) {
|
|
|
366
458
|
'Content-Length': String(Buffer.byteLength(body)),
|
|
367
459
|
},
|
|
368
460
|
body: Buffer.from(body),
|
|
369
|
-
auth
|
|
370
|
-
username: config.username,
|
|
371
|
-
password: config.password,
|
|
372
|
-
},
|
|
461
|
+
auth,
|
|
373
462
|
});
|
|
374
463
|
if (response.statusCode === 200 || response.statusCode === 201) {
|
|
375
464
|
let addonId;
|
|
@@ -388,7 +477,8 @@ export async function createAddon(config, appType) {
|
|
|
388
477
|
if (response.statusCode === 401) {
|
|
389
478
|
return {
|
|
390
479
|
success: false,
|
|
391
|
-
error:
|
|
480
|
+
error: unauthorizedMessage(kind),
|
|
481
|
+
authExpired: true,
|
|
392
482
|
};
|
|
393
483
|
}
|
|
394
484
|
if (response.statusCode === 409) {
|
|
@@ -422,6 +512,7 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
422
512
|
const body = JSON.stringify({
|
|
423
513
|
executableId,
|
|
424
514
|
});
|
|
515
|
+
const { auth, kind } = configAuth(config);
|
|
425
516
|
try {
|
|
426
517
|
const response = await makeRequest(url, {
|
|
427
518
|
method: 'PUT',
|
|
@@ -430,10 +521,7 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
430
521
|
'Content-Length': String(Buffer.byteLength(body)),
|
|
431
522
|
},
|
|
432
523
|
body: Buffer.from(body),
|
|
433
|
-
auth
|
|
434
|
-
username: config.username,
|
|
435
|
-
password: config.password,
|
|
436
|
-
},
|
|
524
|
+
auth,
|
|
437
525
|
});
|
|
438
526
|
if (response.statusCode === 200) {
|
|
439
527
|
return { success: true };
|
|
@@ -441,7 +529,8 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
441
529
|
if (response.statusCode === 401) {
|
|
442
530
|
return {
|
|
443
531
|
success: false,
|
|
444
|
-
error:
|
|
532
|
+
error: unauthorizedMessage(kind),
|
|
533
|
+
authExpired: true,
|
|
445
534
|
};
|
|
446
535
|
}
|
|
447
536
|
return {
|
|
@@ -456,7 +545,80 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
456
545
|
};
|
|
457
546
|
}
|
|
458
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* List the executables (uploaded versions) of the configured addon.
|
|
550
|
+
*/
|
|
551
|
+
export async function listExecutables(config) {
|
|
552
|
+
const protocol = config.useHTTP ? 'http' : 'https';
|
|
553
|
+
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
554
|
+
const { auth, kind } = configAuth(config);
|
|
555
|
+
try {
|
|
556
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
557
|
+
if (response.statusCode === 401) {
|
|
558
|
+
return {
|
|
559
|
+
success: false,
|
|
560
|
+
error: unauthorizedMessage(kind),
|
|
561
|
+
authExpired: true,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (response.statusCode !== 200) {
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
error: `Listing versions failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const data = JSON.parse(response.body.toString());
|
|
571
|
+
return { success: true, executables: data.executables ?? [] };
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
return {
|
|
575
|
+
success: false,
|
|
576
|
+
error: `Listing versions failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const ADDON_TYPES = {
|
|
581
|
+
'sv:customModule': 'web',
|
|
582
|
+
'sv:marketplaceCustomModule': 'web',
|
|
583
|
+
'sv:widgetCustomModule': 'widget',
|
|
584
|
+
'sv:marketplaceWidgetCustomModule': 'widget',
|
|
585
|
+
'sv:headlessCustomModule': 'rest',
|
|
586
|
+
'sv:marketplaceHeadlessCustomModule': 'rest',
|
|
587
|
+
'sv:mcpServerCustomModule': 'mcp',
|
|
588
|
+
};
|
|
589
|
+
/**
|
|
590
|
+
* List the addons (custom modules) in the site's Addon Repository.
|
|
591
|
+
*/
|
|
592
|
+
export async function listAddons(config) {
|
|
593
|
+
const url = `${buildApiBaseUrl(config.domain, config.siteName, config.useHTTP)}/Addon%20Repository/nodes`;
|
|
594
|
+
const { auth, kind } = configAuth(config);
|
|
595
|
+
try {
|
|
596
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
597
|
+
if (response.statusCode === 401) {
|
|
598
|
+
return { success: false, error: unauthorizedMessage(kind) };
|
|
599
|
+
}
|
|
600
|
+
if (response.statusCode !== 200) {
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: `Listing addons failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const nodes = JSON.parse(response.body.toString());
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
addons: nodes
|
|
610
|
+
.filter(node => Object.hasOwn(ADDON_TYPES, node.type))
|
|
611
|
+
.map(node => ({ ...node, appType: ADDON_TYPES[node.type] })),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
error: `Listing addons failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
459
621
|
// =============================================================================
|
|
460
622
|
// HELPER EXPORTS
|
|
461
623
|
// =============================================================================
|
|
462
|
-
export { createBasicAuth };
|
|
624
|
+
export { createBasicAuth, configAuth, unauthorizedMessage };
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
}
|
|
34
|
+
export declare function startBuild(project: ProjectInfo): Task;
|
|
35
|
+
export declare function startSign(project: ProjectInfo, credentials: SigningCredentials): Task;
|
|
36
|
+
export declare function startDeploy(project: ProjectInfo, config: DeployConfig, options: DeployOptions): Task;
|
|
37
|
+
export declare function startActivate(project: ProjectInfo, config: DeployConfig, executableId: string, versionLabel: string): Task;
|
|
38
|
+
export declare function startInstall(project: ProjectInfo): Task;
|
|
39
|
+
export interface DevOptions {
|
|
40
|
+
deploy: boolean;
|
|
41
|
+
signingCredentials?: SigningCredentials;
|
|
42
|
+
deployConfig?: DeployConfig;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Dev / watch loop: build on every source change, then optionally sign and
|
|
46
|
+
* deploy. Runs until `task.stop()`; the task stays in the registry meanwhile.
|
|
47
|
+
*/
|
|
48
|
+
export declare function startDev(project: ProjectInfo, options: DevOptions): Task;
|