sitevision-cli 1.0.0-beta.12 → 1.0.0-beta.14

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 (43) hide show
  1. package/dist/cli.js +70 -41
  2. package/dist/commands/build.js +1 -1
  3. package/dist/commands/dev.d.ts +8 -10
  4. package/dist/commands/dev.js +75 -390
  5. package/dist/commands/watch.js +5 -23
  6. package/dist/components/AuthLoginScreen.js +4 -3
  7. package/dist/components/DevPropertiesForm.js +6 -3
  8. package/dist/components/PasswordInput.js +2 -1
  9. package/dist/shell/AddonPicker.d.ts +14 -0
  10. package/dist/shell/AddonPicker.js +54 -0
  11. package/dist/shell/CommandPalette.d.ts +8 -0
  12. package/dist/shell/CommandPalette.js +63 -0
  13. package/dist/shell/ConfigForm.d.ts +35 -0
  14. package/dist/shell/ConfigForm.js +472 -0
  15. package/dist/shell/Frame.d.ts +52 -0
  16. package/dist/shell/Frame.js +98 -0
  17. package/dist/shell/Settings.d.ts +6 -0
  18. package/dist/shell/Settings.js +96 -0
  19. package/dist/shell/Shell.d.ts +8 -0
  20. package/dist/shell/Shell.js +520 -0
  21. package/dist/shell/Tabs.d.ts +36 -0
  22. package/dist/shell/Tabs.js +85 -0
  23. package/dist/shell/actions.d.ts +45 -0
  24. package/dist/shell/actions.js +0 -0
  25. package/dist/types/index.d.ts +12 -2
  26. package/dist/utils/config.d.ts +10 -0
  27. package/dist/utils/config.js +14 -0
  28. package/dist/utils/environments.d.ts +20 -0
  29. package/dist/utils/environments.js +74 -0
  30. package/dist/utils/i18n.d.ts +12 -0
  31. package/dist/utils/i18n.js +263 -0
  32. package/dist/utils/oauth2-auth.d.ts +26 -1
  33. package/dist/utils/oauth2-auth.js +75 -32
  34. package/dist/utils/project-detection.d.ts +23 -1
  35. package/dist/utils/project-detection.js +124 -51
  36. package/dist/utils/sitevision-api.d.ts +35 -0
  37. package/dist/utils/sitevision-api.js +74 -1
  38. package/dist/utils/tasks.d.ts +48 -0
  39. package/dist/utils/tasks.js +371 -0
  40. package/dist/utils/workspace.d.ts +8 -0
  41. package/dist/utils/workspace.js +48 -0
  42. package/package.json +1 -1
  43. package/readme.md +76 -24
@@ -1,10 +1,13 @@
1
1
  import http from 'http';
2
2
  import crypto from 'crypto';
3
3
  import { spawn } from 'child_process';
4
- import { makeRequest } from './sitevision-api.js';
4
+ import { makeRequest, summarizeErrorBody } from './sitevision-api.js';
5
5
  import { getOAuth2RefreshToken, setOAuth2RefreshToken, deleteOAuth2RefreshToken, getOAuth2ClientSecret, } from './keychain.js';
6
6
  /** Default loopback port. Fixed so a single redirect URI can be whitelisted. */
7
7
  export const DEFAULT_REDIRECT_PORT = 8137;
8
+ // Sitevision's provider grants everything under ALL; offline_access adds the
9
+ // refresh token so later runs log in silently.
10
+ export const DEFAULT_SCOPES = ['ALL', 'offline_access'];
8
11
  /** Seconds to wait for the user to finish logging in before giving up. */
9
12
  const LOGIN_TIMEOUT_MS = 300_000;
10
13
  function base64url(buffer) {
@@ -36,12 +39,17 @@ async function postToken(config, params, secret) {
36
39
  // client_secret_basic when confidential; public+PKCE clients omit it.
37
40
  auth: secret ? { username: config.clientId, password: secret } : undefined,
38
41
  });
39
- if (response.statusCode !== 200)
40
- return null;
41
- return JSON.parse(response.body.toString());
42
+ if (response.statusCode !== 200) {
43
+ return {
44
+ error: `Token endpoint returned ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
45
+ };
46
+ }
47
+ return { tokens: JSON.parse(response.body.toString()) };
42
48
  }
43
- catch {
44
- return null;
49
+ catch (error) {
50
+ return {
51
+ error: `Token request failed: ${error instanceof Error ? error.message : String(error)}`,
52
+ };
45
53
  }
46
54
  }
47
55
  /** OpenID configuration path (published at the issuer root once the provider is saved). */
@@ -87,14 +95,37 @@ export function openBrowser(url) {
87
95
  }
88
96
  }
89
97
  /**
90
- * Serve the loopback redirect once. Returns the awaited code and a `close()`
91
- * that shuts the server down (freeing the port) if the login is cancelled — so
92
- * a retry doesn't hit an EADDRINUSE on the fixed redirect port.
98
+ * Turn a redirect's query params into a result, prioritizing the provider's own
99
+ * error (the most useful reason) over a generic "no code". Exported for testing.
93
100
  */
101
+ export function classifyRedirect(params) {
102
+ if (params.error) {
103
+ return {
104
+ error: `The OAuth2 provider rejected the login: ${params.errorDescription
105
+ ? `${params.error} — ${params.errorDescription}`
106
+ : params.error}`,
107
+ };
108
+ }
109
+ if (params.state !== params.expectedState) {
110
+ return {
111
+ error: 'State mismatch — the login response did not match this request (a stale browser tab, or the wrong window).',
112
+ };
113
+ }
114
+ if (params.code) {
115
+ return { code: params.code };
116
+ }
117
+ return { error: 'No authorization code was returned by the provider.' };
118
+ }
119
+ function escapeHtml(text) {
120
+ return text
121
+ .replaceAll('&', '&')
122
+ .replaceAll('<', '&lt;')
123
+ .replaceAll('>', '&gt;');
124
+ }
94
125
  function startLoopback(port, state) {
95
126
  let finish;
96
127
  let settled = false;
97
- const code = new Promise(resolve => {
128
+ const result = new Promise(resolve => {
98
129
  finish = (value) => {
99
130
  if (settled)
100
131
  return;
@@ -110,19 +141,29 @@ function startLoopback(port, state) {
110
141
  res.writeHead(404).end();
111
142
  return;
112
143
  }
113
- const ok = url.searchParams.get('state') === state;
114
- const authCode = url.searchParams.get('code');
115
- const message = ok && authCode
144
+ const outcome = classifyRedirect({
145
+ expectedState: state,
146
+ state: url.searchParams.get('state'),
147
+ error: url.searchParams.get('error'),
148
+ errorDescription: url.searchParams.get('error_description'),
149
+ code: url.searchParams.get('code'),
150
+ });
151
+ const message = outcome.code
116
152
  ? 'Login complete. You can close this window and return to the terminal.'
117
- : 'Login failed. Check the terminal.';
153
+ : `Login failed: ${outcome.error}`;
118
154
  res.writeHead(200, { 'Content-Type': 'text/html' });
119
- res.end(`<!doctype html><meta charset="utf-8"><p>${message}</p>`);
120
- finish(ok ? authCode : null);
155
+ res.end(`<!doctype html><meta charset="utf-8"><p>${escapeHtml(message)}</p>`);
156
+ finish(outcome);
121
157
  });
122
- const timer = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
123
- server.on('error', () => finish(null));
158
+ const timer = setTimeout(() => finish({ error: 'Timed out waiting for the login to complete.' }), LOGIN_TIMEOUT_MS);
159
+ server.on('error', error => finish({
160
+ error: `Local login server error: ${error instanceof Error ? error.message : String(error)}`,
161
+ }));
124
162
  server.listen(port, '127.0.0.1');
125
- return { code, close: () => finish(null) };
163
+ return {
164
+ result,
165
+ close: (reason) => finish({ error: reason ?? 'Login cancelled.' }),
166
+ };
126
167
  }
127
168
  /**
128
169
  * Start an interactive OAuth2 login. Returns the authorize URL to open and a
@@ -147,29 +188,31 @@ export function beginOAuth2Login(dev) {
147
188
  url.searchParams.set('state', state);
148
189
  url.searchParams.set('code_challenge', challenge);
149
190
  url.searchParams.set('code_challenge_method', 'S256');
150
- if (config.scopes?.length) {
151
- url.searchParams.set('scope', config.scopes.join(' '));
152
- }
191
+ url.searchParams.set('scope', (config.scopes ?? DEFAULT_SCOPES).join(' '));
153
192
  const loopback = startLoopback(port, state);
154
193
  const complete = async () => {
155
- const code = await loopback.code;
156
- if (!code)
157
- return null;
158
- const tokens = await postToken(config, {
194
+ const redirect = await loopback.result;
195
+ if (redirect.error || !redirect.code) {
196
+ return { error: redirect.error ?? 'Login failed.' };
197
+ }
198
+ const { tokens, error } = await postToken(config, {
159
199
  grant_type: 'authorization_code',
160
- code,
200
+ code: redirect.code,
161
201
  redirect_uri: redirectUri,
162
202
  client_id: config.clientId,
163
203
  code_verifier: verifier,
164
204
  }, secret);
165
- if (!tokens?.access_token)
166
- return null;
205
+ if (error)
206
+ return { error };
207
+ if (!tokens?.access_token) {
208
+ return { error: 'The token endpoint did not return an access token.' };
209
+ }
167
210
  if (tokens.refresh_token) {
168
211
  setOAuth2RefreshToken(domain, config.clientId, tokens.refresh_token);
169
212
  }
170
- return tokens.access_token;
213
+ return { token: tokens.access_token };
171
214
  };
172
- return { authUrl: url.href, complete, cancel: loopback.close };
215
+ return { authUrl: url.href, complete, cancel: () => loopback.close() };
173
216
  }
174
217
  /**
175
218
  * Silently resolve an access token by refreshing the keychain refresh token.
@@ -186,7 +229,7 @@ export async function resolveOAuth2AccessToken(dev) {
186
229
  const storedRefresh = getOAuth2RefreshToken(domain, config.clientId);
187
230
  if (!storedRefresh)
188
231
  return null;
189
- const tokens = await postToken(config, {
232
+ const { tokens } = await postToken(config, {
190
233
  grant_type: 'refresh_token',
191
234
  refresh_token: storedRefresh,
192
235
  client_id: config.clientId,
@@ -22,6 +22,14 @@ export declare function findDevPropertiesPath(root: string): string | null;
22
22
  * Get the default dev properties path (for creating new files)
23
23
  */
24
24
  export declare function getDefaultDevPropertiesPath(root: string): string;
25
+ /**
26
+ * The dev properties a workspace root defines (its own file merged over any
27
+ * ancestors'), with the deploy password resolved from the keychain like an
28
+ * app's would be. Used to edit shared config from the shell.
29
+ */
30
+ export declare function readWorkspaceDevProperties(root: string): Partial<DevProperties>;
31
+ /** Dev properties inherited from ancestor directories only (no own file). */
32
+ export declare function readInheritedDevProperties(root: string): Partial<DevProperties>;
25
33
  /**
26
34
  * Get the full app ID including any prefix/suffix from environment
27
35
  *
@@ -81,12 +89,25 @@ export declare function readManifest(cwd: string): {
81
89
  * Detect if the current directory is a Sitevision project
82
90
  */
83
91
  export declare function detectProject(cwd?: string): ProjectInfo | null;
92
+ /**
93
+ * Fill the runtime-only credential fields for the given domain/username:
94
+ * deploy password (env var > keychain), OAuth2 access token (env var), and
95
+ * session cookie (env var > keychain). Mutates and returns `dev`.
96
+ */
97
+ export declare function resolveRuntimeSecrets(dev: DevProperties): DevProperties;
84
98
  /**
85
99
  * Validate that we're in a Sitevision project directory
86
100
  */
87
101
  export declare function requireProject(cwd?: string): ProjectInfo;
88
102
  /**
89
- * Get the app type (web, widget, rest)
103
+ * The app type (web, widget, rest, mcp), or undefined for a manifest type
104
+ * this CLI does not know. Display code uses this so one odd app never takes
105
+ * the whole shell down.
106
+ */
107
+ export declare function appTypeOf(manifest: SitevisionManifest): SimpleAppType | undefined;
108
+ /**
109
+ * Get the app type (web, widget, rest, mcp). Throws for unknown types, since
110
+ * build and deploy cannot proceed without knowing the endpoints.
90
111
  */
91
112
  export declare function getAppType(manifest: SitevisionManifest): SimpleAppType;
92
113
  /**
@@ -109,6 +130,7 @@ export declare function writeDevProperties(projectRoot: string, properties: DevP
109
130
  */
110
131
  export interface SvcConfig {
111
132
  syncPackageJson?: boolean;
133
+ environment?: string;
112
134
  [key: string]: unknown;
113
135
  }
114
136
  export declare function readSvcConfig(projectRoot: string): SvcConfig;
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getDeployPassword, setDeployPassword, getSessionCookie, } from './keychain.js';
4
4
  import { parseJsonc } from './jsonc.js';
5
+ import { getLanguage } from './i18n.js';
5
6
  // =============================================================================
6
7
  // LOCALIZED TEXT
7
8
  // =============================================================================
@@ -14,7 +15,7 @@ import { parseJsonc } from './jsonc.js';
14
15
  * raw object as a React child, which Sitevision's localized manifests would
15
16
  * otherwise trigger.
16
17
  */
17
- export function localizedText(value, preferred = 'sv') {
18
+ export function localizedText(value, preferred = getLanguage()) {
18
19
  if (!value)
19
20
  return '';
20
21
  if (typeof value === 'string')
@@ -61,6 +62,62 @@ export function findDevPropertiesPath(root) {
61
62
  export function getDefaultDevPropertiesPath(root) {
62
63
  return path.join(root, '.dev_properties.json');
63
64
  }
65
+ /**
66
+ * Ancestor directories of `root` (outermost first) up to and including the
67
+ * nearest one containing `.git`, or the filesystem root. Each may carry a
68
+ * `.dev_properties.json` whose values the app inherits (nearest wins).
69
+ */
70
+ function ancestorDirs(root) {
71
+ const dirs = [];
72
+ let dir = path.dirname(root);
73
+ while (true) {
74
+ dirs.unshift(dir);
75
+ if (fs.existsSync(path.join(dir, '.git')))
76
+ break;
77
+ const parent = path.dirname(dir);
78
+ if (parent === dir)
79
+ break;
80
+ dir = parent;
81
+ }
82
+ return dirs;
83
+ }
84
+ function readDevPropertiesFile(dir) {
85
+ const file = findDevPropertiesPath(dir);
86
+ if (!file)
87
+ return null;
88
+ try {
89
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * The dev properties a workspace root defines (its own file merged over any
97
+ * ancestors'), with the deploy password resolved from the keychain like an
98
+ * app's would be. Used to edit shared config from the shell.
99
+ */
100
+ export function readWorkspaceDevProperties(root) {
101
+ const merged = {
102
+ ...readInheritedDevProperties(root),
103
+ ...readDevPropertiesFile(root),
104
+ };
105
+ if (merged.domain && merged.username && !merged.password) {
106
+ merged.password =
107
+ process.env['SITEVISION_DEPLOY_PASSWORD'] ??
108
+ getDeployPassword(merged.domain, merged.username) ??
109
+ undefined;
110
+ }
111
+ return merged;
112
+ }
113
+ /** Dev properties inherited from ancestor directories only (no own file). */
114
+ export function readInheritedDevProperties(root) {
115
+ let merged = {};
116
+ for (const dir of ancestorDirs(root)) {
117
+ merged = { ...merged, ...readDevPropertiesFile(dir) };
118
+ }
119
+ return merged;
120
+ }
64
121
  /**
65
122
  * Get app ID configuration from environment or defaults
66
123
  */
@@ -127,6 +184,11 @@ export function getApiEndpoints(appType) {
127
184
  addon: 'headlesscustommodule',
128
185
  import: 'restAppImport',
129
186
  };
187
+ case 'mcp':
188
+ return {
189
+ addon: 'mcpServerCustomModule',
190
+ import: 'mcpServerImport',
191
+ };
130
192
  }
131
193
  }
132
194
  /**
@@ -217,52 +279,23 @@ export function detectProject(cwd = process.cwd()) {
217
279
  // Check for node_modules
218
280
  const nodeModulesPath = path.join(cwd, 'node_modules');
219
281
  const hasNodeModules = fs.existsSync(nodeModulesPath);
220
- // Check for dev properties
282
+ // Dev properties: ancestor files (workspace root) merged under the app's
283
+ // own file, so shared site/auth config lives once at the repo root.
221
284
  const devPropertiesPath = findDevPropertiesPath(cwd);
285
+ const inherited = readInheritedDevProperties(cwd);
286
+ const own = devPropertiesPath ? readDevPropertiesFile(cwd) : null;
287
+ const inheritedKeys = Object.keys(inherited).filter(key => !own || !Object.hasOwn(own, key));
222
288
  let devProperties;
223
- let hasDevProperties = false;
289
+ const hasDevProperties = Boolean(own) || inheritedKeys.length > 0;
224
290
  let hasLegacyPassword = false;
225
- if (devPropertiesPath) {
226
- hasDevProperties = true;
291
+ if (hasDevProperties) {
227
292
  try {
228
- const parsed = JSON.parse(fs.readFileSync(devPropertiesPath, 'utf-8'));
293
+ const parsed = { ...inherited, ...own };
229
294
  hasLegacyPassword =
230
295
  typeof parsed.password === 'string' && parsed.password.length > 0;
231
296
  devProperties = parsed;
232
- // Resolve deploy password: env var > keychain (file is legacy-only)
233
- if (!hasLegacyPassword &&
234
- devProperties.domain &&
235
- devProperties.username) {
236
- const envPassword = process.env['SITEVISION_DEPLOY_PASSWORD'];
237
- if (envPassword) {
238
- devProperties.password = envPassword;
239
- }
240
- else {
241
- const stored = getDeployPassword(devProperties.domain, devProperties.username);
242
- if (stored) {
243
- devProperties.password = stored;
244
- }
245
- }
246
- }
247
- // Resolve an OAuth2 access token: env var > keychain refresh.
248
- // The env var is the manual/CI path; the interactive login stores a
249
- // refresh token in the keychain and mints access tokens from it.
250
- if (devProperties.authMethod === 'oauth2') {
251
- const envToken = process.env['SITEVISION_ACCESS_TOKEN'];
252
- if (envToken) {
253
- devProperties.accessToken = envToken;
254
- }
255
- }
256
- // Resolve a session cookie: env var > keychain (captured at login).
257
- if (devProperties.authMethod === 'cookie' &&
258
- devProperties.domain &&
259
- devProperties.username) {
260
- const envCookie = process.env['SITEVISION_SESSION_COOKIE'];
261
- devProperties.sessionCookie =
262
- envCookie ??
263
- getSessionCookie(devProperties.domain, devProperties.username) ??
264
- undefined;
265
- }
297
+ if (!hasLegacyPassword)
298
+ resolveRuntimeSecrets(devProperties);
266
299
  }
267
300
  catch {
268
301
  // Invalid dev properties file
@@ -279,6 +312,7 @@ export function detectProject(cwd = process.cwd()) {
279
312
  hasSigningProperties,
280
313
  hasLegacyPassword,
281
314
  devProperties,
315
+ inheritedKeys,
282
316
  packageJson,
283
317
  hasSitevisionScripts,
284
318
  hasNodeModules,
@@ -294,6 +328,29 @@ export function detectProject(cwd = process.cwd()) {
294
328
  return null;
295
329
  }
296
330
  }
331
+ /**
332
+ * Fill the runtime-only credential fields for the given domain/username:
333
+ * deploy password (env var > keychain), OAuth2 access token (env var), and
334
+ * session cookie (env var > keychain). Mutates and returns `dev`.
335
+ */
336
+ export function resolveRuntimeSecrets(dev) {
337
+ if (dev.domain && dev.username) {
338
+ dev.password =
339
+ process.env['SITEVISION_DEPLOY_PASSWORD'] ??
340
+ getDeployPassword(dev.domain, dev.username) ??
341
+ undefined;
342
+ }
343
+ if (dev.authMethod === 'oauth2') {
344
+ dev.accessToken = process.env['SITEVISION_ACCESS_TOKEN'] ?? undefined;
345
+ }
346
+ if (dev.authMethod === 'cookie' && dev.domain && dev.username) {
347
+ dev.sessionCookie =
348
+ process.env['SITEVISION_SESSION_COOKIE'] ??
349
+ getSessionCookie(dev.domain, dev.username) ??
350
+ undefined;
351
+ }
352
+ return dev;
353
+ }
297
354
  /**
298
355
  * Validate that we're in a Sitevision project directory
299
356
  */
@@ -305,20 +362,31 @@ export function requireProject(cwd) {
305
362
  return project;
306
363
  }
307
364
  /**
308
- * Get the app type (web, widget, rest)
365
+ * The app type (web, widget, rest, mcp), or undefined for a manifest type
366
+ * this CLI does not know. Display code uses this so one odd app never takes
367
+ * the whole shell down.
309
368
  */
310
- export function getAppType(manifest) {
369
+ export function appTypeOf(manifest) {
311
370
  const type = manifest.type.toLowerCase();
312
- if (type.startsWith('web')) {
371
+ if (type.startsWith('web'))
313
372
  return 'web';
314
- }
315
- if (type.startsWith('widget')) {
373
+ if (type.startsWith('widget'))
316
374
  return 'widget';
317
- }
318
- if (type.startsWith('rest')) {
375
+ if (type.startsWith('rest'))
319
376
  return 'rest';
320
- }
321
- throw new Error(`Unknown app type: ${manifest.type}`);
377
+ if (type.startsWith('mcp'))
378
+ return 'mcp';
379
+ return undefined;
380
+ }
381
+ /**
382
+ * Get the app type (web, widget, rest, mcp). Throws for unknown types, since
383
+ * build and deploy cannot proceed without knowing the endpoints.
384
+ */
385
+ export function getAppType(manifest) {
386
+ const type = appTypeOf(manifest);
387
+ if (!type)
388
+ throw new Error(`Unknown app type: ${manifest.type}`);
389
+ return type;
322
390
  }
323
391
  /**
324
392
  * Check if the app uses webpack bundling
@@ -349,8 +417,13 @@ export function readDevProperties(projectRoot) {
349
417
  export function writeDevProperties(projectRoot, properties) {
350
418
  const devPropertiesPath = findDevPropertiesPath(projectRoot) ||
351
419
  getDefaultDevPropertiesPath(projectRoot);
352
- const { password: _password, accessToken: _accessToken, sessionCookie: _sessionCookie, ...persisted } = properties;
353
- fs.writeFileSync(devPropertiesPath, JSON.stringify(persisted, null, 2));
420
+ const { password: _password, accessToken: _accessToken, sessionCookie: _sessionCookie, environmentName: _environmentName, productionEnvironment: _productionEnvironment, ...persisted } = properties;
421
+ // Keep the app file minimal: values identical to the inherited ones stay
422
+ // at the workspace root instead of being copied into every app.
423
+ const inherited = readInheritedDevProperties(projectRoot);
424
+ const own = Object.fromEntries(Object.entries(persisted).filter(([key, value]) => !Object.hasOwn(inherited, key) ||
425
+ JSON.stringify(inherited[key]) !== JSON.stringify(value)));
426
+ fs.writeFileSync(devPropertiesPath, JSON.stringify(own, null, 2));
354
427
  }
355
428
  export function readSvcConfig(projectRoot) {
356
429
  try {
@@ -109,4 +109,39 @@ export declare function createAddon(config: DeployConfig, appType: SimpleAppType
109
109
  * @param appType - The app type (web, widget, rest)
110
110
  */
111
111
  export declare function activateApp(executableId: string, config: DeployConfig, _appType: SimpleAppType): Promise<ActivationResponse>;
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
+ }>;
112
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
  // =============================================================================
@@ -545,6 +545,79 @@ export async function activateApp(executableId, config, _appType) {
545
545
  };
546
546
  }
547
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
+ }
548
621
  // =============================================================================
549
622
  // HELPER EXPORTS
550
623
  // =============================================================================
@@ -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;