sitevision-cli 1.0.0-beta.9 → 1.0.1
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 +143 -0
- package/LICENSE +21 -0
- package/dist/cli.js +194 -70
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +95 -212
- package/dist/commands/dev.js +87 -416
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +50 -33
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +5 -31
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.js +33 -15
- package/dist/components/PasswordInput.js +2 -1
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.js +50 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.js +703 -0
- package/dist/shell/Frame.js +138 -0
- package/dist/shell/Help.js +96 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.js +149 -0
- package/dist/shell/Shell.js +953 -0
- package/dist/shell/Tabs.js +96 -0
- package/dist/shell/actions.js +493 -0
- package/dist/utils/config.js +62 -2
- package/dist/utils/create-app.js +231 -0
- package/dist/utils/debug.js +32 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.js +369 -0
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +54 -6
- package/dist/utils/oauth2-auth.js +146 -43
- package/dist/utils/password-prompt.js +37 -3
- package/dist/utils/process-runner.js +77 -38
- package/dist/utils/project-detection.js +457 -112
- package/dist/utils/session-cookie-auth.js +10 -6
- package/dist/utils/sitevision-api.js +134 -25
- package/dist/utils/sitevision-scripts-runner.js +50 -22
- package/dist/utils/tasks.js +510 -0
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +9 -5
- package/dist/utils/workspace.js +105 -0
- package/dist/utils/zip.js +18 -19
- package/package.json +29 -5
- package/readme.md +117 -141
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -307
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -16
- package/dist/commands/dev.d.ts +0 -16
- 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 -10
- package/dist/components/DevPropertiesForm.js +0 -210
- 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 -83
- 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/types/index.d.ts +0 -284
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -17
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -27
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -22
- package/dist/utils/project-detection.d.ts +0 -151
- package/dist/utils/session-cookie-auth.d.ts +0 -34
- 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 -112
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/zip.d.ts +0 -71
|
@@ -1,42 +1,45 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import { findDevPropertiesPath } from '../utils/project-detection.js';
|
|
5
|
+
import { configProblem, getGlobalSigning, setGlobalSigning, settingsFile, } from '../utils/config.js';
|
|
6
|
+
import { debug, errorText, say } from '../utils/debug.js';
|
|
7
|
+
// rl.question drops lines that arrive before it is called, which is every line
|
|
8
|
+
// but the first when stdin is piped. The iterator buffers them.
|
|
9
|
+
function lineReader(rl) {
|
|
10
|
+
const lines = rl[Symbol.asyncIterator]();
|
|
11
|
+
return async (prompt) => {
|
|
12
|
+
process.stdout.write(prompt);
|
|
13
|
+
const { value, done } = await lines.next();
|
|
14
|
+
return done ? '' : String(value);
|
|
15
|
+
};
|
|
10
16
|
}
|
|
11
17
|
export const setupSigningCommand = {
|
|
12
18
|
name: 'setup-signing',
|
|
13
19
|
description: 'Configure signing credentials for developer.sitevision.se',
|
|
14
20
|
requiresProject: true,
|
|
15
|
-
async execute({ project }) {
|
|
21
|
+
async execute({ project, flags }) {
|
|
22
|
+
const global = Boolean(flags['global']);
|
|
16
23
|
const rl = readline.createInterface({
|
|
17
24
|
input: process.stdin,
|
|
18
25
|
output: process.stdout,
|
|
19
26
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
path.join(project.root, '.dev_properties.json')
|
|
26
|
-
path.join(project.root, '.dev-properties.json'),
|
|
27
|
-
];
|
|
28
|
-
let devPropertiesPath = devPropertiesPaths[0];
|
|
27
|
+
const question = lineReader(rl);
|
|
28
|
+
say('\n\x1b[36m\x1b[1mSetup Signing Credentials\x1b[0m\n');
|
|
29
|
+
say('Configure credentials for signing apps on developer.sitevision.se');
|
|
30
|
+
say('(Password will be prompted when running signing commands)\n');
|
|
31
|
+
const devPropertiesPath = findDevPropertiesPath(project.root) ??
|
|
32
|
+
path.join(project.root, '.dev_properties.json');
|
|
29
33
|
let existingProperties = {};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
break;
|
|
34
|
+
if (global) {
|
|
35
|
+
existingProperties = { ...getGlobalSigning() };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
try {
|
|
39
|
+
existingProperties = JSON.parse(fs.readFileSync(devPropertiesPath, 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Missing or invalid file, start fresh
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
try {
|
|
@@ -45,13 +48,14 @@ export const setupSigningCommand = {
|
|
|
45
48
|
const usernamePrompt = defaultUsername
|
|
46
49
|
? `Signing username [${defaultUsername}]: `
|
|
47
50
|
: 'Signing username: ';
|
|
48
|
-
let signingUsername = await question(
|
|
51
|
+
let signingUsername = await question(usernamePrompt);
|
|
49
52
|
if (!signingUsername && defaultUsername) {
|
|
50
53
|
signingUsername = defaultUsername;
|
|
51
54
|
}
|
|
52
55
|
if (!signingUsername) {
|
|
53
|
-
|
|
56
|
+
say('\x1b[31mError: Signing username is required\x1b[0m');
|
|
54
57
|
rl.close();
|
|
58
|
+
process.exitCode = 1;
|
|
55
59
|
return;
|
|
56
60
|
}
|
|
57
61
|
// Get certificate name (optional)
|
|
@@ -59,24 +63,37 @@ export const setupSigningCommand = {
|
|
|
59
63
|
const certPrompt = defaultCertName
|
|
60
64
|
? `Certificate name (blank for default) [${defaultCertName}]: `
|
|
61
65
|
: 'Certificate name (blank for default): ';
|
|
62
|
-
let certificateName = await question(
|
|
66
|
+
let certificateName = await question(certPrompt);
|
|
63
67
|
if (!certificateName && defaultCertName) {
|
|
64
68
|
certificateName = defaultCertName;
|
|
65
69
|
}
|
|
66
70
|
rl.close();
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
if (global) {
|
|
72
|
+
if (configProblem()) {
|
|
73
|
+
say(`\x1b[31mError: ${settingsFile()} does not parse; fix it first\x1b[0m`);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setGlobalSigning({
|
|
78
|
+
signingUsername,
|
|
79
|
+
...(certificateName && { certificateName }),
|
|
80
|
+
});
|
|
81
|
+
say(`\n\x1b[32mSigning credentials saved to ${settingsFile()}\x1b[0m\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
69
84
|
const updatedProperties = {
|
|
70
|
-
...
|
|
85
|
+
...existingProperties,
|
|
71
86
|
signingUsername,
|
|
72
87
|
...(certificateName && { certificateName }),
|
|
73
88
|
};
|
|
74
89
|
fs.writeFileSync(devPropertiesPath, JSON.stringify(updatedProperties, null, 2));
|
|
75
|
-
|
|
90
|
+
say(`\n\x1b[32mSigning credentials saved to ${path.basename(devPropertiesPath)}\x1b[0m\n`);
|
|
76
91
|
}
|
|
77
92
|
catch (error) {
|
|
78
93
|
rl.close();
|
|
94
|
+
debug('out', `Error setting up signing credentials: ${errorText(error)}`);
|
|
79
95
|
console.error('\x1b[31mError setting up signing credentials:\x1b[0m', error);
|
|
96
|
+
process.exitCode = 1;
|
|
80
97
|
}
|
|
81
98
|
},
|
|
82
99
|
};
|
package/dist/commands/sign.js
CHANGED
|
@@ -1,94 +1,17 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { resolveSigningPassword } from '../utils/signing-password.js';
|
|
7
|
-
import { getZipPath, getSignedZipPath } from '../utils/project-detection.js';
|
|
8
|
-
import { formatFileSize, getZipSize, zipExists } from '../utils/zip.js';
|
|
9
|
-
export function SignScreen({ projectRoot, manifest, devProperties, password, onBack, onRetryCredentials, }) {
|
|
10
|
-
const [state, setState] = React.useState({
|
|
11
|
-
status: 'signing',
|
|
12
|
-
message: 'Signing app via developer.sitevision.se...',
|
|
13
|
-
});
|
|
14
|
-
useInput((input, key) => {
|
|
15
|
-
if (state.status !== 'signing') {
|
|
16
|
-
if (onBack && (key.escape || input === 'q')) {
|
|
17
|
-
onBack();
|
|
18
|
-
}
|
|
19
|
-
if (onRetryCredentials && state.status === 'error' && input === 'r') {
|
|
20
|
-
onRetryCredentials();
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
React.useEffect(() => {
|
|
25
|
-
async function runSign() {
|
|
26
|
-
try {
|
|
27
|
-
const zipPath = getZipPath(projectRoot, manifest);
|
|
28
|
-
const signedZipPath = getSignedZipPath(projectRoot, manifest);
|
|
29
|
-
// Validate zip exists
|
|
30
|
-
if (!zipExists(zipPath)) {
|
|
31
|
-
setState({
|
|
32
|
-
status: 'error',
|
|
33
|
-
error: `Zip file not found: ${zipPath}\nRun 'build' first to create the zip file.`,
|
|
34
|
-
});
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
// Sign the app
|
|
38
|
-
const result = await signApp(zipPath, {
|
|
39
|
-
username: devProperties.signingUsername,
|
|
40
|
-
password,
|
|
41
|
-
certificateName: devProperties.certificateName,
|
|
42
|
-
}, signedZipPath);
|
|
43
|
-
if (!result.success) {
|
|
44
|
-
setState({
|
|
45
|
-
status: 'error',
|
|
46
|
-
error: result.error || 'Signing failed',
|
|
47
|
-
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const signedSize = getZipSize(signedZipPath);
|
|
51
|
-
setState({
|
|
52
|
-
status: 'success',
|
|
53
|
-
message: 'App signed successfully',
|
|
54
|
-
signedPath: signedZipPath,
|
|
55
|
-
signedSize,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
setState({
|
|
60
|
-
status: 'error',
|
|
61
|
-
error: error instanceof Error ? error.message : String(error),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
runSign();
|
|
66
|
-
}, [projectRoot, manifest, devProperties, password]);
|
|
67
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: state.status === 'signing' ? 'running' : state.status, label: state.status === 'signing'
|
|
68
|
-
? 'Signing'
|
|
69
|
-
: state.status === 'success'
|
|
70
|
-
? 'Signed'
|
|
71
|
-
: 'Failed', message: state.message }) }), state.status === 'success' && state.signedPath && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: ["Created: ", state.signedPath] }), state.signedSize !== undefined && (_jsxs(Text, { dimColor: true, children: ["Size: ", formatFileSize(state.signedSize)] }))] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), state.status !== 'signing' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack && _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" })] }))] }));
|
|
72
|
-
}
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { TaskScreen } from './TaskScreen.js';
|
|
4
|
+
import { resolveSigningForCli } from './dev.js';
|
|
5
|
+
import { startSign } from '../utils/tasks.js';
|
|
73
6
|
export const signCommand = {
|
|
74
7
|
name: 'sign',
|
|
75
8
|
description: 'Sign the app for production deployment',
|
|
76
9
|
requiresProject: true,
|
|
77
10
|
async execute({ project }) {
|
|
78
|
-
|
|
79
|
-
if (!
|
|
80
|
-
!project.devProperties?.signingUsername) {
|
|
81
|
-
console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
|
|
82
|
-
console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
|
|
11
|
+
const credentials = await resolveSigningForCli(project);
|
|
12
|
+
if (!credentials)
|
|
83
13
|
return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const password = await resolveSigningPassword(signingUsername);
|
|
87
|
-
if (!password) {
|
|
88
|
-
console.log('\x1b[31mError: Password is required\x1b[0m');
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const { waitUntilExit } = render(_jsx(SignScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, password: password }));
|
|
92
|
-
await waitUntilExit();
|
|
14
|
+
const task = startSign(project, credentials);
|
|
15
|
+
await render(_jsx(TaskScreen, { task: task })).waitUntilExit();
|
|
93
16
|
},
|
|
94
17
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useApp } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Standalone commands end themselves: once `result` is set the Ink app exits,
|
|
5
|
+
* and a failure becomes a non-zero exit code. `stay` keeps the screen up (for a
|
|
6
|
+
* retry key) while still recording the failure.
|
|
7
|
+
*/
|
|
8
|
+
export function useFinish(result, stay = false) {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (!result)
|
|
12
|
+
return;
|
|
13
|
+
process.exitCode = result === 'error' ? 1 : 0;
|
|
14
|
+
if (!stay)
|
|
15
|
+
exit();
|
|
16
|
+
}, [result, stay, exit]);
|
|
17
|
+
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -1,44 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from 'ink';
|
|
3
|
-
import { DevScreen } from './dev.js';
|
|
4
|
-
import { resolveSigningPassword } from '../utils/signing-password.js';
|
|
3
|
+
import { DevScreen, resolveSigningForCli } from './dev.js';
|
|
5
4
|
export const watchCommand = {
|
|
6
5
|
name: 'watch',
|
|
7
6
|
description: 'Watch and rebuild (optionally signing) without deploying',
|
|
8
7
|
requiresProject: true,
|
|
9
|
-
flags: {
|
|
10
|
-
signed: {
|
|
11
|
-
type: 'boolean',
|
|
12
|
-
description: 'Sign after each build',
|
|
13
|
-
alias: 's',
|
|
14
|
-
default: false,
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
8
|
async execute({ project, flags }) {
|
|
18
|
-
const signed = Boolean(flags['signed']);
|
|
19
9
|
let signingCredentials;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!project.hasSigningProperties ||
|
|
24
|
-
!project.devProperties?.signingUsername) {
|
|
25
|
-
console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
|
|
26
|
-
console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const signingUsername = project.devProperties.signingUsername;
|
|
30
|
-
const password = await resolveSigningPassword(signingUsername);
|
|
31
|
-
if (!password) {
|
|
32
|
-
console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
|
|
10
|
+
if (flags['signed']) {
|
|
11
|
+
signingCredentials = await resolveSigningForCli(project);
|
|
12
|
+
if (!signingCredentials)
|
|
33
13
|
return;
|
|
34
|
-
}
|
|
35
|
-
signingCredentials = {
|
|
36
|
-
username: signingUsername,
|
|
37
|
-
password,
|
|
38
|
-
certificateName: project.devProperties.certificateName,
|
|
39
|
-
};
|
|
40
14
|
}
|
|
41
|
-
const { waitUntilExit } = render(_jsx(DevScreen, {
|
|
15
|
+
const { waitUntilExit } = render(_jsx(DevScreen, { project: project, options: { deploy: false, signingCredentials } }));
|
|
42
16
|
await waitUntilExit();
|
|
43
17
|
},
|
|
44
18
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { AUTHOR, BIG_LOGO } from '../utils/branding.js';
|
|
5
5
|
const FRAME_MS = 45;
|
|
6
6
|
const SWEEP_COLS_PER_FRAME = 7; // how fast the wipe edge moves left → right
|
|
@@ -83,5 +83,11 @@ export function AnimatedLogo({ art = BIG_LOGO, onDone }) {
|
|
|
83
83
|
}
|
|
84
84
|
}, [frame, total, onDone]);
|
|
85
85
|
const edge = (frame + 1) * SWEEP_COLS_PER_FRAME;
|
|
86
|
-
|
|
86
|
+
// Centre the wordmark on the alternate screen. `rows - 1` leaves room for
|
|
87
|
+
// Ink's trailing newline; a terminal too short for the art just renders it
|
|
88
|
+
// top-aligned rather than scrolling.
|
|
89
|
+
const { stdout } = useStdout();
|
|
90
|
+
const height = (stdout.rows || 24) - 1;
|
|
91
|
+
const fits = height >= art.length + 2;
|
|
92
|
+
return (_jsx(Box, { width: stdout.columns || 80, height: fits ? height : undefined, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", padding: 1, children: [art.map((line, y) => (_jsx(Text, { children: buildSpans(line, y, frame, edge).map((span, index) => (_jsx(Fragment, { children: span.color ? (_jsx(Text, { color: span.color, children: span.text })) : (_jsx(Text, { children: span.text })) }, index))) }, y))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ' a tool by ' }), _jsx(Text, { bold: true, children: AUTHOR })] })] }) }));
|
|
87
93
|
}
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
|
-
import {
|
|
5
|
+
import { t } from '../utils/i18n.js';
|
|
6
|
+
import { beginOAuth2Login, oauth2ConfigProblem, openBrowser, } from '../utils/oauth2-auth.js';
|
|
6
7
|
import { beginCookieLogin, } from '../utils/session-cookie-auth.js';
|
|
7
8
|
/**
|
|
8
9
|
* Ink-native interactive login for OAuth2 and session-cookie auth. Drives the
|
|
@@ -14,42 +15,53 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
14
15
|
const [phase, setPhase] = React.useState('starting');
|
|
15
16
|
const [authUrl, setAuthUrl] = React.useState('');
|
|
16
17
|
const [note, setNote] = React.useState('');
|
|
18
|
+
const [loginUrl, setLoginUrl] = React.useState('');
|
|
17
19
|
const cookieRef = React.useRef(null);
|
|
18
20
|
const cancelOAuthRef = React.useRef(null);
|
|
19
21
|
React.useEffect(() => {
|
|
22
|
+
// Set by the cleanup: the screen went away while a login was starting.
|
|
23
|
+
let gone = false;
|
|
20
24
|
void (async () => {
|
|
21
25
|
if (method === 'oauth2') {
|
|
22
26
|
const session = beginOAuth2Login(devProperties);
|
|
23
27
|
if (!session) {
|
|
24
|
-
onError('
|
|
28
|
+
onError(oauth2ConfigProblem(devProperties) ?? 'Could not start the login.');
|
|
25
29
|
return;
|
|
26
30
|
}
|
|
27
31
|
cancelOAuthRef.current = session.cancel;
|
|
28
32
|
setAuthUrl(session.authUrl);
|
|
29
33
|
openBrowser(session.authUrl);
|
|
30
34
|
setPhase('awaiting');
|
|
31
|
-
const token = await session.complete();
|
|
35
|
+
const { token, error } = await session.complete();
|
|
32
36
|
cancelOAuthRef.current = null;
|
|
37
|
+
if (gone)
|
|
38
|
+
return;
|
|
33
39
|
if (token) {
|
|
34
40
|
onComplete({ accessToken: token });
|
|
35
41
|
}
|
|
36
42
|
else {
|
|
37
|
-
onError('OAuth2 login failed
|
|
43
|
+
onError(error ?? 'OAuth2 login failed.');
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
else {
|
|
41
47
|
const session = await beginCookieLogin(devProperties);
|
|
48
|
+
if (gone) {
|
|
49
|
+
void session?.close();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
42
52
|
if (!session) {
|
|
43
|
-
onError('Could not open a login browser (is Chrome installed?). Set SITEVISION_SESSION_COOKIE
|
|
53
|
+
onError('Could not open a login browser (is Chrome installed?). Set SITEVISION_SESSION_COOKIE to a cookie copied from your browser.');
|
|
44
54
|
return;
|
|
45
55
|
}
|
|
46
56
|
cookieRef.current = session;
|
|
57
|
+
setLoginUrl(session.loginUrl);
|
|
47
58
|
setPhase('awaiting');
|
|
48
59
|
}
|
|
49
60
|
})();
|
|
50
61
|
// Release resources if the screen unmounts before completing: close the
|
|
51
62
|
// browser (cookie) and the loopback server (oauth2, frees the port).
|
|
52
63
|
return () => {
|
|
64
|
+
gone = true;
|
|
53
65
|
void cookieRef.current?.close();
|
|
54
66
|
cookieRef.current = null;
|
|
55
67
|
cancelOAuthRef.current?.();
|
|
@@ -69,19 +81,25 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
69
81
|
return;
|
|
70
82
|
setPhase('capturing');
|
|
71
83
|
void (async () => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
try {
|
|
85
|
+
const result = await session.capture();
|
|
86
|
+
if (result.cookie) {
|
|
87
|
+
cookieRef.current = null;
|
|
88
|
+
await session.close();
|
|
89
|
+
onComplete({ sessionCookie: result.cookie });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Keep the browser open so the user can navigate and retry.
|
|
93
|
+
setNote(result.error ?? 'No session cookie found.');
|
|
94
|
+
setPhase('awaiting');
|
|
95
|
+
}
|
|
77
96
|
}
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
setPhase('awaiting');
|
|
97
|
+
catch (error) {
|
|
98
|
+
// The browser window was closed: nothing left to capture from.
|
|
99
|
+
onError(`Could not read the session from the browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
100
|
}
|
|
83
101
|
})();
|
|
84
102
|
}
|
|
85
103
|
});
|
|
86
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: method === 'oauth2' ? 'OAuth2 login' : 'Session cookie login' }) }), method === 'oauth2' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }),
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: method === 'oauth2' ? t('OAuth2 login') : t('Session cookie login') }) }), method === 'oauth2' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Waiting for you to finish login in the browser…')] })] }), authUrl && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: t("If the browser didn't open, visit:") }), _jsx(Text, { children: authUrl })] }))] })) : (_jsxs(Box, { flexDirection: "column", children: [phase === 'starting' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Opening browser…')] })] })), phase === 'awaiting' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: t('A Chrome window is open at:') }), _jsx(Text, { color: "cyan", children: loginUrl }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: t('1. Log in to the site there, single sign-on included.') }), _jsx(Text, { children: t('2. Wait until the site itself has finished loading.') }), _jsx(Text, { children: t('3. Come back here and press Enter.') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Leave the browser window open; it closes once the session is captured.') }) })] })), phase === 'capturing' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Capturing session…')] })] })), note && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: note }) }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Press Esc to cancel.') }) })] }));
|
|
87
105
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { t } from '../utils/i18n.js';
|
|
4
5
|
export function PasswordInput({ label = 'Enter Signing Password', showRememberOption = false, defaultRemember = false, rememberLabel = 'Save to OS keychain: ', onSubmit, onCancel, }) {
|
|
5
6
|
const [password, setPassword] = useState('');
|
|
6
7
|
const [remember, setRemember] = useState(defaultRemember);
|
|
@@ -26,5 +27,5 @@ export function PasswordInput({ label = 'Enter Signing Password', showRememberOp
|
|
|
26
27
|
setPassword(prev => prev + input);
|
|
27
28
|
}
|
|
28
29
|
});
|
|
29
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: label }) }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: '*'.repeat(password.length) }) }), showRememberOption && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: rememberLabel }), _jsxs(Text, { color: remember ? 'green' : 'gray', children: ["[", remember ? 'x' : ' ', "]"] }),
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: label }) }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: '*'.repeat(password.length) }) }), showRememberOption && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: rememberLabel }), _jsxs(Text, { color: remember ? 'green' : 'gray', children: ["[", remember ? 'x' : ' ', "]"] }), _jsxs(Text, { dimColor: true, children: [" ", t('(Tab to toggle)')] })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Press Enter to submit, Esc to cancel') }) })] }));
|
|
30
31
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stands in for inquirer inside the scaffolder: every question goes to svc over
|
|
3
|
+
* the socket in SVC_PROMPT_SOCKET and the reply comes back as the answer. `when`,
|
|
4
|
+
* `filter`, `validate` and function-valued fields run here, where they live.
|
|
5
|
+
* Self-contained on purpose: it is loaded by the tool's node, not by svc.
|
|
6
|
+
*/
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import readline from 'node:readline';
|
|
10
|
+
const CHOICE_TYPES = new Set(['list', 'rawlist', 'expand', 'select', 'search']);
|
|
11
|
+
const evaluate = async (field, ...args) => typeof field === 'function' ? (await field(...args)) : field;
|
|
12
|
+
const text = (value) => typeof value === 'string' ? value : JSON.stringify(value);
|
|
13
|
+
function normalizeChoices(raw) {
|
|
14
|
+
if (!Array.isArray(raw))
|
|
15
|
+
return [];
|
|
16
|
+
const choices = [];
|
|
17
|
+
for (const entry of raw) {
|
|
18
|
+
if (entry === null || typeof entry !== 'object') {
|
|
19
|
+
choices.push({ name: text(entry), value: entry, checked: false });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const choice = entry;
|
|
23
|
+
if (choice['type'] === 'separator' || choice['disabled'])
|
|
24
|
+
continue;
|
|
25
|
+
const value = 'value' in choice ? choice['value'] : choice['name'];
|
|
26
|
+
choices.push({
|
|
27
|
+
name: text(choice['name'] ?? value),
|
|
28
|
+
value,
|
|
29
|
+
checked: Boolean(choice['checked']),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return choices;
|
|
33
|
+
}
|
|
34
|
+
/** Ask svc until the tool's own validation accepts; undefined when skipped. */
|
|
35
|
+
async function askOne(question, answers, exchange) {
|
|
36
|
+
const type = text(question['type'] ?? 'input');
|
|
37
|
+
const choices = normalizeChoices(await evaluate(question['choices'], answers));
|
|
38
|
+
const fallback = await evaluate(question['default'], answers);
|
|
39
|
+
const values = choices.map(choice => choice.value);
|
|
40
|
+
const message = text(await evaluate(question['message'], answers));
|
|
41
|
+
let initial = fallback;
|
|
42
|
+
if (type === 'checkbox') {
|
|
43
|
+
const preset = Array.isArray(fallback) ? fallback : [];
|
|
44
|
+
initial = choices.flatMap((choice, i) => choice.checked || preset.includes(choice.value) ? [i] : []);
|
|
45
|
+
}
|
|
46
|
+
else if (CHOICE_TYPES.has(type)) {
|
|
47
|
+
// Inquirer takes a default as either the value or its index.
|
|
48
|
+
const at = values.indexOf(fallback);
|
|
49
|
+
initial = at === -1 && typeof fallback === 'number' ? fallback : at;
|
|
50
|
+
initial = Math.max(0, initial);
|
|
51
|
+
}
|
|
52
|
+
let error;
|
|
53
|
+
for (;;) {
|
|
54
|
+
// eslint-disable-next-line no-await-in-loop
|
|
55
|
+
const reply = await exchange({
|
|
56
|
+
name: question.name,
|
|
57
|
+
type,
|
|
58
|
+
message,
|
|
59
|
+
choices: choices.map(choice => choice.name),
|
|
60
|
+
default: initial,
|
|
61
|
+
error,
|
|
62
|
+
});
|
|
63
|
+
if (reply.skip)
|
|
64
|
+
return undefined;
|
|
65
|
+
let { value } = reply;
|
|
66
|
+
if (type === 'checkbox')
|
|
67
|
+
value = value.map(i => values[i]);
|
|
68
|
+
else if (CHOICE_TYPES.has(type))
|
|
69
|
+
value = values[value];
|
|
70
|
+
else if (type === 'number')
|
|
71
|
+
value = Number(value);
|
|
72
|
+
// eslint-disable-next-line no-await-in-loop
|
|
73
|
+
value = await evaluate(question['filter'] ?? value, value, answers);
|
|
74
|
+
// eslint-disable-next-line no-await-in-loop
|
|
75
|
+
const valid = await evaluate(question['validate'] ?? true, value, answers);
|
|
76
|
+
if (valid === true)
|
|
77
|
+
return value;
|
|
78
|
+
error = typeof valid === 'string' ? valid : 'Invalid value';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function prompt(questions, initial = {}) {
|
|
82
|
+
const list = Array.isArray(questions)
|
|
83
|
+
? questions
|
|
84
|
+
: typeof questions.name === 'string'
|
|
85
|
+
? [questions]
|
|
86
|
+
: Object.entries(questions).map(([name, question]) => ({ ...question, name }));
|
|
87
|
+
const socket = net.connect(process.env['SVC_PROMPT_SOCKET']);
|
|
88
|
+
const lines = readline.createInterface({ input: socket });
|
|
89
|
+
const replies = lines[Symbol.asyncIterator]();
|
|
90
|
+
const exchange = async (message) => {
|
|
91
|
+
socket.write(JSON.stringify(message) + '\n');
|
|
92
|
+
const next = await replies.next();
|
|
93
|
+
if (next.done)
|
|
94
|
+
throw new Error('svc closed the prompt');
|
|
95
|
+
return JSON.parse(next.value);
|
|
96
|
+
};
|
|
97
|
+
// ponytail: dotted names ("a.b") stay flat keys; nest them if a question ever uses one.
|
|
98
|
+
const answers = { ...initial };
|
|
99
|
+
try {
|
|
100
|
+
for (const question of list) {
|
|
101
|
+
const wanted = question['when'] === undefined ||
|
|
102
|
+
// eslint-disable-next-line no-await-in-loop
|
|
103
|
+
Boolean(await evaluate(question['when'], answers));
|
|
104
|
+
const answered = answers[question.name] !== undefined && !question['askAnswered'];
|
|
105
|
+
if (!wanted || answered)
|
|
106
|
+
continue;
|
|
107
|
+
// eslint-disable-next-line no-await-in-loop
|
|
108
|
+
const value = await askOne(question, answers, exchange);
|
|
109
|
+
if (value !== undefined)
|
|
110
|
+
answers[question.name] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
socket.end();
|
|
115
|
+
}
|
|
116
|
+
return answers;
|
|
117
|
+
}
|
|
118
|
+
const inquirer = { prompt };
|
|
119
|
+
export default inquirer;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Loaded through NODE_OPTIONS into every process the scaffolder starts: hands
|
|
2
|
+
// sitevision-scripts svc's prompt bridge in place of inquirer.
|
|
3
|
+
import module from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const stub = new URL(`inquirer-stub${path.extname(import.meta.url)}`, import.meta.url).href;
|
|
6
|
+
// Missing before Node 22.15; svc then finds the run unmanaged and falls back.
|
|
7
|
+
module.registerHooks?.({
|
|
8
|
+
resolve(specifier, context, nextResolve) {
|
|
9
|
+
if (specifier === 'inquirer' &&
|
|
10
|
+
context.parentURL?.includes('sitevision-scripts')) {
|
|
11
|
+
return { url: stub, shortCircuit: true };
|
|
12
|
+
}
|
|
13
|
+
return nextResolve(specifier, context);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { ACCENT } from './Frame.js';
|
|
6
|
+
import { fuzzyMatch } from './actions.js';
|
|
7
|
+
import { t } from '../utils/i18n.js';
|
|
8
|
+
export function AddonPicker({ domain, appType, initialQuery, load, onSelect, onClose, height, }) {
|
|
9
|
+
const [query, setQuery] = useState(initialQuery);
|
|
10
|
+
const [index, setIndex] = useState(0);
|
|
11
|
+
const [addons, setAddons] = useState(null);
|
|
12
|
+
const [error, setError] = useState('');
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
void load().then(result => {
|
|
15
|
+
setAddons(result.addons ?? []);
|
|
16
|
+
setError(result.error ?? '');
|
|
17
|
+
});
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
19
|
+
}, []);
|
|
20
|
+
const matches = (addons ?? [])
|
|
21
|
+
.filter(a => fuzzyMatch(query, a.name))
|
|
22
|
+
.toSorted((a, b) => Number(b.appType === appType) - Number(a.appType === appType) ||
|
|
23
|
+
a.name.localeCompare(b.name));
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (key.escape) {
|
|
26
|
+
onClose();
|
|
27
|
+
}
|
|
28
|
+
else if (key.return) {
|
|
29
|
+
const pick = matches[index];
|
|
30
|
+
if (pick)
|
|
31
|
+
onSelect(pick.name);
|
|
32
|
+
}
|
|
33
|
+
else if (key.upArrow) {
|
|
34
|
+
setIndex(i => (i > 0 ? i - 1 : Math.max(0, matches.length - 1)));
|
|
35
|
+
}
|
|
36
|
+
else if (key.downArrow) {
|
|
37
|
+
setIndex(i => (i < matches.length - 1 ? i + 1 : 0));
|
|
38
|
+
}
|
|
39
|
+
else if (key.backspace || key.delete) {
|
|
40
|
+
setQuery(q => q.slice(0, -1));
|
|
41
|
+
setIndex(0);
|
|
42
|
+
}
|
|
43
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
44
|
+
setQuery(q => q + input);
|
|
45
|
+
setIndex(0);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const visible = Math.max(3, height - 6);
|
|
49
|
+
const start = Math.max(0, Math.min(index - visible + 1, matches.length - visible));
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: t('Addon Repository') }), _jsxs(Text, { dimColor: true, children: [" ", domain] })] }), _jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), query, _jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { dimColor: true, children: [' ', addons ? t('{n} of {m}', { n: matches.length, m: addons.length }) : ''] })] }), !addons && !error && (_jsxs(Text, { color: ACCENT, children: [_jsx(Spinner, { type: "dots" }), " ", _jsx(Text, { dimColor: true, children: t('fetching addons') })] })), error && _jsx(Text, { color: "red", children: error }), matches.slice(start, start + visible).map((a, i) => {
|
|
51
|
+
const selected = start + i === index;
|
|
52
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, marginRight: 1, children: _jsxs(Text, { backgroundColor: selected ? ACCENT : undefined, color: selected ? 'black' : undefined, wrap: "truncate", children: [' ', a.name] }) }), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, children: a.appType ?? a.type }) })] }, a.id));
|
|
53
|
+
}), _jsx(Text, { dimColor: true, children: t('↑↓ move · Enter select · Esc cancel') })] }));
|
|
54
|
+
}
|