sitevision-cli 1.0.0-beta.32 → 1.0.0-beta.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/dist/cli.js +56 -35
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +85 -238
- package/dist/commands/dev.js +34 -37
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +43 -28
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +0 -8
- package/dist/components/AuthLoginScreen.js +27 -12
- package/dist/shell/ConfigForm.js +90 -29
- package/dist/shell/Frame.js +4 -1
- package/dist/shell/Help.js +2 -1
- package/dist/shell/Settings.js +59 -6
- package/dist/shell/Shell.js +124 -41
- package/dist/shell/Tabs.js +8 -2
- package/dist/shell/actions.js +0 -0
- package/dist/utils/config.js +46 -3
- package/dist/utils/create-app.js +9 -2
- package/dist/utils/i18n.js +36 -13
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +44 -4
- package/dist/utils/oauth2-auth.js +35 -7
- package/dist/utils/password-prompt.js +23 -3
- package/dist/utils/process-runner.js +4 -2
- package/dist/utils/project-detection.js +84 -33
- package/dist/utils/session-cookie-auth.js +5 -4
- package/dist/utils/sitevision-api.js +21 -14
- package/dist/utils/sitevision-scripts-runner.js +32 -15
- package/dist/utils/tasks.js +130 -64
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +6 -5
- package/dist/utils/workspace.js +41 -3
- package/dist/utils/zip.js +18 -19
- package/package.json +24 -3
- package/readme.md +11 -4
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -321
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -17
- package/dist/commands/dev.d.ts +0 -14
- package/dist/commands/index.d.ts +0 -4
- package/dist/commands/info.d.ts +0 -2
- package/dist/commands/setup-signing.d.ts +0 -2
- package/dist/commands/sign.d.ts +0 -14
- package/dist/commands/types.d.ts +0 -18
- package/dist/commands/watch.d.ts +0 -2
- package/dist/components/AnimatedLogo.d.ts +0 -10
- package/dist/components/AuthLoginScreen.d.ts +0 -21
- package/dist/components/DevPropertiesForm.d.ts +0 -11
- package/dist/components/DevPropertiesForm.js +0 -246
- package/dist/components/InfoScreen.d.ts +0 -7
- package/dist/components/InfoScreen.js +0 -14
- package/dist/components/KeychainPasswordChoice.d.ts +0 -8
- package/dist/components/KeychainPasswordChoice.js +0 -30
- package/dist/components/MainMenu.d.ts +0 -7
- package/dist/components/MainMenu.js +0 -88
- package/dist/components/PasswordInput.d.ts +0 -10
- package/dist/components/ProcessOutput.d.ts +0 -6
- package/dist/components/ProcessOutput.js +0 -30
- package/dist/components/SetupFlow.d.ts +0 -8
- package/dist/components/SetupFlow.js +0 -233
- package/dist/components/SigningPropertiesForm.d.ts +0 -7
- package/dist/components/SigningPropertiesForm.js +0 -38
- package/dist/components/StatusIndicator.d.ts +0 -8
- package/dist/components/TextInput.d.ts +0 -10
- package/dist/components/TextInput.js +0 -29
- package/dist/components/WelcomeScreen.d.ts +0 -14
- package/dist/scaffold/inquirer-stub.d.ts +0 -6
- package/dist/scaffold/register.d.ts +0 -1
- package/dist/shell/AddonPicker.d.ts +0 -14
- package/dist/shell/Changelog.d.ts +0 -10
- package/dist/shell/CommandPalette.d.ts +0 -12
- package/dist/shell/ConfigForm.d.ts +0 -42
- package/dist/shell/Frame.d.ts +0 -59
- package/dist/shell/Help.d.ts +0 -9
- package/dist/shell/Popover.d.ts +0 -12
- package/dist/shell/Settings.d.ts +0 -6
- package/dist/shell/Shell.d.ts +0 -11
- package/dist/shell/Tabs.d.ts +0 -36
- package/dist/shell/actions.d.ts +0 -49
- package/dist/types/index.d.ts +0 -295
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -27
- package/dist/utils/create-app.d.ts +0 -53
- package/dist/utils/environments.d.ts +0 -20
- package/dist/utils/i18n.d.ts +0 -12
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -64
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -32
- package/dist/utils/project-detection.d.ts +0 -210
- package/dist/utils/session-cookie-auth.d.ts +0 -35
- package/dist/utils/signing-password.d.ts +0 -12
- package/dist/utils/signing-step.d.ts +0 -15
- package/dist/utils/signing-step.js +0 -16
- package/dist/utils/sitevision-api.d.ts +0 -155
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
- package/dist/utils/tasks.d.ts +0 -61
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/workspace.d.ts +0 -17
- package/dist/utils/zip.d.ts +0 -71
package/dist/utils/i18n.js
CHANGED
|
@@ -45,7 +45,7 @@ const sv = {
|
|
|
45
45
|
auth: 'auth',
|
|
46
46
|
'signing user': 'signeringsanv.',
|
|
47
47
|
'not set': 'ej satt',
|
|
48
|
-
'↑ root': '↑
|
|
48
|
+
'↑ root': '↑ root',
|
|
49
49
|
' · bundled': ' · paketerad',
|
|
50
50
|
deps: 'beroenden',
|
|
51
51
|
config: 'konfig',
|
|
@@ -54,16 +54,16 @@ const sv = {
|
|
|
54
54
|
scripts: 'scripts',
|
|
55
55
|
'missing · i to install': 'saknas · i för att installera',
|
|
56
56
|
'not installed · i to install': 'ej installerat · i för att installera',
|
|
57
|
-
'dev properties': '
|
|
57
|
+
'dev properties': 'dev_properties',
|
|
58
58
|
'missing · e to edit': 'saknas · e för att redigera',
|
|
59
59
|
'{n} diffs · y to apply': '{n} skillnader · y för att tillämpa',
|
|
60
60
|
'1 diff · y to apply': '1 skillnad · y för att tillämpa',
|
|
61
61
|
'missing · / set up signing': 'saknas · / konfigurera signering',
|
|
62
62
|
'not installed': 'ej installerat',
|
|
63
|
-
'⚠ plaintext password in .dev_properties.json · / migrate': '⚠ lösenord i klartext i .dev_properties.json · / migrera',
|
|
63
|
+
'⚠ plaintext password in .dev_properties.json · / migrate': '⚠ lösenord i klartext funna i .dev_properties.json · / migrera',
|
|
64
64
|
RECENT: 'SENASTE',
|
|
65
65
|
'⚠ Configure dev properties first (e).': '⚠ Konfigurera dev-egenskaper först (e).',
|
|
66
|
-
'Press
|
|
66
|
+
'Press r to fetch versions from {domain}.': 'Tryck r för att hämta versioner från {domain}.',
|
|
67
67
|
'APP IDENTIFIER': 'APP-ID',
|
|
68
68
|
VERSION: 'VERSION',
|
|
69
69
|
ACTIVE: 'AKTIV',
|
|
@@ -89,13 +89,13 @@ const sv = {
|
|
|
89
89
|
Watch: 'Watch',
|
|
90
90
|
Build: 'Build',
|
|
91
91
|
Sign: 'Sign',
|
|
92
|
-
Deploy: '
|
|
93
|
-
'Deploy (force)': '
|
|
92
|
+
Deploy: 'Deploy',
|
|
93
|
+
'Deploy (force)': 'Deploy (force)',
|
|
94
94
|
'to {env}': 'till {env}',
|
|
95
95
|
'Switch environment': 'Byt miljö',
|
|
96
96
|
'Add environment': 'Lägg till miljö',
|
|
97
97
|
Help: 'Hjälp',
|
|
98
|
-
'every key in one place': 'alla
|
|
98
|
+
'every key in one place': 'alla kortkommandon på ett ställe',
|
|
99
99
|
Keys: 'Tangenter',
|
|
100
100
|
'Esc close': 'Esc stäng',
|
|
101
101
|
Here: 'Här',
|
|
@@ -129,7 +129,7 @@ const sv = {
|
|
|
129
129
|
'Addon {addon} does not exist on {domain}. Create it and deploy again?': 'Tillägget {addon} finns inte på {domain}. Skapa det och driftsätt igen?',
|
|
130
130
|
'e.g. test or prod, overriding domain and auth': 't.ex. test eller prod, med egen domän och auth',
|
|
131
131
|
'Environment name (e.g. test, prod)': 'Miljönamn (t.ex. test, prod)',
|
|
132
|
-
'Deploy the signed {id} to {env} and activate it?': 'Driftsätt signerade {id} till {env} och aktivera?',
|
|
132
|
+
'Deploy the signed {id} to {env} (addon {addon}) and activate it?': 'Driftsätt signerade {id} till {env} (addon {addon}) och aktivera?',
|
|
133
133
|
'Dev never deploys to a production environment ({env}). Switch with v.': 'Dev driftsätter aldrig till en produktionsmiljö ({env}). Byt med v.',
|
|
134
134
|
'switched to {env}': 'bytte till {env}',
|
|
135
135
|
'environment {env} added': 'miljön {env} tillagd',
|
|
@@ -137,9 +137,16 @@ const sv = {
|
|
|
137
137
|
production: 'produktion',
|
|
138
138
|
env: 'miljö',
|
|
139
139
|
'↑ dev': '↑ dev',
|
|
140
|
-
'Activate a remote version': 'Aktivera en
|
|
141
|
-
'Stop running task': 'Stoppa pågående
|
|
140
|
+
'Activate a remote version': 'Aktivera en uppladdad version',
|
|
141
|
+
'Stop running task': 'Stoppa pågående körning',
|
|
142
142
|
'Edit config': 'Redigera konfig',
|
|
143
|
+
'New app': 'Ny app',
|
|
144
|
+
'Skipped: {reason}': 'Hoppade över: {reason}',
|
|
145
|
+
'Skipped {n} apps. First: {reason}': 'Hoppade över {n} appar. Första: {reason}',
|
|
146
|
+
'A new app is still being created. Stop it and quit?': 'En ny app håller på att skapas. Avbryta den och avsluta?',
|
|
147
|
+
'An app is already being created.': 'En app håller redan på att skapas.',
|
|
148
|
+
'Could not save to the OS keychain; you will be asked again next time.': 'Kunde inte spara i nyckelringen; du får frågan igen nästa gång.',
|
|
149
|
+
Navigator: 'Navigering',
|
|
143
150
|
'Sync package.json': 'Synka package.json',
|
|
144
151
|
'Migrate password to OS keychain': 'Flytta lösenord till nyckelringen',
|
|
145
152
|
'Install dependencies': 'Installera beroenden',
|
|
@@ -187,7 +194,7 @@ const sv = {
|
|
|
187
194
|
'Auth method': 'Auth-metod',
|
|
188
195
|
Password: 'Lösenord',
|
|
189
196
|
'OAuth2 client id': 'OAuth2 klient-id',
|
|
190
|
-
'Authorization endpoint': '
|
|
197
|
+
'Authorization endpoint': 'Authorization url',
|
|
191
198
|
'Token endpoint': 'Token-endpoint',
|
|
192
199
|
Scopes: 'Scopes',
|
|
193
200
|
'Client secret': 'Klienthemlighet',
|
|
@@ -197,7 +204,7 @@ const sv = {
|
|
|
197
204
|
Certificate: 'Certifikat',
|
|
198
205
|
'Signing password': 'Signeringslösenord',
|
|
199
206
|
'^O pick from repo': '^O välj från förråd',
|
|
200
|
-
'Space-separated scopes to request. ALL grants the Sitevision API and offline_access adds a refresh token so later runs log in silently. Match the casing your client expects.': 'Scopes att begära, separerade med mellanslag. ALL ger Sitevision-API:et och offline_access ger en refresh-token så att senare körningar loggar in tyst.
|
|
207
|
+
'Space-separated scopes to request. ALL grants the Sitevision API and offline_access adds a refresh token so later runs log in silently. Match the casing your client expects.': 'Scopes att begära, separerade med mellanslag. ALL ger Sitevision-API:et och offline_access ger en refresh-token så att senare körningar loggar in tyst. Skriftlägesskänsligt.',
|
|
201
208
|
'blank = site root': 'tomt = webbplatsens rot',
|
|
202
209
|
'required for signed deploys': 'krävs för signerad driftsättning',
|
|
203
210
|
'blank = prompt each run': 'tomt = fråga varje gång',
|
|
@@ -306,7 +313,9 @@ const sv = {
|
|
|
306
313
|
// Config help
|
|
307
314
|
"Domain of this environment's site (USE or TSE) without https://, e.g. myorg-use.sitevision-cloud.se. Deploys and version lookups go here.": 'Domän för den här miljöns webbplats (USE eller TSE) utan https://, t.ex. myorg-use.sitevision-cloud.se. Driftsättningar och versionslistor går hit.',
|
|
308
315
|
"Name of the site's root node in Sitevision, exactly as shown in the site tree. It becomes part of the REST API path.": 'Namnet på webbplatsens rotnod i Sitevision, exakt som i webbplatsträdet. Det blir en del av REST API-sökvägen.',
|
|
309
|
-
"Name of the addon (custom module) in the site's Addon Repository that this app is uploaded into. Ctrl+O lists the existing ones.": 'Namnet på tillägget (custom module) i webbplatsens tilläggsförråd som appen laddas upp till. Ctrl+O listar befintliga.',
|
|
316
|
+
"Name of the addon (custom module) in the site's Addon Repository that this app is uploaded into. Ctrl+O lists the existing ones. It is separate from the manifest name: to make them match, rename the addon in Sitevision (Addons, General, Settings) and pick it again with Ctrl+O. For a RESTApp the addon name is part of its endpoint URL.": 'Namnet på tillägget (custom module) i webbplatsens tilläggsförråd som appen laddas upp till. Ctrl+O listar befintliga. Det är skilt från manifestets namn: för att få dem att stämma, byt namn på tillägget i Sitevision (Tillägg, Allmänt, Inställningar) och välj det igen med Ctrl+O. För en RESTApp ingår tilläggets namn i dess endpoint-URL.',
|
|
317
|
+
'≠ manifest': '≠ manifest',
|
|
318
|
+
'addon name {addon} · manifest name {names}': 'tilläggsnamn {addon} · manifestnamn {names}',
|
|
310
319
|
'Sitevision account used for deploys, usually your Sitevision Cloud e-mail. It needs DEVELOPER or MANAGE_ADDONS permission on the site. Required for basic auth; with oauth2 or cookie it only labels the stored credential.': 'Sitevision-konto som används för driftsättning, oftast din Sitevision Cloud-e-post. Behöver DEVELOPER eller MANAGE_ADDONS på webbplatsen. Krävs för basic; med oauth2 eller cookie används det bara för att märka den sparade inloggningen.',
|
|
311
320
|
"How deploys authenticate: basic = username and password; oauth2 = bearer token from the site's OAuth2 provider (PKCE, opens a browser); cookie = reuse a browser SSO/SAML session.": 'Hur driftsättningar autentiseras: basic = användarnamn och lösenord; oauth2 = bearer-token från webbplatsens OAuth2-provider (PKCE, öppnar webbläsare); cookie = återanvänd en SSO/SAML-session från webbläsaren.',
|
|
312
321
|
'Deploy password for the account above. Stored in the OS keychain, never in a file. Leave empty to be asked on each run.': 'Driftsättningslösenord för kontot ovan. Sparas i nyckelringen, aldrig i en fil. Lämna tomt för att bli tillfrågad varje gång.',
|
|
@@ -322,6 +331,20 @@ const sv = {
|
|
|
322
331
|
// Settings screen
|
|
323
332
|
Language: 'Språk',
|
|
324
333
|
'Intro animation': 'Introanimation',
|
|
334
|
+
'Update check': 'Uppdateringskoll',
|
|
335
|
+
'Addon name warning': 'Varning om tilläggsnamn',
|
|
336
|
+
'Default signing user': 'Förvald signeringsanvändare',
|
|
337
|
+
'Default certificate': 'Förvalt certifikat',
|
|
338
|
+
'The language svc itself speaks.': 'Språket som svc självt använder.',
|
|
339
|
+
'Play the logo animation when the shell starts.': 'Spela logotypanimationen när skalet startar.',
|
|
340
|
+
'Look for a newer svc on npm at start.': 'Leta efter en nyare svc på npm vid start.',
|
|
341
|
+
'Mark the addon name with ≠ manifest when it is none of the names in manifest.json.': 'Markera tilläggsnamnet med ≠ manifest när det inte är något av namnen i manifest.json.',
|
|
342
|
+
'The developer.sitevision.se account that signs in every project without a signing user of its own.': 'Kontot på developer.sitevision.se som signerar i alla projekt utan egen signeringsanvändare.',
|
|
343
|
+
'Which certificate signs when the account has several. Empty = the account default.': 'Vilket certifikat som signerar när kontot har flera. Tomt = kontots standard.',
|
|
344
|
+
'Enter confirm · Esc cancel': 'Enter bekräfta · Esc avbryt',
|
|
345
|
+
'Not saved: the settings file does not parse.': 'Inte sparat: inställningsfilen går inte att tolka.',
|
|
346
|
+
'The settings file does not parse and is ignored until it is fixed.': 'Inställningsfilen går inte att tolka och ignoreras tills den är lagad.',
|
|
347
|
+
global: 'global',
|
|
325
348
|
on: 'på',
|
|
326
349
|
off: 'av',
|
|
327
350
|
'Saved.': 'Sparat.',
|
package/dist/utils/jsonc.js
CHANGED
|
@@ -70,5 +70,6 @@ export function stripJsonComments(input) {
|
|
|
70
70
|
* SyntaxError if the content is invalid even after comments are removed.
|
|
71
71
|
*/
|
|
72
72
|
export function parseJsonc(input) {
|
|
73
|
-
|
|
73
|
+
// Editors on Windows may prepend a BOM, which JSON.parse rejects.
|
|
74
|
+
return JSON.parse(stripJsonComments(input.replace(/^\uFEFF/, '')));
|
|
74
75
|
}
|
package/dist/utils/keychain.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
2
|
const SERVICE = 'sitevision-cli';
|
|
3
|
+
let EntryImpl;
|
|
4
|
+
// SVC_NO_KEYCHAIN=1 turns the keychain off (documented, for CI). `memory` is
|
|
5
|
+
// for the test suite only: secrets live and die with the process.
|
|
6
|
+
const memory = new Map();
|
|
7
|
+
const memoryEntry = (account) => ({
|
|
8
|
+
getPassword: () => memory.get(account) ?? null,
|
|
9
|
+
setPassword(password) {
|
|
10
|
+
memory.set(account, password);
|
|
11
|
+
},
|
|
12
|
+
deletePassword: () => memory.delete(account),
|
|
13
|
+
});
|
|
14
|
+
// Loaded on first use so a missing native binding never breaks startup.
|
|
15
|
+
function entry(account) {
|
|
16
|
+
const off = process.env['SVC_NO_KEYCHAIN'];
|
|
17
|
+
if (off === 'memory')
|
|
18
|
+
return memoryEntry(account);
|
|
19
|
+
if (off)
|
|
20
|
+
throw new Error('Keychain disabled');
|
|
21
|
+
if (EntryImpl === undefined) {
|
|
22
|
+
try {
|
|
23
|
+
const keyring = createRequire(import.meta.url)('@napi-rs/keyring');
|
|
24
|
+
EntryImpl = keyring.Entry;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
EntryImpl = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!EntryImpl)
|
|
31
|
+
throw new Error('Keychain unavailable');
|
|
32
|
+
return new EntryImpl(SERVICE, account);
|
|
33
|
+
}
|
|
3
34
|
function deployAccount(domain, username) {
|
|
4
35
|
return `deploy:${username}@${domain}`;
|
|
5
36
|
}
|
|
@@ -17,24 +48,33 @@ function sessionCookieAccount(domain, username) {
|
|
|
17
48
|
}
|
|
18
49
|
function safeGet(account) {
|
|
19
50
|
try {
|
|
20
|
-
return
|
|
51
|
+
return entry(account).getPassword();
|
|
21
52
|
}
|
|
22
53
|
catch {
|
|
23
54
|
return null;
|
|
24
55
|
}
|
|
25
56
|
}
|
|
57
|
+
let saveFailed = () => {
|
|
58
|
+
console.warn('\u001B[33mCould not save to the OS keychain; you will be asked again next time.\u001B[0m');
|
|
59
|
+
};
|
|
60
|
+
/** Replace how a failed keychain save is reported (the shell shows a notice). */
|
|
61
|
+
export function onKeychainSaveFailed(listener) {
|
|
62
|
+
saveFailed = listener;
|
|
63
|
+
}
|
|
26
64
|
function safeSet(account, password) {
|
|
27
65
|
try {
|
|
28
|
-
|
|
66
|
+
entry(account).setPassword(password);
|
|
29
67
|
return true;
|
|
30
68
|
}
|
|
31
69
|
catch {
|
|
70
|
+
// Callers mostly ignore the result, so the user hears it from here.
|
|
71
|
+
saveFailed();
|
|
32
72
|
return false;
|
|
33
73
|
}
|
|
34
74
|
}
|
|
35
75
|
function safeDelete(account) {
|
|
36
76
|
try {
|
|
37
|
-
|
|
77
|
+
entry(account).deletePassword();
|
|
38
78
|
}
|
|
39
79
|
catch {
|
|
40
80
|
// ignore
|
|
@@ -23,8 +23,32 @@ export function createPkcePair() {
|
|
|
23
23
|
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
|
|
24
24
|
return { verifier, challenge };
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Why this project's OAuth2 settings cannot be used, if they cannot. The
|
|
28
|
+
* endpoints may come from a committed package.json, and the keychain secrets
|
|
29
|
+
* are stored per site, so they only ever go to that site, over TLS.
|
|
30
|
+
*/
|
|
31
|
+
export function oauth2ConfigProblem(dev) {
|
|
32
|
+
const config = dev.oauth2;
|
|
33
|
+
if (!config?.authorizationEndpoint ||
|
|
34
|
+
!config.tokenEndpoint ||
|
|
35
|
+
!config.clientId) {
|
|
36
|
+
return 'OAuth2 is not fully configured (authorization/token endpoint or client ID missing).';
|
|
37
|
+
}
|
|
38
|
+
if (!dev.domain)
|
|
39
|
+
return 'OAuth2 needs the site domain to be set.';
|
|
40
|
+
const schemes = dev.useHTTPForDevDeploy ? ['https:', 'http:'] : ['https:'];
|
|
41
|
+
for (const endpoint of [config.authorizationEndpoint, config.tokenEndpoint]) {
|
|
42
|
+
const url = URL.canParse(endpoint) ? new URL(endpoint) : undefined;
|
|
43
|
+
if (!url || !schemes.includes(url.protocol)) {
|
|
44
|
+
return `OAuth2 endpoint ${endpoint} must be an https URL.`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const tokenHost = new URL(config.tokenEndpoint).host.toLowerCase();
|
|
48
|
+
if (tokenHost !== dev.domain.toLowerCase()) {
|
|
49
|
+
return `OAuth2 token endpoint is on ${tokenHost}, not on ${dev.domain}. Stored secrets are only sent to the site they belong to.`;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
28
52
|
}
|
|
29
53
|
async function postToken(config, params, secret) {
|
|
30
54
|
const body = Buffer.from(new URLSearchParams(params).toString());
|
|
@@ -41,6 +65,7 @@ async function postToken(config, params, secret) {
|
|
|
41
65
|
});
|
|
42
66
|
if (response.statusCode !== 200) {
|
|
43
67
|
return {
|
|
68
|
+
status: response.statusCode,
|
|
44
69
|
error: `Token endpoint returned ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
45
70
|
};
|
|
46
71
|
}
|
|
@@ -169,7 +194,7 @@ function startLoopback(port, state) {
|
|
|
169
194
|
*/
|
|
170
195
|
export function beginOAuth2Login(dev) {
|
|
171
196
|
const config = dev.oauth2;
|
|
172
|
-
if (!
|
|
197
|
+
if (!config || oauth2ConfigProblem(dev))
|
|
173
198
|
return null;
|
|
174
199
|
const { domain } = dev;
|
|
175
200
|
const secret = getOAuth2ClientSecret(domain, config.clientId) ?? undefined;
|
|
@@ -218,14 +243,14 @@ export function beginOAuth2Login(dev) {
|
|
|
218
243
|
*/
|
|
219
244
|
export async function resolveOAuth2AccessToken(dev) {
|
|
220
245
|
const config = dev.oauth2;
|
|
221
|
-
if (!
|
|
246
|
+
if (!config || oauth2ConfigProblem(dev))
|
|
222
247
|
return null;
|
|
223
248
|
const { domain } = dev;
|
|
224
249
|
const secret = getOAuth2ClientSecret(domain, config.clientId) ?? undefined;
|
|
225
250
|
const storedRefresh = getOAuth2RefreshToken(domain, config.clientId);
|
|
226
251
|
if (!storedRefresh)
|
|
227
252
|
return null;
|
|
228
|
-
const { tokens } = await postToken(config, {
|
|
253
|
+
const { tokens, status } = await postToken(config, {
|
|
229
254
|
grant_type: 'refresh_token',
|
|
230
255
|
refresh_token: storedRefresh,
|
|
231
256
|
client_id: config.clientId,
|
|
@@ -236,7 +261,10 @@ export async function resolveOAuth2AccessToken(dev) {
|
|
|
236
261
|
}
|
|
237
262
|
return tokens.access_token;
|
|
238
263
|
}
|
|
239
|
-
//
|
|
240
|
-
|
|
264
|
+
// Only a rejection (invalid_grant) means the token is dead. Offline, a
|
|
265
|
+
// timeout or a 5xx says nothing about it, so it stays.
|
|
266
|
+
if (status === 400 || status === 401) {
|
|
267
|
+
deleteOAuth2RefreshToken(domain, config.clientId);
|
|
268
|
+
}
|
|
241
269
|
return null;
|
|
242
270
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
// Without a terminal nobody can answer: prompts resolve to their "no answer"
|
|
2
|
+
// value (default, nothing, empty password) and the caller reports what is missing.
|
|
3
|
+
const interactive = () => Boolean(process.stdin.isTTY);
|
|
4
|
+
// 130 = 128 + SIGINT, so `svc sign && svc deploy` stops at a cancelled prompt.
|
|
5
|
+
const cancelled = () => process.exit(130);
|
|
1
6
|
/**
|
|
2
7
|
* Prompt for a yes/no answer. Returns true for y/Y.
|
|
3
8
|
* Pressing Enter (empty answer) returns `defaultYes` (default: false).
|
|
4
9
|
*/
|
|
5
10
|
export function promptYesNo(prompt, defaultYes = false) {
|
|
6
11
|
return new Promise(resolve => {
|
|
12
|
+
if (!interactive()) {
|
|
13
|
+
resolve(defaultYes);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
7
16
|
process.stdout.write(prompt);
|
|
8
17
|
const stdin = process.stdin;
|
|
9
18
|
stdin.setRawMode(true);
|
|
@@ -17,7 +26,7 @@ export function promptYesNo(prompt, defaultYes = false) {
|
|
|
17
26
|
process.stdout.write(`${char}\n`);
|
|
18
27
|
const charCode = char.charCodeAt(0);
|
|
19
28
|
if (charCode === 3) {
|
|
20
|
-
|
|
29
|
+
cancelled();
|
|
21
30
|
}
|
|
22
31
|
// Enter (CR/LF) or empty input → use the default
|
|
23
32
|
if (char === '' || charCode === 13 || charCode === 10) {
|
|
@@ -35,6 +44,10 @@ export function promptYesNo(prompt, defaultYes = false) {
|
|
|
35
44
|
*/
|
|
36
45
|
export function promptEnter(prompt) {
|
|
37
46
|
return new Promise(resolve => {
|
|
47
|
+
if (!interactive()) {
|
|
48
|
+
resolve();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
38
51
|
process.stdout.write(prompt);
|
|
39
52
|
const stdin = process.stdin;
|
|
40
53
|
stdin.setRawMode(true);
|
|
@@ -44,7 +57,7 @@ export function promptEnter(prompt) {
|
|
|
44
57
|
const char = data[0] || '';
|
|
45
58
|
const charCode = char.charCodeAt(0);
|
|
46
59
|
if (charCode === 3) {
|
|
47
|
-
|
|
60
|
+
cancelled();
|
|
48
61
|
}
|
|
49
62
|
if (char === '' || charCode === 13 || charCode === 10) {
|
|
50
63
|
stdin.setRawMode(false);
|
|
@@ -62,6 +75,10 @@ export function promptEnter(prompt) {
|
|
|
62
75
|
*/
|
|
63
76
|
export function promptPassword(prompt) {
|
|
64
77
|
return new Promise(resolve => {
|
|
78
|
+
if (!interactive()) {
|
|
79
|
+
resolve('');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
65
82
|
process.stdout.write(prompt);
|
|
66
83
|
const stdin = process.stdin;
|
|
67
84
|
stdin.setRawMode(true);
|
|
@@ -69,6 +86,9 @@ export function promptPassword(prompt) {
|
|
|
69
86
|
stdin.setEncoding('utf8');
|
|
70
87
|
let password = '';
|
|
71
88
|
const onData = (data) => {
|
|
89
|
+
// Arrow keys and the like arrive as escape sequences, not as text.
|
|
90
|
+
if (data.startsWith('\u001B'))
|
|
91
|
+
return;
|
|
72
92
|
// Handle each character in the input (supports paste)
|
|
73
93
|
for (const char of data) {
|
|
74
94
|
const charCode = char.charCodeAt(0);
|
|
@@ -90,7 +110,7 @@ export function promptPassword(prompt) {
|
|
|
90
110
|
}
|
|
91
111
|
else if (charCode === 3) {
|
|
92
112
|
// Ctrl+C
|
|
93
|
-
|
|
113
|
+
cancelled();
|
|
94
114
|
}
|
|
95
115
|
else if (charCode >= 32) {
|
|
96
116
|
// Printable characters
|
|
@@ -63,7 +63,9 @@ export class ProcessRunner extends EventEmitter {
|
|
|
63
63
|
}
|
|
64
64
|
run() {
|
|
65
65
|
return new Promise((resolve, reject) => {
|
|
66
|
-
|
|
66
|
+
// One pre-joined string: the shell resolves npm.cmd on Windows, and Node
|
|
67
|
+
// deprecates shell + args (DEP0190). Callers pass trusted args only.
|
|
68
|
+
this.process = spawnChild([this.command, ...this.args].join(' '), [], {
|
|
67
69
|
cwd: this.cwd || process.cwd(),
|
|
68
70
|
env: this.env,
|
|
69
71
|
shell: true,
|
|
@@ -84,7 +86,7 @@ export class ProcessRunner extends EventEmitter {
|
|
|
84
86
|
reject(error);
|
|
85
87
|
});
|
|
86
88
|
this.process.on('close', code => {
|
|
87
|
-
const exitCode = code ??
|
|
89
|
+
const exitCode = code ?? 1;
|
|
88
90
|
this.emit('exit', exitCode);
|
|
89
91
|
resolve({
|
|
90
92
|
exitCode,
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { getDeployPassword, setDeployPassword, getSessionCookie, } from './keychain.js';
|
|
4
4
|
import { parseJsonc, stripJsonComments } from './jsonc.js';
|
|
5
5
|
import { getLanguage } from './i18n.js';
|
|
6
|
+
import { getGlobalSigning } from './config.js';
|
|
6
7
|
// =============================================================================
|
|
7
8
|
// LOCALIZED TEXT
|
|
8
9
|
// =============================================================================
|
|
@@ -22,6 +23,20 @@ export function localizedText(value, preferred = getLanguage()) {
|
|
|
22
23
|
return value;
|
|
23
24
|
return value[preferred] ?? value['en'] ?? Object.values(value)[0] ?? '';
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The manifest's names when the addon name is none of them. The two are
|
|
28
|
+
* separate things (the addon is an object on the site, the name a label in the
|
|
29
|
+
* zip), so a difference is only ever pointed out, never "fixed".
|
|
30
|
+
*/
|
|
31
|
+
export function addonNameDrift(addonName, manifest) {
|
|
32
|
+
const same = (text) => text.trim().toLowerCase();
|
|
33
|
+
const names = (typeof manifest?.name === 'string'
|
|
34
|
+
? [manifest.name]
|
|
35
|
+
: Object.values(manifest?.name ?? {})).filter(name => name.trim() !== '');
|
|
36
|
+
if (!addonName?.trim() || names.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return names.some(name => same(name) === same(addonName)) ? undefined : names;
|
|
39
|
+
}
|
|
25
40
|
// =============================================================================
|
|
26
41
|
// PATH UTILITIES
|
|
27
42
|
// =============================================================================
|
|
@@ -105,16 +120,33 @@ function readDevPropertiesFile(dir) {
|
|
|
105
120
|
return null;
|
|
106
121
|
}
|
|
107
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Merge config layers, later ones winning. `environments` merges per name, so a
|
|
125
|
+
* layer that overrides one field of an environment keeps the rest of it.
|
|
126
|
+
*/
|
|
127
|
+
export function mergeDevLayers(...layers) {
|
|
128
|
+
let merged = {};
|
|
129
|
+
for (const layer of layers) {
|
|
130
|
+
if (!layer)
|
|
131
|
+
continue;
|
|
132
|
+
const environments = { ...merged.environments };
|
|
133
|
+
for (const [name, override] of Object.entries(layer.environments ?? {})) {
|
|
134
|
+
environments[name] = { ...environments[name], ...override };
|
|
135
|
+
}
|
|
136
|
+
merged = { ...merged, ...layer };
|
|
137
|
+
if (Object.keys(environments).length > 0) {
|
|
138
|
+
merged.environments = environments;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return merged;
|
|
142
|
+
}
|
|
108
143
|
/**
|
|
109
144
|
* The dev properties a workspace root defines (its own file merged over any
|
|
110
145
|
* ancestors'), with the deploy password resolved from the keychain like an
|
|
111
146
|
* app's would be. Used to edit shared config from the shell.
|
|
112
147
|
*/
|
|
113
148
|
export function readWorkspaceDevProperties(root) {
|
|
114
|
-
const merged =
|
|
115
|
-
...readInheritedDevProperties(root),
|
|
116
|
-
...readDevPropertiesFile(root),
|
|
117
|
-
};
|
|
149
|
+
const merged = mergeDevLayers(readInheritedDevProperties(root), readDevPropertiesFile(root));
|
|
118
150
|
if (merged.domain && merged.username && !merged.password) {
|
|
119
151
|
merged.password =
|
|
120
152
|
process.env['SITEVISION_DEPLOY_PASSWORD'] ??
|
|
@@ -125,37 +157,31 @@ export function readWorkspaceDevProperties(root) {
|
|
|
125
157
|
}
|
|
126
158
|
/** Ancestor directories' .dev_properties.json merged, nearest wins (no own file). */
|
|
127
159
|
export function readAncestorDevProperties(root) {
|
|
128
|
-
|
|
129
|
-
for (const dir of ancestorDirs(root)) {
|
|
130
|
-
merged = { ...merged, ...readDevPropertiesFile(dir) };
|
|
131
|
-
}
|
|
132
|
-
return merged;
|
|
160
|
+
return mergeDevLayers(...ancestorDirs(root).map(dir => readDevPropertiesFile(dir)));
|
|
133
161
|
}
|
|
134
162
|
/** Shared defaults from package.json: the workspace root's first, the app's on top. */
|
|
135
163
|
export function readPackageDefaultsChain(root) {
|
|
136
|
-
|
|
137
|
-
for (const dir of [...ancestorDirs(root), root]) {
|
|
138
|
-
merged = { ...merged, ...readPackageDefaults(dir) };
|
|
139
|
-
}
|
|
140
|
-
return merged;
|
|
164
|
+
return mergeDevLayers(...[...ancestorDirs(root), root].map(dir => readPackageDefaults(dir)));
|
|
141
165
|
}
|
|
142
166
|
/**
|
|
143
167
|
* Everything an app's own .dev_properties.json sits on top of: package.json
|
|
144
168
|
* defaults (root, then app), then ancestor .dev_properties.json files.
|
|
145
169
|
*/
|
|
146
170
|
export function readInheritedDevProperties(root) {
|
|
147
|
-
return
|
|
148
|
-
...readPackageDefaultsChain(root),
|
|
149
|
-
...readAncestorDevProperties(root),
|
|
150
|
-
};
|
|
171
|
+
return mergeDevLayers(readPackageDefaultsChain(root), readAncestorDevProperties(root));
|
|
151
172
|
}
|
|
152
173
|
/**
|
|
153
174
|
* Get app ID configuration from environment or defaults
|
|
154
175
|
*/
|
|
155
|
-
function getAppIdConfig() {
|
|
176
|
+
export function getAppIdConfig() {
|
|
156
177
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
// svc's own names first, then the ones sitevision-scripts reads.
|
|
179
|
+
prefix: process.env['SITEVISION_APP_ID_PREFIX'] ||
|
|
180
|
+
process.env['APP_ID_PREFIX'] ||
|
|
181
|
+
'',
|
|
182
|
+
suffix: process.env['SITEVISION_APP_ID_SUFFIX'] ||
|
|
183
|
+
process.env['APP_ID_SUFFIX'] ||
|
|
184
|
+
'',
|
|
159
185
|
};
|
|
160
186
|
}
|
|
161
187
|
/**
|
|
@@ -270,7 +296,7 @@ export function buildImportEndpointUrl(domain, siteName, addonName, appType, use
|
|
|
270
296
|
export class ManifestParseError extends Error {
|
|
271
297
|
constructor(manifestPath, cause) {
|
|
272
298
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
273
|
-
super(`${manifestPath}
|
|
299
|
+
super(`${manifestPath} ${reason}`);
|
|
274
300
|
this.name = 'ManifestParseError';
|
|
275
301
|
}
|
|
276
302
|
}
|
|
@@ -290,15 +316,23 @@ export function readManifest(cwd) {
|
|
|
290
316
|
}
|
|
291
317
|
// Manifests may contain comments (Sitevision's own docs show them), so
|
|
292
318
|
// parse as JSONC.
|
|
319
|
+
let manifest;
|
|
293
320
|
try {
|
|
294
|
-
|
|
295
|
-
manifestPath,
|
|
296
|
-
manifest: parseJsonc(fs.readFileSync(manifestPath, 'utf-8')),
|
|
297
|
-
};
|
|
321
|
+
manifest = parseJsonc(fs.readFileSync(manifestPath, 'utf-8'));
|
|
298
322
|
}
|
|
299
323
|
catch (error) {
|
|
300
|
-
throw new ManifestParseError(manifestPath, error);
|
|
324
|
+
throw new ManifestParseError(manifestPath, `is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
}
|
|
326
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
327
|
+
throw new ManifestParseError(manifestPath, 'must be a JSON object');
|
|
328
|
+
}
|
|
329
|
+
// Zip names, endpoints and the build all derive from these.
|
|
330
|
+
for (const key of ['id', 'version', 'type']) {
|
|
331
|
+
if (typeof manifest[key] !== 'string' || manifest[key] === '') {
|
|
332
|
+
throw new ManifestParseError(manifestPath, `is missing "${key}"`);
|
|
333
|
+
}
|
|
301
334
|
}
|
|
335
|
+
return { manifestPath, manifest };
|
|
302
336
|
}
|
|
303
337
|
return null;
|
|
304
338
|
}
|
|
@@ -313,6 +347,11 @@ const escapeRegExp = (s) => s.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`
|
|
|
313
347
|
export function writeManifestField(manifestPath, key, value, lang) {
|
|
314
348
|
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
315
349
|
const wanted = parseJsonc(raw);
|
|
350
|
+
const existing = wanted[key];
|
|
351
|
+
if (lang && typeof existing !== 'object') {
|
|
352
|
+
// A plain string becomes the English text of a multilingual value.
|
|
353
|
+
wanted[key] = existing ? { en: existing } : {};
|
|
354
|
+
}
|
|
316
355
|
const holder = lang ? wanted[key] : wanted;
|
|
317
356
|
const leaf = lang ?? key;
|
|
318
357
|
// JSON.stringify drops an undefined key.
|
|
@@ -369,7 +408,9 @@ export function detectProject(cwd = process.cwd()) {
|
|
|
369
408
|
let hasLegacyPassword = false;
|
|
370
409
|
if (hasDevProperties) {
|
|
371
410
|
try {
|
|
372
|
-
|
|
411
|
+
// ponytail: the global signing identity only reaches a project that
|
|
412
|
+
// has some config of its own, so "configured?" checks stay truthful.
|
|
413
|
+
const parsed = mergeDevLayers(getGlobalSigning(), inherited, own);
|
|
373
414
|
hasLegacyPassword =
|
|
374
415
|
typeof parsed.password === 'string' && parsed.password.length > 0;
|
|
375
416
|
devProperties = parsed;
|
|
@@ -446,7 +487,7 @@ export function requireProject(cwd) {
|
|
|
446
487
|
* the whole shell down.
|
|
447
488
|
*/
|
|
448
489
|
export function appTypeOf(manifest) {
|
|
449
|
-
const type = manifest.type.toLowerCase();
|
|
490
|
+
const type = String(manifest.type ?? '').toLowerCase();
|
|
450
491
|
if (type.startsWith('web'))
|
|
451
492
|
return 'web';
|
|
452
493
|
if (type.startsWith('widget'))
|
|
@@ -505,10 +546,18 @@ export function writeDevProperties(projectRoot, properties,
|
|
|
505
546
|
// that would shadow the inherited value.
|
|
506
547
|
const inherited = complete
|
|
507
548
|
? {}
|
|
508
|
-
: readInheritedDevProperties(projectRoot);
|
|
549
|
+
: mergeDevLayers(getGlobalSigning(), readInheritedDevProperties(projectRoot));
|
|
509
550
|
const own = Object.fromEntries(Object.entries(persisted).filter(([key, value]) => value !== '' &&
|
|
510
551
|
(!Object.hasOwn(inherited, key) ||
|
|
511
552
|
JSON.stringify(inherited[key]) !== JSON.stringify(value))));
|
|
553
|
+
// A plaintext password already in the file (sitevision-scripts puts it
|
|
554
|
+
// there) moves to the keychain; if that fails it stays rather than be lost.
|
|
555
|
+
const legacy = readDevPropertiesFile(projectRoot)?.password;
|
|
556
|
+
if (legacy) {
|
|
557
|
+
const { domain, username } = { ...inherited, ...persisted };
|
|
558
|
+
if (!setDeployPassword(domain, username, legacy))
|
|
559
|
+
own['password'] = legacy;
|
|
560
|
+
}
|
|
512
561
|
fs.writeFileSync(devPropertiesPath, JSON.stringify(own, null, 2));
|
|
513
562
|
}
|
|
514
563
|
export function readSvcConfig(projectRoot) {
|
|
@@ -742,11 +791,13 @@ export function migrateLegacyPassword(project) {
|
|
|
742
791
|
}
|
|
743
792
|
}
|
|
744
793
|
catch {
|
|
745
|
-
// Unreadable file: nothing to migrate there.
|
|
794
|
+
// Unreadable file: nothing to migrate there. Unwritable: checked below.
|
|
746
795
|
}
|
|
747
796
|
}
|
|
748
|
-
|
|
749
|
-
|
|
797
|
+
// Moved only if no file still holds it.
|
|
798
|
+
const left = [project.root, ...ancestorDirs(project.root)].some(dir => readDevPropertiesFile(dir)?.password);
|
|
799
|
+
project.hasLegacyPassword = left;
|
|
800
|
+
return !left;
|
|
750
801
|
}
|
|
751
802
|
/**
|
|
752
803
|
* Ensure the dist directory exists
|
|
@@ -15,14 +15,15 @@ function domainRelated(a, b) {
|
|
|
15
15
|
* browser — so it's unit-testable.
|
|
16
16
|
*/
|
|
17
17
|
export function selectSessionCookie(all, siteDomain) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Only a session on the deploy site counts: a Java IdP sets a JSESSIONID of
|
|
19
|
+
// its own, and that one must never be sent to the site.
|
|
20
|
+
const chosen = all.find(c => c.name === 'JSESSIONID' && domainRelated(c.domain, siteDomain));
|
|
21
|
+
if (!chosen) {
|
|
20
22
|
const domains = [...new Set(all.map(c => bareDomain(c.domain)))];
|
|
21
23
|
return {
|
|
22
|
-
error: `No JSESSIONID among ${all.length} cookies. Domains seen: ${domains.join(', ') || 'none'}. If these are only your IdP, open a Sitevision page in the browser, then press Enter again.`,
|
|
24
|
+
error: `No JSESSIONID for ${siteDomain} among ${all.length} cookies. Domains seen: ${domains.join(', ') || 'none'}. If these are only your IdP, open a Sitevision page in the browser, then press Enter again.`,
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
|
-
const chosen = sessions.find(c => domainRelated(c.domain, siteDomain)) ?? sessions[0];
|
|
26
27
|
const cookies = all.filter(c => domainRelated(c.domain, chosen.domain));
|
|
27
28
|
return {
|
|
28
29
|
cookie: cookies.map(c => `${c.name}=${c.value}`).join('; '),
|