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.
- package/dist/cli.js +70 -41
- package/dist/commands/build.js +1 -1
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +75 -390
- package/dist/commands/watch.js +5 -23
- package/dist/components/AuthLoginScreen.js +4 -3
- package/dist/components/DevPropertiesForm.js +6 -3
- package/dist/components/PasswordInput.js +2 -1
- 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 +35 -0
- package/dist/shell/ConfigForm.js +472 -0
- package/dist/shell/Frame.d.ts +52 -0
- package/dist/shell/Frame.js +98 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +8 -0
- package/dist/shell/Shell.js +520 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +85 -0
- package/dist/shell/actions.d.ts +45 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +12 -2
- 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 +263 -0
- package/dist/utils/oauth2-auth.d.ts +26 -1
- package/dist/utils/oauth2-auth.js +75 -32
- package/dist/utils/project-detection.d.ts +23 -1
- package/dist/utils/project-detection.js +124 -51
- package/dist/utils/sitevision-api.d.ts +35 -0
- package/dist/utils/sitevision-api.js +74 -1
- package/dist/utils/tasks.d.ts +48 -0
- package/dist/utils/tasks.js +371 -0
- package/dist/utils/workspace.d.ts +8 -0
- package/dist/utils/workspace.js +48 -0
- package/package.json +1 -1
- 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
|
|
41
|
-
|
|
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
|
|
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
|
-
*
|
|
91
|
-
*
|
|
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('<', '<')
|
|
123
|
+
.replaceAll('>', '>');
|
|
124
|
+
}
|
|
94
125
|
function startLoopback(port, state) {
|
|
95
126
|
let finish;
|
|
96
127
|
let settled = false;
|
|
97
|
-
const
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
:
|
|
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(
|
|
155
|
+
res.end(`<!doctype html><meta charset="utf-8"><p>${escapeHtml(message)}</p>`);
|
|
156
|
+
finish(outcome);
|
|
121
157
|
});
|
|
122
|
-
const timer = setTimeout(() => finish(
|
|
123
|
-
server.on('error',
|
|
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 {
|
|
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
|
-
|
|
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
|
|
156
|
-
if (!code)
|
|
157
|
-
return
|
|
158
|
-
|
|
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 (
|
|
166
|
-
return
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
289
|
+
const hasDevProperties = Boolean(own) || inheritedKeys.length > 0;
|
|
224
290
|
let hasLegacyPassword = false;
|
|
225
|
-
if (
|
|
226
|
-
hasDevProperties = true;
|
|
291
|
+
if (hasDevProperties) {
|
|
227
292
|
try {
|
|
228
|
-
const parsed =
|
|
293
|
+
const parsed = { ...inherited, ...own };
|
|
229
294
|
hasLegacyPassword =
|
|
230
295
|
typeof parsed.password === 'string' && parsed.password.length > 0;
|
|
231
296
|
devProperties = parsed;
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|