sitevision-cli 1.0.0-beta.31 → 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 +70 -0
- package/LICENSE +21 -0
- package/dist/cli.js +73 -34
- 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/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/ConfigForm.js +174 -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 +361 -43
- 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 +226 -0
- package/dist/utils/i18n.js +68 -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 +8 -3
- package/dist/utils/project-detection.js +172 -44
- 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 +132 -66
- 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/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 -36
- 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 -10
- package/dist/shell/Tabs.d.ts +0 -36
- package/dist/shell/actions.d.ts +0 -48
- 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/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 -31
- package/dist/utils/project-detection.d.ts +0 -202
- 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 -50
- 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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New apps are scaffolded by Sitevision's own tool. svc runs it as a task and
|
|
3
|
+
* answers its questions through a socket (see source/scaffold); when the tool no
|
|
4
|
+
* longer asks through inquirer the run is reported as unmanaged, and the caller
|
|
5
|
+
* hands it the real terminal instead.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
14
|
+
import { t } from './i18n.js';
|
|
15
|
+
import { ProcessRunner } from './process-runner.js';
|
|
16
|
+
import { createTask, setPhase } from './tasks.js';
|
|
17
|
+
import { detectProject, findDevPropertiesPath, migrateLegacyPassword, readInheritedDevProperties, readManifest, updatePackageJson, writeManifestField, } from './project-detection.js';
|
|
18
|
+
export const CREATE_COMMAND = [
|
|
19
|
+
'npx',
|
|
20
|
+
'--yes',
|
|
21
|
+
'@sitevision/create-sitevision-app',
|
|
22
|
+
];
|
|
23
|
+
/** A folder name that is also a usable package name and manifest id. */
|
|
24
|
+
export const validAppName = (name) => /^[\da-z][\w\-.]*$/i.test(name);
|
|
25
|
+
/** The closest conventional name: "Min nya app!" and "MinNyaApp" become "min-nya-app". */
|
|
26
|
+
export function suggestAppName(name) {
|
|
27
|
+
return name
|
|
28
|
+
.trim()
|
|
29
|
+
.normalize('NFKD')
|
|
30
|
+
.replaceAll(/[\u0300-\u036F]/g, '')
|
|
31
|
+
.replaceAll(/(?<=[\da-z])(?=[A-Z])/g, '-')
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replaceAll(/\s+/g, '-')
|
|
34
|
+
.replaceAll(/[^\w\-.]/g, '')
|
|
35
|
+
.replace(/^[^\da-z]+/i, '');
|
|
36
|
+
}
|
|
37
|
+
/** What is wrong with a typed app name, said so the fix is obvious. */
|
|
38
|
+
export function appNameProblem(typed) {
|
|
39
|
+
const name = typed.trim();
|
|
40
|
+
if (!name)
|
|
41
|
+
return t('Type a name for the app, for example my-new-app.');
|
|
42
|
+
if (validAppName(name))
|
|
43
|
+
return undefined;
|
|
44
|
+
const suggestion = suggestAppName(name);
|
|
45
|
+
const tryThis = suggestion
|
|
46
|
+
? ' ' + t('Try "{suggestion}".', { suggestion })
|
|
47
|
+
: '';
|
|
48
|
+
if (/\s/.test(name) && validAppName(name.replaceAll(/\s+/g, '-')))
|
|
49
|
+
return t("An app name can't have spaces.") + tryThis;
|
|
50
|
+
const bad = [...new Set(name.replaceAll(/[\s\w\-.]/g, ''))].join(' ');
|
|
51
|
+
if (bad) {
|
|
52
|
+
return (t("An app name can't contain {chars}: it becomes the folder, the package name and the app id.", {
|
|
53
|
+
chars: bad,
|
|
54
|
+
}) + tryThis);
|
|
55
|
+
}
|
|
56
|
+
return t('Start the name with a letter or a digit.') + tryThis;
|
|
57
|
+
}
|
|
58
|
+
/** A valid name that could be better; the user may keep it. */
|
|
59
|
+
export function appNameAdvice(typed) {
|
|
60
|
+
const name = typed.trim();
|
|
61
|
+
if (!validAppName(name) || name === name.toLowerCase())
|
|
62
|
+
return undefined;
|
|
63
|
+
return t('Lowercase is safer: npm expects it, and "{name}" and "{lower}" are one folder on macOS and Windows but two on Linux. Try "{suggestion}", or press Enter again to keep "{name}".', { name, lower: name.toLowerCase(), suggestion: suggestAppName(name) });
|
|
64
|
+
}
|
|
65
|
+
export const isEmptyOrMissing = (dir) => !fs.existsSync(dir) || fs.readdirSync(dir).length === 0;
|
|
66
|
+
/** Listen for the stub's questions. `null` from `ask` means the user cancelled. */
|
|
67
|
+
export async function servePrompts(ask, onCancel) {
|
|
68
|
+
const id = `svc-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
69
|
+
const socketPath = process.platform === 'win32'
|
|
70
|
+
? `\\\\.\\pipe\\${id}`
|
|
71
|
+
: path.join(os.tmpdir(), `${id}.sock`);
|
|
72
|
+
let connected = false;
|
|
73
|
+
const server = net.createServer(socket => {
|
|
74
|
+
connected = true;
|
|
75
|
+
socket.on('error', () => { });
|
|
76
|
+
void (async () => {
|
|
77
|
+
for await (const line of readline.createInterface({ input: socket })) {
|
|
78
|
+
const reply = await ask(JSON.parse(line));
|
|
79
|
+
if (!reply) {
|
|
80
|
+
socket.destroy();
|
|
81
|
+
onCancel();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
socket.write(JSON.stringify(reply) + '\n');
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
});
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
server.once('error', reject);
|
|
90
|
+
server.listen(socketPath, resolve);
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
socketPath,
|
|
94
|
+
connected: () => connected,
|
|
95
|
+
close: () => server.close(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run the scaffolder for `name` inside `parentDir`. "unmanaged" means it never
|
|
100
|
+
* asked svc anything and left no app: the prompt bridge did not take.
|
|
101
|
+
*/
|
|
102
|
+
export function startCreateApp(options) {
|
|
103
|
+
const { name, parentDir, ask, command = CREATE_COMMAND } = options;
|
|
104
|
+
const appDir = path.join(parentDir, name);
|
|
105
|
+
let runner;
|
|
106
|
+
let stopped = false;
|
|
107
|
+
const { task, log, finish } = createTask('create', { root: appDir, manifest: { name, id: name } }, 'create-sitevision-app', () => {
|
|
108
|
+
stopped = true;
|
|
109
|
+
runner?.kill();
|
|
110
|
+
finish('stopped');
|
|
111
|
+
});
|
|
112
|
+
const done = (async () => {
|
|
113
|
+
const prompts = await servePrompts(ask, () => task.stop());
|
|
114
|
+
try {
|
|
115
|
+
// .js from dist, .ts when svc itself runs from source.
|
|
116
|
+
const register = new URL(`../scaffold/register${path.extname(import.meta.url)}`, import.meta.url).href;
|
|
117
|
+
runner = new ProcessRunner(command[0], [...command.slice(1), name], parentDir, {
|
|
118
|
+
...process.env,
|
|
119
|
+
NODE_OPTIONS: `${process.env['NODE_OPTIONS'] ?? ''} --import=${register}`.trim(),
|
|
120
|
+
SVC_PROMPT_SOCKET: prompts.socketPath,
|
|
121
|
+
});
|
|
122
|
+
runner.on('output', (output) => {
|
|
123
|
+
// The tool clears the screen between steps; that is noise in a log.
|
|
124
|
+
const text = stripVTControlCharacters(output.data);
|
|
125
|
+
if (text.trim())
|
|
126
|
+
log('create', text, output.type === 'stderr' ? 'warn' : 'info');
|
|
127
|
+
});
|
|
128
|
+
setPhase(task, 'scaffolding');
|
|
129
|
+
await runner.run();
|
|
130
|
+
if (stopped)
|
|
131
|
+
return 'stopped';
|
|
132
|
+
// The exit code is 0 even when the prompts crash, so look at the result.
|
|
133
|
+
if (readManifest(appDir)) {
|
|
134
|
+
finish('success');
|
|
135
|
+
return 'created';
|
|
136
|
+
}
|
|
137
|
+
if (prompts.connected()) {
|
|
138
|
+
finish('error', 'The scaffolder finished without creating an app');
|
|
139
|
+
return 'failed';
|
|
140
|
+
}
|
|
141
|
+
finish('stopped');
|
|
142
|
+
return 'unmanaged';
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
finish('error', error instanceof Error ? error.message : String(error));
|
|
146
|
+
return 'failed';
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
prompts.close();
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
return { task, done };
|
|
153
|
+
}
|
|
154
|
+
/** Run the scaffolder on the real terminal; resolves when it exits. */
|
|
155
|
+
export async function createAppInTerminal(name, parentDir) {
|
|
156
|
+
await new Promise(resolve => {
|
|
157
|
+
// `name` is validated by the caller; joined to avoid DEP0190.
|
|
158
|
+
const child = spawn([...CREATE_COMMAND, name].join(' '), {
|
|
159
|
+
cwd: parentDir,
|
|
160
|
+
stdio: 'inherit',
|
|
161
|
+
shell: true,
|
|
162
|
+
});
|
|
163
|
+
child.on('close', () => resolve());
|
|
164
|
+
child.on('error', () => resolve());
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
return Boolean(readManifest(path.join(parentDir, name)));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// A half-written manifest is a failed scaffold, not a crash.
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Replace the template's placeholders with what is known, and fold the config
|
|
176
|
+
* the tool wrote into svc's layout: no plaintext password, and nothing in the
|
|
177
|
+
* app's .dev_properties.json that the workspace already provides.
|
|
178
|
+
*/
|
|
179
|
+
export function seedNewApp(appDir, workspaceRoot) {
|
|
180
|
+
const found = readManifest(appDir);
|
|
181
|
+
if (!found)
|
|
182
|
+
return;
|
|
183
|
+
const { manifest, manifestPath } = found;
|
|
184
|
+
const seed = (key, value) => {
|
|
185
|
+
const localized = typeof manifest[key] === 'object';
|
|
186
|
+
writeManifestField(manifestPath, key, value, localized ? 'en' : undefined);
|
|
187
|
+
};
|
|
188
|
+
seed('name', path.basename(appDir));
|
|
189
|
+
if (workspaceRoot) {
|
|
190
|
+
let shared = {};
|
|
191
|
+
try {
|
|
192
|
+
shared = JSON.parse(fs.readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8'));
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
const author = typeof shared['author'] === 'object'
|
|
196
|
+
? shared['author']?.name
|
|
197
|
+
: shared['author'];
|
|
198
|
+
if (typeof author === 'string' && author)
|
|
199
|
+
writeManifestField(manifestPath, 'author', author);
|
|
200
|
+
if (typeof shared['homepage'] === 'string' && shared['homepage'])
|
|
201
|
+
writeManifestField(manifestPath, 'helpUrl', shared['homepage']);
|
|
202
|
+
}
|
|
203
|
+
const project = detectProject(appDir);
|
|
204
|
+
if (project)
|
|
205
|
+
migrateLegacyPassword(project);
|
|
206
|
+
const file = findDevPropertiesPath(appDir);
|
|
207
|
+
if (!workspaceRoot || !file)
|
|
208
|
+
return;
|
|
209
|
+
const own = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
210
|
+
const inherited = readInheritedDevProperties(appDir);
|
|
211
|
+
if (typeof own['addonName'] === 'string' && own['addonName']) {
|
|
212
|
+
const addonName = own['addonName'];
|
|
213
|
+
updatePackageJson(appDir, packageJson => {
|
|
214
|
+
packageJson['addonName'] = addonName;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const rest = Object.fromEntries(Object.entries(own).filter(([key, value]) => key !== 'addonName' &&
|
|
218
|
+
value !== '' &&
|
|
219
|
+
value !== undefined &&
|
|
220
|
+
!(key === 'useHTTPForDevDeploy' && value === false) &&
|
|
221
|
+
JSON.stringify(value) !== JSON.stringify(inherited[key])));
|
|
222
|
+
if (Object.keys(rest).length === 0)
|
|
223
|
+
fs.rmSync(file);
|
|
224
|
+
else
|
|
225
|
+
fs.writeFileSync(file, JSON.stringify(rest, null, 2) + '\n');
|
|
226
|
+
}
|
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,11 +204,43 @@ 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',
|
|
204
211
|
SIGNING: 'SIGNERING',
|
|
212
|
+
MANIFEST: 'MANIFEST',
|
|
213
|
+
// New app
|
|
214
|
+
'Name of the new app': 'Namn på den nya appen',
|
|
215
|
+
'Create it in folder': 'Skapa den i mappen',
|
|
216
|
+
'Other folder…': 'Annan mapp…',
|
|
217
|
+
'Type a name for the app, for example my-new-app.': 'Skriv ett namn på appen, till exempel min-nya-app.',
|
|
218
|
+
'Try "{suggestion}".': 'Prova "{suggestion}".',
|
|
219
|
+
"An app name can't have spaces.": 'Ett appnamn kan inte innehålla mellanslag.',
|
|
220
|
+
"An app name can't contain {chars}: it becomes the folder, the package name and the app id.": 'Ett appnamn kan inte innehålla {chars}: det blir mappen, paketnamnet och appens id.',
|
|
221
|
+
'Lowercase is safer: npm expects it, and "{name}" and "{lower}" are one folder on macOS and Windows but two on Linux. Try "{suggestion}", or press Enter again to keep "{name}".': 'Gemener är säkrare: npm förväntar sig det, och "{name}" och "{lower}" är samma mapp på macOS och Windows men två på Linux. Prova "{suggestion}", eller tryck Enter igen för att behålla "{name}".',
|
|
222
|
+
'Start the name with a letter or a digit.': 'Börja namnet med en bokstav eller en siffra.',
|
|
223
|
+
'{dir} already exists. Pick another name, or keep it and choose a different folder next.': '{dir} finns redan. Välj ett annat namn, eller behåll det och välj en annan mapp i nästa steg.',
|
|
224
|
+
'creating {app}: installing, questions follow': 'skapar {app}: installerar, frågor kommer strax',
|
|
225
|
+
'The scaffolder could not be run from here': 'Verktyget kunde inte köras härifrån',
|
|
226
|
+
'creating {app} failed, see the log': 'kunde inte skapa {app}, se loggen',
|
|
227
|
+
'{app} created in {dir}': '{app} skapad i {dir}',
|
|
228
|
+
'{app} created': '{app} skapad',
|
|
229
|
+
'Create the addon "{addon}" on {domain} now?': 'Skapa tillägget "{addon}" på {domain} nu?',
|
|
230
|
+
'↑↓ move · Space toggle · Enter submit · Esc cancel': '↑↓ flytta · Mellanslag markera · Enter skicka · Esc avbryt',
|
|
231
|
+
'App id': 'App-id',
|
|
232
|
+
Version: 'Version',
|
|
233
|
+
Name: 'Namn',
|
|
234
|
+
Description: 'Beskrivning',
|
|
235
|
+
Author: 'Författare',
|
|
236
|
+
'Help URL': 'Hjälp-URL',
|
|
237
|
+
'Not saved: {label} is required.': 'Inte sparat: {label} krävs.',
|
|
238
|
+
'Identifier of the app in Sitevision; it also names the zip. Changing it makes the next deploy a new app instead of an update.': 'Appens identifierare i Sitevision; den namnger också zip-filen. Ändras den blir nästa driftsättning en ny app i stället för en uppdatering.',
|
|
239
|
+
'Version of the app. Id and version together identify an upload, so bump it to deploy a new version instead of overwriting the current one.': 'Appens version. Id och version identifierar tillsammans en uppladdning, så höj den för att driftsätta en ny version i stället för att skriva över den nuvarande.',
|
|
240
|
+
'Name shown when importing and administering the app in the Sitevision editor. A multilingual manifest needs at least English.': 'Namnet som visas när appen importeras och administreras i Sitevisions redigerare. Ett flerspråkigt manifest behöver minst engelska.',
|
|
241
|
+
'Short description shown next to the name in the Sitevision editor.': 'Kort beskrivning som visas bredvid namnet i Sitevisions redigerare.',
|
|
242
|
+
'Who made the app, shown in the Sitevision editor.': 'Vem som gjort appen, visas i Sitevisions redigerare.',
|
|
243
|
+
"Link to the app's documentation, shown in the Sitevision editor.": 'Länk till appens dokumentation, visas i Sitevisions redigerare.',
|
|
205
244
|
ENVIRONMENT: 'MILJÖ',
|
|
206
245
|
'Environment name': 'Miljönamn',
|
|
207
246
|
Production: 'Produktion',
|
|
@@ -274,7 +313,9 @@ const sv = {
|
|
|
274
313
|
// Config help
|
|
275
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.',
|
|
276
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.',
|
|
277
|
-
"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}',
|
|
278
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.',
|
|
279
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.',
|
|
280
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.',
|
|
@@ -290,6 +331,20 @@ const sv = {
|
|
|
290
331
|
// Settings screen
|
|
291
332
|
Language: 'Språk',
|
|
292
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',
|
|
293
348
|
on: 'på',
|
|
294
349
|
off: 'av',
|
|
295
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
|
|
@@ -53,16 +53,21 @@ export class ProcessRunner extends EventEmitter {
|
|
|
53
53
|
command;
|
|
54
54
|
args;
|
|
55
55
|
cwd;
|
|
56
|
-
|
|
56
|
+
env;
|
|
57
|
+
constructor(command, args = [], cwd, env) {
|
|
57
58
|
super();
|
|
58
59
|
this.command = command;
|
|
59
60
|
this.args = args;
|
|
60
61
|
this.cwd = cwd;
|
|
62
|
+
this.env = env;
|
|
61
63
|
}
|
|
62
64
|
run() {
|
|
63
65
|
return new Promise((resolve, reject) => {
|
|
64
|
-
|
|
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(' '), [], {
|
|
65
69
|
cwd: this.cwd || process.cwd(),
|
|
70
|
+
env: this.env,
|
|
66
71
|
shell: true,
|
|
67
72
|
stdio: 'pipe',
|
|
68
73
|
});
|
|
@@ -81,7 +86,7 @@ export class ProcessRunner extends EventEmitter {
|
|
|
81
86
|
reject(error);
|
|
82
87
|
});
|
|
83
88
|
this.process.on('close', code => {
|
|
84
|
-
const exitCode = code ??
|
|
89
|
+
const exitCode = code ?? 1;
|
|
85
90
|
this.emit('exit', exitCode);
|
|
86
91
|
resolve({
|
|
87
92
|
exitCode,
|