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
|
@@ -1,42 +1,44 @@
|
|
|
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
|
+
// rl.question drops lines that arrive before it is called, which is every line
|
|
7
|
+
// but the first when stdin is piped. The iterator buffers them.
|
|
8
|
+
function lineReader(rl) {
|
|
9
|
+
const lines = rl[Symbol.asyncIterator]();
|
|
10
|
+
return async (prompt) => {
|
|
11
|
+
process.stdout.write(prompt);
|
|
12
|
+
const { value, done } = await lines.next();
|
|
13
|
+
return done ? '' : String(value);
|
|
14
|
+
};
|
|
10
15
|
}
|
|
11
16
|
export const setupSigningCommand = {
|
|
12
17
|
name: 'setup-signing',
|
|
13
18
|
description: 'Configure signing credentials for developer.sitevision.se',
|
|
14
19
|
requiresProject: true,
|
|
15
|
-
async execute({ project }) {
|
|
20
|
+
async execute({ project, flags }) {
|
|
21
|
+
const global = Boolean(flags['global']);
|
|
16
22
|
const rl = readline.createInterface({
|
|
17
23
|
input: process.stdin,
|
|
18
24
|
output: process.stdout,
|
|
19
25
|
});
|
|
26
|
+
const question = lineReader(rl);
|
|
20
27
|
console.log('\n\x1b[36m\x1b[1mSetup Signing Credentials\x1b[0m\n');
|
|
21
28
|
console.log('Configure credentials for signing apps on developer.sitevision.se');
|
|
22
29
|
console.log('(Password will be prompted when running signing commands)\n');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
path.join(project.root, '.dev_properties.json'),
|
|
26
|
-
path.join(project.root, '.dev-properties.json'),
|
|
27
|
-
];
|
|
28
|
-
let devPropertiesPath = devPropertiesPaths[0];
|
|
30
|
+
const devPropertiesPath = findDevPropertiesPath(project.root) ??
|
|
31
|
+
path.join(project.root, '.dev_properties.json');
|
|
29
32
|
let existingProperties = {};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
break;
|
|
33
|
+
if (global) {
|
|
34
|
+
existingProperties = { ...getGlobalSigning() };
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
try {
|
|
38
|
+
existingProperties = JSON.parse(fs.readFileSync(devPropertiesPath, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Missing or invalid file, start fresh
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
try {
|
|
@@ -45,13 +47,14 @@ export const setupSigningCommand = {
|
|
|
45
47
|
const usernamePrompt = defaultUsername
|
|
46
48
|
? `Signing username [${defaultUsername}]: `
|
|
47
49
|
: 'Signing username: ';
|
|
48
|
-
let signingUsername = await question(
|
|
50
|
+
let signingUsername = await question(usernamePrompt);
|
|
49
51
|
if (!signingUsername && defaultUsername) {
|
|
50
52
|
signingUsername = defaultUsername;
|
|
51
53
|
}
|
|
52
54
|
if (!signingUsername) {
|
|
53
55
|
console.log('\x1b[31mError: Signing username is required\x1b[0m');
|
|
54
56
|
rl.close();
|
|
57
|
+
process.exitCode = 1;
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
// Get certificate name (optional)
|
|
@@ -59,15 +62,26 @@ export const setupSigningCommand = {
|
|
|
59
62
|
const certPrompt = defaultCertName
|
|
60
63
|
? `Certificate name (blank for default) [${defaultCertName}]: `
|
|
61
64
|
: 'Certificate name (blank for default): ';
|
|
62
|
-
let certificateName = await question(
|
|
65
|
+
let certificateName = await question(certPrompt);
|
|
63
66
|
if (!certificateName && defaultCertName) {
|
|
64
67
|
certificateName = defaultCertName;
|
|
65
68
|
}
|
|
66
69
|
rl.close();
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
if (global) {
|
|
71
|
+
if (configProblem()) {
|
|
72
|
+
console.log(`\x1b[31mError: ${settingsFile()} does not parse; fix it first\x1b[0m`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setGlobalSigning({
|
|
77
|
+
signingUsername,
|
|
78
|
+
...(certificateName && { certificateName }),
|
|
79
|
+
});
|
|
80
|
+
console.log(`\n\x1b[32mSigning credentials saved to ${settingsFile()}\x1b[0m\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
69
83
|
const updatedProperties = {
|
|
70
|
-
...
|
|
84
|
+
...existingProperties,
|
|
71
85
|
signingUsername,
|
|
72
86
|
...(certificateName && { certificateName }),
|
|
73
87
|
};
|
|
@@ -77,6 +91,7 @@ export const setupSigningCommand = {
|
|
|
77
91
|
catch (error) {
|
|
78
92
|
rl.close();
|
|
79
93
|
console.error('\x1b[31mError setting up signing credentials:\x1b[0m', error);
|
|
94
|
+
process.exitCode = 1;
|
|
80
95
|
}
|
|
81
96
|
},
|
|
82
97
|
};
|
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
|
@@ -5,14 +5,6 @@ export const watchCommand = {
|
|
|
5
5
|
name: 'watch',
|
|
6
6
|
description: 'Watch and rebuild (optionally signing) without deploying',
|
|
7
7
|
requiresProject: true,
|
|
8
|
-
flags: {
|
|
9
|
-
signed: {
|
|
10
|
-
type: 'boolean',
|
|
11
|
-
description: 'Sign after each build',
|
|
12
|
-
alias: 's',
|
|
13
|
-
default: false,
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
8
|
async execute({ project, flags }) {
|
|
17
9
|
let signingCredentials;
|
|
18
10
|
if (flags['signed']) {
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
5
|
import { t } from '../utils/i18n.js';
|
|
6
|
-
import { beginOAuth2Login, openBrowser } from '../utils/oauth2-auth.js';
|
|
6
|
+
import { beginOAuth2Login, oauth2ConfigProblem, openBrowser, } from '../utils/oauth2-auth.js';
|
|
7
7
|
import { beginCookieLogin, } from '../utils/session-cookie-auth.js';
|
|
8
8
|
/**
|
|
9
9
|
* Ink-native interactive login for OAuth2 and session-cookie auth. Drives the
|
|
@@ -19,11 +19,13 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
19
19
|
const cookieRef = React.useRef(null);
|
|
20
20
|
const cancelOAuthRef = React.useRef(null);
|
|
21
21
|
React.useEffect(() => {
|
|
22
|
+
// Set by the cleanup: the screen went away while a login was starting.
|
|
23
|
+
let gone = false;
|
|
22
24
|
void (async () => {
|
|
23
25
|
if (method === 'oauth2') {
|
|
24
26
|
const session = beginOAuth2Login(devProperties);
|
|
25
27
|
if (!session) {
|
|
26
|
-
onError('
|
|
28
|
+
onError(oauth2ConfigProblem(devProperties) ?? 'Could not start the login.');
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
cancelOAuthRef.current = session.cancel;
|
|
@@ -32,6 +34,8 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
32
34
|
setPhase('awaiting');
|
|
33
35
|
const { token, error } = await session.complete();
|
|
34
36
|
cancelOAuthRef.current = null;
|
|
37
|
+
if (gone)
|
|
38
|
+
return;
|
|
35
39
|
if (token) {
|
|
36
40
|
onComplete({ accessToken: token });
|
|
37
41
|
}
|
|
@@ -41,8 +45,12 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
41
45
|
}
|
|
42
46
|
else {
|
|
43
47
|
const session = await beginCookieLogin(devProperties);
|
|
48
|
+
if (gone) {
|
|
49
|
+
void session?.close();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
44
52
|
if (!session) {
|
|
45
|
-
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.');
|
|
46
54
|
return;
|
|
47
55
|
}
|
|
48
56
|
cookieRef.current = session;
|
|
@@ -53,6 +61,7 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
53
61
|
// Release resources if the screen unmounts before completing: close the
|
|
54
62
|
// browser (cookie) and the loopback server (oauth2, frees the port).
|
|
55
63
|
return () => {
|
|
64
|
+
gone = true;
|
|
56
65
|
void cookieRef.current?.close();
|
|
57
66
|
cookieRef.current = null;
|
|
58
67
|
cancelOAuthRef.current?.();
|
|
@@ -72,16 +81,22 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
|
|
|
72
81
|
return;
|
|
73
82
|
setPhase('capturing');
|
|
74
83
|
void (async () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
96
|
}
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
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)}`);
|
|
85
100
|
}
|
|
86
101
|
})();
|
|
87
102
|
}
|
package/dist/shell/ConfigForm.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { findDevPropertiesPath, getPackageJsonSyncChanges, hasPackageJson, IMPLICIT_VALUES, normalizeDomain, readAncestorDevProperties, readInheritedDevProperties, readWorkspaceDevProperties, updatePackageJson, writeDevProperties, writeManifestField, } from '../utils/project-detection.js';
|
|
4
|
+
import { addonNameDrift, findDevPropertiesPath, getPackageJsonSyncChanges, hasPackageJson, IMPLICIT_VALUES, normalizeDomain, readAncestorDevProperties, readInheritedDevProperties, readWorkspaceDevProperties, updatePackageJson, writeDevProperties, writeManifestField, } from '../utils/project-detection.js';
|
|
5
5
|
import { setDeployPassword, deleteDeployPassword, getOAuth2ClientSecret, setOAuth2ClientSecret, deleteOAuth2ClientSecret, getSigningPassword, setSigningPassword, deleteSigningPassword, } from '../utils/keychain.js';
|
|
6
6
|
import { DEFAULT_SCOPES, discoverOAuth2Config } from '../utils/oauth2-auth.js';
|
|
7
7
|
import { ACCENT } from './Frame.js';
|
|
8
8
|
import { baseEnvironment, withEnvironmentOverride, } from '../utils/environments.js';
|
|
9
9
|
import { t } from '../utils/i18n.js';
|
|
10
|
+
import { getGlobalSigning, getSettings } from '../utils/config.js';
|
|
10
11
|
const METHODS = ['basic', 'oauth2', 'cookie'];
|
|
11
12
|
const FIELDS = [
|
|
12
13
|
{
|
|
@@ -23,7 +24,7 @@ const FIELDS = [
|
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
26
|
key: 'addonName',
|
|
26
|
-
help: "Name of the addon (custom module) in the site's Addon Repository that this app is uploaded into. Ctrl+O lists the existing ones.",
|
|
27
|
+
help: "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.",
|
|
27
28
|
label: 'Addon name',
|
|
28
29
|
required: true,
|
|
29
30
|
hint: '^O pick from repo',
|
|
@@ -168,6 +169,7 @@ const MANIFEST_FIELDS = [
|
|
|
168
169
|
},
|
|
169
170
|
];
|
|
170
171
|
/** Rows for manifest.json: one per text field, one per language of a localized one. */
|
|
172
|
+
const LOCALIZABLE = new Set(['name', 'description']);
|
|
171
173
|
export function manifestFields(manifest) {
|
|
172
174
|
if (!manifest)
|
|
173
175
|
return [];
|
|
@@ -175,15 +177,20 @@ export function manifestFields(manifest) {
|
|
|
175
177
|
return MANIFEST_FIELDS.flatMap(f => {
|
|
176
178
|
const value = raw[f.key];
|
|
177
179
|
const base = { ...f, section: 'MANIFEST', manifestKey: f.key };
|
|
180
|
+
const localized = (lang) => ({
|
|
181
|
+
...base,
|
|
182
|
+
key: `manifest.${f.key}.${lang}`,
|
|
183
|
+
lang,
|
|
184
|
+
required: f.required && lang === 'en',
|
|
185
|
+
});
|
|
178
186
|
if (value && typeof value === 'object') {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
required: f.required && lang === 'en',
|
|
184
|
-
}));
|
|
187
|
+
const langs = Object.keys(value);
|
|
188
|
+
if (!langs.includes('sv'))
|
|
189
|
+
langs.push('sv');
|
|
190
|
+
return langs.map(lang => localized(lang));
|
|
185
191
|
}
|
|
186
|
-
|
|
192
|
+
const plain = { ...base, key: `manifest.${f.key}` };
|
|
193
|
+
return LOCALIZABLE.has(f.key) ? [plain, localized('sv')] : [plain];
|
|
187
194
|
});
|
|
188
195
|
}
|
|
189
196
|
const labelOf = (f) => t(f.label) + (f.lang ? ` (${f.lang})` : '');
|
|
@@ -227,9 +234,11 @@ function fromProject(project) {
|
|
|
227
234
|
signingPassword: '',
|
|
228
235
|
...Object.fromEntries(manifestFields(project.manifest).map(f => {
|
|
229
236
|
const value = project.manifest[f.manifestKey];
|
|
230
|
-
const text = f.lang
|
|
237
|
+
const text = f.lang && value && typeof value === 'object'
|
|
231
238
|
? value[f.lang]
|
|
232
|
-
:
|
|
239
|
+
: f.lang
|
|
240
|
+
? undefined
|
|
241
|
+
: value;
|
|
233
242
|
return [f.key, typeof text === 'string' ? text : ''];
|
|
234
243
|
})),
|
|
235
244
|
};
|
|
@@ -252,8 +261,9 @@ const sameValue = (key, a, b) => JSON.stringify(a === '' || a === undefined ? IM
|
|
|
252
261
|
JSON.stringify(b === '' || b === undefined ? IMPLICIT_VALUES[key] : b);
|
|
253
262
|
/**
|
|
254
263
|
* App mode writes the app's complete file. Workspace mode never creates an
|
|
255
|
-
* app's .dev_properties.json: changes go to the root file, and
|
|
256
|
-
* to the app's package.json
|
|
264
|
+
* app's .dev_properties.json: changes go to the root file, and addon names (the
|
|
265
|
+
* base one and each environment's) to the app's package.json, since the root
|
|
266
|
+
* file is shared by every app. An app that already has its own file keeps it.
|
|
257
267
|
*/
|
|
258
268
|
function writeConfigFile(project, file) {
|
|
259
269
|
const { workspaceRoot } = project;
|
|
@@ -276,6 +286,40 @@ function writeConfigFile(project, file) {
|
|
|
276
286
|
packageJson['addonName'] = after[key] || undefined;
|
|
277
287
|
});
|
|
278
288
|
}
|
|
289
|
+
else if (key === 'environments') {
|
|
290
|
+
// `after` is this app's merged view. Only the environment being edited
|
|
291
|
+
// goes to the shared file, or the app's own overrides would reach
|
|
292
|
+
// every other app.
|
|
293
|
+
const environments = Object.entries((after[key] ?? {})).filter(([name]) => !project.environment || name === project.environment);
|
|
294
|
+
root[key] = {
|
|
295
|
+
...root[key],
|
|
296
|
+
...Object.fromEntries(environments.map(([name, { addonName: _addonName, ...shared }]) => [
|
|
297
|
+
name,
|
|
298
|
+
shared,
|
|
299
|
+
])),
|
|
300
|
+
};
|
|
301
|
+
updatePackageJson(project.root, packageJson => {
|
|
302
|
+
const svc = { ...packageJson['svc'] };
|
|
303
|
+
const own = new Map(Object.entries((svc['environments'] ?? {})));
|
|
304
|
+
for (const [name, { addonName }] of environments) {
|
|
305
|
+
const override = {
|
|
306
|
+
...own.get(name),
|
|
307
|
+
addonName: addonName || undefined,
|
|
308
|
+
};
|
|
309
|
+
if (Object.values(override).every(value => value === undefined)) {
|
|
310
|
+
own.delete(name);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
own.set(name, override);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
svc['environments'] =
|
|
317
|
+
own.size > 0 ? Object.fromEntries(own) : undefined;
|
|
318
|
+
packageJson['svc'] = Object.values(svc).some(value => value !== undefined)
|
|
319
|
+
? svc
|
|
320
|
+
: undefined;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
279
323
|
else {
|
|
280
324
|
root[key] = after[key];
|
|
281
325
|
}
|
|
@@ -285,22 +329,21 @@ function writeConfigFile(project, file) {
|
|
|
285
329
|
/** Apply the form to disk and the keychain. Exported for the test. */
|
|
286
330
|
export function saveConfig(project, values, edited) {
|
|
287
331
|
const method = values['authMethod'];
|
|
332
|
+
// Start from what is there: keys the form does not show, and the settings of
|
|
333
|
+
// the auth methods not in use, survive a save.
|
|
288
334
|
const next = {
|
|
335
|
+
...project.devProperties,
|
|
289
336
|
domain: values['domain'],
|
|
290
337
|
siteName: values['siteName'],
|
|
291
338
|
addonName: values['addonName'],
|
|
292
339
|
username: values['username'],
|
|
293
340
|
authMethod: method,
|
|
294
341
|
useHTTPForDevDeploy: values['useHTTPForDevDeploy'] === 'yes',
|
|
342
|
+
baseEnvironment: values['baseEnvironment']?.trim().toLowerCase() || undefined,
|
|
343
|
+
production: values['production'] === 'yes' ? true : undefined,
|
|
344
|
+
signingUsername: values['signingUsername'] || undefined,
|
|
345
|
+
certificateName: values['certificateName'] || undefined,
|
|
295
346
|
};
|
|
296
|
-
if (values['baseEnvironment'])
|
|
297
|
-
next.baseEnvironment = values['baseEnvironment'].trim().toLowerCase();
|
|
298
|
-
if (values['production'] === 'yes')
|
|
299
|
-
next.production = true;
|
|
300
|
-
if (values['signingUsername'])
|
|
301
|
-
next.signingUsername = values['signingUsername'];
|
|
302
|
-
if (values['certificateName'])
|
|
303
|
-
next.certificateName = values['certificateName'];
|
|
304
347
|
if (method === 'oauth2') {
|
|
305
348
|
const scopes = values['scopes'].split(/[\s,]+/).filter(Boolean);
|
|
306
349
|
next.oauth2 = {
|
|
@@ -313,8 +356,8 @@ export function saveConfig(project, values, edited) {
|
|
|
313
356
|
}),
|
|
314
357
|
};
|
|
315
358
|
}
|
|
316
|
-
else if (method === 'cookie'
|
|
317
|
-
next.sessionLoginUrl = values['sessionLoginUrl'];
|
|
359
|
+
else if (method === 'cookie') {
|
|
360
|
+
next.sessionLoginUrl = values['sessionLoginUrl'] || undefined;
|
|
318
361
|
}
|
|
319
362
|
const env = project.environment;
|
|
320
363
|
if (env && env !== baseEnvironment(project.base) && project.base) {
|
|
@@ -368,6 +411,11 @@ export function visibleFields(method, workspace = false, envMode = false) {
|
|
|
368
411
|
}
|
|
369
412
|
export function ConfigForm({ project, active, width, height, pickAddon, onSaved, onEditingChange, }) {
|
|
370
413
|
const [values, setValues] = useState(() => fromProject(project));
|
|
414
|
+
// What the form holds when an async lookup lands, not when it started.
|
|
415
|
+
const latest = useRef({ values, project });
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
latest.current = { values, project };
|
|
418
|
+
});
|
|
371
419
|
const [cursor, setCursor] = useState(0);
|
|
372
420
|
const [editing, setEditing] = useState(false);
|
|
373
421
|
const [draft, setDraft] = useState('');
|
|
@@ -394,6 +442,7 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
394
442
|
const current = fields[Math.min(cursor, fields.length - 1)];
|
|
395
443
|
const inherited = readInheritedDevProperties(project.root);
|
|
396
444
|
const ancestors = readAncestorDevProperties(project.root);
|
|
445
|
+
const globalSigning = getGlobalSigning();
|
|
397
446
|
const changes = getPackageJsonSyncChanges(project.root);
|
|
398
447
|
const packageJsonExists = hasPackageJson(project.root);
|
|
399
448
|
// Write one field to disk (and the keychain for secrets) right away.
|
|
@@ -438,14 +487,15 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
438
487
|
if (cancelled)
|
|
439
488
|
return;
|
|
440
489
|
if (found) {
|
|
490
|
+
const now = latest.current.values;
|
|
441
491
|
const next = {
|
|
442
|
-
...
|
|
443
|
-
authorizationEndpoint:
|
|
444
|
-
tokenEndpoint:
|
|
492
|
+
...now,
|
|
493
|
+
authorizationEndpoint: now['authorizationEndpoint'] || found.authorizationEndpoint,
|
|
494
|
+
tokenEndpoint: now['tokenEndpoint'] || found.tokenEndpoint,
|
|
445
495
|
};
|
|
446
496
|
setValues(next);
|
|
447
497
|
try {
|
|
448
|
-
saveConfig(project, next, new Set());
|
|
498
|
+
saveConfig(latest.current.project, next, new Set());
|
|
449
499
|
}
|
|
450
500
|
catch (error) {
|
|
451
501
|
setNote(t('Not saved: {error}', { error: errorText(error) }));
|
|
@@ -536,10 +586,16 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
536
586
|
setEditing(true);
|
|
537
587
|
}
|
|
538
588
|
}, { isActive: active });
|
|
589
|
+
// An environment's addon name differs from the app's name on purpose.
|
|
590
|
+
const drift = envMode || !getSettings().addonNameDriftWarning
|
|
591
|
+
? undefined
|
|
592
|
+
: addonNameDrift(values['addonName'], project.manifest);
|
|
539
593
|
const source = (f) => {
|
|
540
594
|
const required = f.required === true || f.required === method;
|
|
541
595
|
if (required && !(values[f.key] ?? ''))
|
|
542
596
|
return { text: t('✗ required'), color: 'red' };
|
|
597
|
+
if (f.key === 'addonName' && drift)
|
|
598
|
+
return { text: t('≠ manifest'), color: 'yellow' };
|
|
543
599
|
if (f.kind === 'secret') {
|
|
544
600
|
return { text: storedSecret(project, f.key) ? t('keychain') : '—' };
|
|
545
601
|
}
|
|
@@ -579,6 +635,8 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
579
635
|
: 'package.json',
|
|
580
636
|
};
|
|
581
637
|
}
|
|
638
|
+
if (value && globalSigning[f.key] === value)
|
|
639
|
+
return { text: t('global') };
|
|
582
640
|
return { text: (values[f.key] ?? '') ? t('local') : '' };
|
|
583
641
|
};
|
|
584
642
|
// label column (24) + source column + paddings; never below 20.
|
|
@@ -638,5 +696,8 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
|
|
|
638
696
|
? t('in sync')
|
|
639
697
|
: changes.length === 1
|
|
640
698
|
? t('1 diff · y to apply')
|
|
641
|
-
: t('{n} diffs · y to apply', { n: changes.length }) })] }),
|
|
699
|
+
: t('{n} diffs · y to apply', { n: changes.length }) })] }), drift && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: "yellow", children: "\u2260 " }), t('addon name {addon} · manifest name {names}', {
|
|
700
|
+
addon: values['addonName'] ?? '',
|
|
701
|
+
names: drift.join(' / '),
|
|
702
|
+
})] })), changes.map(c => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: c.from === undefined ? 'green' : 'yellow', children: c.from === undefined ? '+ ' : '~ ' }), c.key, ": ", c.from !== undefined && _jsxs(Text, { dimColor: true, children: [c.from, " \u2192 "] }), c.to] }, c.key)))] }), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderDimColor: true, borderLeft: false, borderRight: false, borderBottom: false, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { bold: true, color: ACCENT, children: labelOf(current) }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t(current.help)] })] }) }), _jsx(Text, { color: "yellow", children: note })] }));
|
|
642
703
|
}
|
package/dist/shell/Frame.js
CHANGED
|
@@ -11,7 +11,10 @@ export function navWidth(columns) {
|
|
|
11
11
|
return Math.min(48, Math.max(32, Math.floor(columns / 4)));
|
|
12
12
|
}
|
|
13
13
|
export function TopBar({ context, domain, auth, version, environment, }) {
|
|
14
|
-
|
|
14
|
+
// Narrow terminals squeeze the domain first, then the context, then the
|
|
15
|
+
// login. `svc` and the environment badge never give way: the badge is the
|
|
16
|
+
// one thing that says a deploy goes to production.
|
|
17
|
+
return (_jsxs(Box, { paddingX: 1, height: 1, overflow: "hidden", children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { bold: true, color: ACCENT, children: "svc" }), environment && (_jsxs(Text, { children: [' ', _jsx(Text, { backgroundColor: environment.color, color: "black", bold: true, children: ` ${environment.name.toUpperCase()} ` })] }))] }), _jsx(Box, { flexShrink: 2, flexGrow: 1, marginRight: 2, minWidth: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', context] }) }), domain && (_jsx(Box, { flexShrink: 100, minWidth: 0, marginRight: 1, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: domain }) })), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: auth.ready ? 'green' : 'yellow', children: auth.ready ? '●' : '○' }), _jsxs(Text, { children: [" ", auth.label] })] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: [" v", version] }) })] }));
|
|
15
18
|
}
|
|
16
19
|
const TYPE_GLYPH = {
|
|
17
20
|
web: 'web',
|
package/dist/shell/Help.js
CHANGED
|
@@ -66,10 +66,11 @@ export function HelpPanel({ here, where, height, onClose, }) {
|
|
|
66
66
|
const inner = useRef(null);
|
|
67
67
|
const [top, setTop] = useState(0);
|
|
68
68
|
const [total, setTotal] = useState(0);
|
|
69
|
+
// The text depends on the pane and the language, both fixed while this is open.
|
|
69
70
|
useEffect(() => {
|
|
70
71
|
if (inner.current)
|
|
71
72
|
setTotal(measureElement(inner.current).height);
|
|
72
|
-
});
|
|
73
|
+
}, [here, where, height]);
|
|
73
74
|
const visible = Math.max(1, height - 1);
|
|
74
75
|
const max = Math.max(0, total - visible);
|
|
75
76
|
useInput((input, key) => {
|