sitevision-cli 1.0.0-beta.13 → 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 +2 -1
  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 +1 -0
  33. package/dist/utils/oauth2-auth.js +4 -3
  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
@@ -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;