sitevision-cli 1.0.0-beta.3 → 1.0.0-beta.30
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 +49 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +59 -8
- package/dist/cli.js +125 -40
- package/dist/commands/build.js +1 -1
- package/dist/commands/deploy.d.ts +2 -2
- package/dist/commands/deploy.js +137 -28
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +70 -371
- package/dist/commands/watch.js +5 -23
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.d.ts +21 -0
- package/dist/components/AuthLoginScreen.js +90 -0
- package/dist/components/DevPropertiesForm.d.ts +2 -1
- package/dist/components/DevPropertiesForm.js +198 -33
- package/dist/components/MainMenu.js +5 -0
- package/dist/components/PasswordInput.js +2 -1
- package/dist/components/SetupFlow.d.ts +2 -1
- package/dist/components/SetupFlow.js +99 -10
- package/dist/shell/AddonPicker.d.ts +14 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.d.ts +10 -0
- package/dist/shell/Changelog.js +44 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.d.ts +36 -0
- package/dist/shell/ConfigForm.js +558 -0
- package/dist/shell/Frame.d.ts +59 -0
- package/dist/shell/Frame.js +135 -0
- package/dist/shell/Help.d.ts +6 -0
- package/dist/shell/Help.js +31 -0
- package/dist/shell/Popover.d.ts +12 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +10 -0
- package/dist/shell/Shell.js +620 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +90 -0
- package/dist/shell/actions.d.ts +48 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +37 -3
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.js +14 -0
- package/dist/utils/environments.d.ts +20 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.d.ts +12 -0
- package/dist/utils/i18n.js +300 -0
- package/dist/utils/jsonc.d.ts +19 -0
- package/dist/utils/jsonc.js +74 -0
- package/dist/utils/keychain.d.ts +9 -0
- package/dist/utils/keychain.js +54 -0
- package/dist/utils/oauth2-auth.d.ts +64 -0
- package/dist/utils/oauth2-auth.js +242 -0
- package/dist/utils/password-prompt.d.ts +5 -0
- package/dist/utils/password-prompt.js +28 -0
- package/dist/utils/process-runner.d.ts +13 -4
- package/dist/utils/process-runner.js +63 -37
- package/dist/utils/project-detection.d.ts +90 -4
- package/dist/utils/project-detection.js +389 -63
- package/dist/utils/session-cookie-auth.d.ts +35 -0
- package/dist/utils/session-cookie-auth.js +99 -0
- package/dist/utils/sitevision-api.d.ts +72 -5
- package/dist/utils/sitevision-api.js +220 -38
- package/dist/utils/sitevision-scripts-runner.d.ts +2 -1
- package/dist/utils/sitevision-scripts-runner.js +18 -7
- package/dist/utils/tasks.d.ts +50 -0
- package/dist/utils/tasks.js +439 -0
- package/dist/utils/workspace.d.ts +17 -0
- package/dist/utils/workspace.js +67 -0
- package/package.json +5 -2
- package/readme.md +103 -121
|
@@ -3,8 +3,9 @@ interface Props {
|
|
|
3
3
|
projectRoot: string;
|
|
4
4
|
initialProperties?: DevProperties;
|
|
5
5
|
packageJson: PackageJson;
|
|
6
|
+
authOnly?: boolean;
|
|
6
7
|
onComplete: () => void;
|
|
7
8
|
onCancel: () => void;
|
|
8
9
|
}
|
|
9
|
-
export declare function DevPropertiesForm({ projectRoot, initialProperties, packageJson, onComplete, onCancel, }: Props): import("react").JSX.Element;
|
|
10
|
+
export declare function DevPropertiesForm({ projectRoot, initialProperties, packageJson, authOnly, onComplete, onCancel, }: Props): import("react").JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -1,67 +1,232 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { TextInput } from './TextInput.js';
|
|
5
5
|
import { writeDevProperties } from '../utils/project-detection.js';
|
|
6
|
-
import { setDeployPassword, deleteDeployPassword } from '../utils/keychain.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { setDeployPassword, deleteDeployPassword, setOAuth2ClientSecret, } from '../utils/keychain.js';
|
|
7
|
+
import { DEFAULT_REDIRECT_PORT, discoverOAuth2Config, } from '../utils/oauth2-auth.js';
|
|
8
|
+
const LABELS = {
|
|
9
|
+
domain: 'Domain',
|
|
10
|
+
siteName: 'Site Name',
|
|
11
|
+
addonName: 'Addon Name',
|
|
12
|
+
username: 'Username',
|
|
13
|
+
authMethod: 'Auth Method',
|
|
14
|
+
password: 'Password',
|
|
15
|
+
oauthClientId: 'Client ID',
|
|
16
|
+
oauthAuthEndpoint: 'Authorization Endpoint',
|
|
17
|
+
oauthTokenEndpoint: 'Token Endpoint',
|
|
18
|
+
oauthScopes: 'Scopes',
|
|
19
|
+
oauthClientSecret: 'Client Secret',
|
|
20
|
+
sessionLoginUrl: 'Login URL',
|
|
21
|
+
useHTTP: 'Use HTTP',
|
|
22
|
+
};
|
|
23
|
+
const AUTH_METHODS = [
|
|
24
|
+
{ value: 'basic', label: 'Basic auth (username + password)' },
|
|
25
|
+
{ value: 'oauth2', label: 'OAuth2 bearer token (PKCE)' },
|
|
26
|
+
{ value: 'cookie', label: 'Session cookie (SAML / SSO login)' },
|
|
14
27
|
];
|
|
15
|
-
|
|
28
|
+
function parseScopes(raw) {
|
|
29
|
+
const scopes = raw
|
|
30
|
+
.split(/[\s,]+/)
|
|
31
|
+
.map(s => s.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return scopes.length > 0 ? scopes : undefined;
|
|
34
|
+
}
|
|
35
|
+
export function DevPropertiesForm({ projectRoot, initialProperties, packageJson, authOnly, onComplete, onCancel, }) {
|
|
16
36
|
const [stepIndex, setStepIndex] = useState(0);
|
|
17
37
|
const [properties, setProperties] = useState(() => {
|
|
18
38
|
const defaults = {
|
|
19
39
|
domain: packageJson.developmentDomain || '',
|
|
20
40
|
addonName: packageJson.addonName || '',
|
|
21
41
|
siteName: packageJson.siteName || '',
|
|
42
|
+
authMethod: 'basic',
|
|
22
43
|
};
|
|
23
44
|
return { ...defaults, ...initialProperties };
|
|
24
45
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
const [oauth, setOauth] = useState(() => ({
|
|
47
|
+
authorizationEndpoint: initialProperties?.oauth2?.authorizationEndpoint ?? '',
|
|
48
|
+
tokenEndpoint: initialProperties?.oauth2?.tokenEndpoint ?? '',
|
|
49
|
+
clientId: initialProperties?.oauth2?.clientId ?? '',
|
|
50
|
+
scopes: initialProperties?.oauth2?.scopes?.join(' ') ?? '',
|
|
51
|
+
clientSecret: '',
|
|
52
|
+
}));
|
|
53
|
+
const [discoveryNote, setDiscoveryNote] = useState('');
|
|
54
|
+
const method = properties.authMethod ?? 'basic';
|
|
55
|
+
const isOAuth = method === 'oauth2';
|
|
56
|
+
// When OAuth2 is chosen, auto-fill the endpoints from the site's OpenID
|
|
57
|
+
// configuration (unauthenticated) so the user doesn't type them in.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (method !== 'oauth2' || !properties.domain)
|
|
60
|
+
return;
|
|
61
|
+
if (oauth.authorizationEndpoint && oauth.tokenEndpoint)
|
|
62
|
+
return;
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
setDiscoveryNote('Looking up OAuth2 endpoints…');
|
|
65
|
+
void discoverOAuth2Config(properties.domain, properties.useHTTPForDevDeploy).then(discovered => {
|
|
66
|
+
if (cancelled)
|
|
67
|
+
return;
|
|
68
|
+
if (discovered) {
|
|
69
|
+
// Endpoints only — do NOT pre-fill scopes. `scopes_supported` is the
|
|
70
|
+
// provider's list, not what this client is granted (and casing may
|
|
71
|
+
// differ, e.g. advertised `all` vs client `ALL`), so requesting them
|
|
72
|
+
// causes `invalid_scope`. Empty scopes → the client's default scopes.
|
|
73
|
+
setOauth(previous => ({
|
|
74
|
+
...previous,
|
|
75
|
+
authorizationEndpoint: previous.authorizationEndpoint || discovered.authorizationEndpoint,
|
|
76
|
+
tokenEndpoint: previous.tokenEndpoint || discovered.tokenEndpoint,
|
|
77
|
+
}));
|
|
78
|
+
setDiscoveryNote('Endpoints auto-filled from the site OpenID config.');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setDiscoveryNote('Could not auto-discover endpoints — enter them below.');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
};
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [method, properties.domain]);
|
|
89
|
+
const methodSteps = method === 'oauth2'
|
|
90
|
+
? [
|
|
91
|
+
'oauthClientId',
|
|
92
|
+
'oauthAuthEndpoint',
|
|
93
|
+
'oauthTokenEndpoint',
|
|
94
|
+
'oauthScopes',
|
|
95
|
+
'oauthClientSecret',
|
|
96
|
+
]
|
|
97
|
+
: method === 'cookie'
|
|
98
|
+
? ['sessionLoginUrl']
|
|
99
|
+
: ['password'];
|
|
100
|
+
const steps = authOnly
|
|
101
|
+
? ['authMethod', ...methodSteps]
|
|
102
|
+
: [
|
|
103
|
+
'domain',
|
|
104
|
+
'siteName',
|
|
105
|
+
'addonName',
|
|
106
|
+
'username',
|
|
107
|
+
'authMethod',
|
|
108
|
+
...methodSteps,
|
|
109
|
+
'useHTTP',
|
|
110
|
+
];
|
|
111
|
+
const currentStep = steps[stepIndex];
|
|
112
|
+
const redirectPort = initialProperties?.oauth2?.redirectPort ?? DEFAULT_REDIRECT_PORT;
|
|
113
|
+
const advance = () => {
|
|
114
|
+
if (stepIndex < steps.length - 1) {
|
|
30
115
|
setStepIndex(stepIndex + 1);
|
|
31
116
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
117
|
+
};
|
|
118
|
+
const finalize = (props, fields) => {
|
|
119
|
+
const authMethod = props.authMethod ?? 'basic';
|
|
120
|
+
const domain = props.domain ?? '';
|
|
121
|
+
const username = props.username ?? '';
|
|
122
|
+
const finalProps = {
|
|
123
|
+
domain,
|
|
124
|
+
siteName: props.siteName ?? '',
|
|
125
|
+
addonName: props.addonName ?? '',
|
|
126
|
+
username,
|
|
127
|
+
authMethod,
|
|
128
|
+
useHTTPForDevDeploy: props.useHTTPForDevDeploy ?? false,
|
|
129
|
+
};
|
|
130
|
+
if (authMethod === 'oauth2') {
|
|
131
|
+
finalProps.oauth2 = {
|
|
132
|
+
authorizationEndpoint: fields.authorizationEndpoint,
|
|
133
|
+
tokenEndpoint: fields.tokenEndpoint,
|
|
134
|
+
clientId: fields.clientId,
|
|
135
|
+
scopes: parseScopes(fields.scopes),
|
|
136
|
+
...(initialProperties?.oauth2?.redirectPort && {
|
|
137
|
+
redirectPort: initialProperties.oauth2.redirectPort,
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
if (fields.clientSecret && domain && fields.clientId) {
|
|
141
|
+
setOAuth2ClientSecret(domain, fields.clientId, fields.clientSecret);
|
|
37
142
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
143
|
+
}
|
|
144
|
+
else if (authMethod === 'cookie') {
|
|
145
|
+
if (props.sessionLoginUrl) {
|
|
146
|
+
finalProps.sessionLoginUrl = props.sessionLoginUrl;
|
|
41
147
|
}
|
|
42
|
-
|
|
43
|
-
|
|
148
|
+
}
|
|
149
|
+
else if (props.password && domain && username) {
|
|
150
|
+
setDeployPassword(domain, username, props.password);
|
|
151
|
+
}
|
|
152
|
+
else if (domain && username) {
|
|
153
|
+
// Empty password — clear any stale keychain entry so deploy prompts.
|
|
154
|
+
deleteDeployPassword(domain, username);
|
|
155
|
+
}
|
|
156
|
+
writeDevProperties(projectRoot, finalProps);
|
|
157
|
+
onComplete();
|
|
158
|
+
};
|
|
159
|
+
// Update a top-level DevProperties field, then advance or finalize.
|
|
160
|
+
const submitProperty = (key, value) => {
|
|
161
|
+
const next = { ...properties, [key]: value };
|
|
162
|
+
setProperties(next);
|
|
163
|
+
if (steps[stepIndex] === steps[steps.length - 1]) {
|
|
164
|
+
finalize(next, oauth);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
advance();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// Update an OAuth field, then advance or finalize.
|
|
171
|
+
const submitOAuth = (key, value) => {
|
|
172
|
+
const next = { ...oauth, [key]: value };
|
|
173
|
+
setOauth(next);
|
|
174
|
+
if (steps[stepIndex] === steps[steps.length - 1]) {
|
|
175
|
+
finalize(properties, next);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
advance();
|
|
44
179
|
}
|
|
45
180
|
};
|
|
46
181
|
const renderInput = () => {
|
|
47
|
-
switch (currentStep
|
|
182
|
+
switch (currentStep) {
|
|
48
183
|
case 'domain':
|
|
49
|
-
return (_jsx(TextInput, { label: "Development Domain (e.g. www.sitevision.se)", defaultValue: properties.domain, placeholder: "sitevision.se", onSubmit:
|
|
184
|
+
return (_jsx(TextInput, { label: "Development Domain (e.g. www.sitevision.se)", defaultValue: properties.domain, placeholder: "sitevision.se", onSubmit: value => submitProperty('domain', value), onCancel: onCancel }, "domain"));
|
|
50
185
|
case 'siteName':
|
|
51
|
-
return (_jsx(TextInput, { label: "Site Name (Root node name)", defaultValue: properties.siteName, onSubmit:
|
|
186
|
+
return (_jsx(TextInput, { label: "Site Name (Root node name)", defaultValue: properties.siteName, onSubmit: value => submitProperty('siteName', value), onCancel: onCancel }, "siteName"));
|
|
52
187
|
case 'addonName':
|
|
53
|
-
return (_jsx(TextInput, { label: "Addon Name", defaultValue: properties.addonName, onSubmit:
|
|
188
|
+
return (_jsx(TextInput, { label: "Addon Name", defaultValue: properties.addonName, onSubmit: value => submitProperty('addonName', value), onCancel: onCancel }, "addonName"));
|
|
54
189
|
case 'username':
|
|
55
|
-
return (_jsx(TextInput, { label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit:
|
|
190
|
+
return (_jsx(TextInput, { label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit: value => submitProperty('username', value), onCancel: onCancel }, "username"));
|
|
191
|
+
case 'authMethod':
|
|
192
|
+
return (_jsx(MethodSelect, { defaultValue: method, onSubmit: value => submitProperty('authMethod', value) }, "authMethod"));
|
|
56
193
|
case 'password':
|
|
57
|
-
return (_jsx(TextInput, { label: "Password (saved in OS keychain \u2014 leave empty to prompt on each run)", type: "password", defaultValue: properties.password, onSubmit:
|
|
194
|
+
return (_jsx(TextInput, { label: "Password (saved in OS keychain \u2014 leave empty to prompt on each run)", type: "password", defaultValue: properties.password, onSubmit: value => submitProperty('password', value), onCancel: onCancel }, "password"));
|
|
195
|
+
case 'oauthClientId':
|
|
196
|
+
return (_jsx(TextInput, { label: "OAuth2 Client ID", defaultValue: oauth.clientId, onSubmit: value => submitOAuth('clientId', value), onCancel: onCancel }, "oauthClientId"));
|
|
197
|
+
case 'oauthAuthEndpoint':
|
|
198
|
+
return (_jsx(TextInput, { label: "Authorization Endpoint URL", defaultValue: oauth.authorizationEndpoint, onSubmit: value => submitOAuth('authorizationEndpoint', value), onCancel: onCancel }, "oauthAuthEndpoint"));
|
|
199
|
+
case 'oauthTokenEndpoint':
|
|
200
|
+
return (_jsx(TextInput, { label: "Token Endpoint URL", defaultValue: oauth.tokenEndpoint, onSubmit: value => submitOAuth('tokenEndpoint', value), onCancel: onCancel }, "oauthTokenEndpoint"));
|
|
201
|
+
case 'oauthScopes':
|
|
202
|
+
return (_jsx(TextInput, { label: "Scopes (space-separated \u2014 leave empty to use the client's default scopes; add offline_access, matching your client's casing, for a refresh token)", defaultValue: oauth.scopes, onSubmit: value => submitOAuth('scopes', value), onCancel: onCancel }, "oauthScopes"));
|
|
203
|
+
case 'oauthClientSecret':
|
|
204
|
+
return (_jsx(TextInput, { label: "Client Secret (OS keychain \u2014 required if your Sitevision client has a secret; leave empty only for a public client)", type: "password", defaultValue: oauth.clientSecret, onSubmit: value => submitOAuth('clientSecret', value), onCancel: onCancel }, "oauthClientSecret"));
|
|
205
|
+
case 'sessionLoginUrl':
|
|
206
|
+
return (_jsx(TextInput, { label: "Login URL (opened in a browser; blank = site root)", defaultValue: properties.sessionLoginUrl ??
|
|
207
|
+
(properties.domain ? `https://${properties.domain}/` : ''), onSubmit: value => submitProperty('sessionLoginUrl', value), onCancel: onCancel }, "sessionLoginUrl"));
|
|
58
208
|
case 'useHTTP':
|
|
59
|
-
return (_jsx(BooleanInput, { label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy ?? false, onSubmit:
|
|
209
|
+
return (_jsx(BooleanInput, { label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy ?? false, onSubmit: value => submitProperty('useHTTPForDevDeploy', value) }, "useHTTP"));
|
|
60
210
|
default:
|
|
61
211
|
return null;
|
|
62
212
|
}
|
|
63
213
|
};
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Development Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ",
|
|
214
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Development Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ", steps.length, ":", ' ', currentStep ? LABELS[currentStep] : ''] })] }), isOAuth && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Whitelist this redirect URI on the OAuth2 client: http://127.0.0.1:", redirectPort, "/callback"] }), discoveryNote && _jsx(Text, { color: "yellow", children: discoveryNote })] })), _jsx(Box, { marginBottom: 1, children: steps.map((s, i) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: i <= stepIndex ? 'green' : 'gray', children: i < stepIndex ? '✓' : i === stepIndex ? '●' : '○' }) }, s))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: renderInput() })] }));
|
|
215
|
+
}
|
|
216
|
+
function MethodSelect({ defaultValue, onSubmit, }) {
|
|
217
|
+
const [index, setIndex] = useState(() => Math.max(0, AUTH_METHODS.findIndex(m => m.value === defaultValue)));
|
|
218
|
+
useInput((_input, key) => {
|
|
219
|
+
if (key.upArrow) {
|
|
220
|
+
setIndex(p => (p === 0 ? AUTH_METHODS.length - 1 : p - 1));
|
|
221
|
+
}
|
|
222
|
+
else if (key.downArrow) {
|
|
223
|
+
setIndex(p => (p === AUTH_METHODS.length - 1 ? 0 : p + 1));
|
|
224
|
+
}
|
|
225
|
+
else if (key.return) {
|
|
226
|
+
onSubmit(AUTH_METHODS[index].value);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Authentication method" }) }), AUTH_METHODS.map((m, i) => (_jsxs(Text, { color: i === index ? 'green' : undefined, children: [i === index ? '❯ ' : ' ', m.label] }, m.value))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move, Enter to select" }) })] }));
|
|
65
230
|
}
|
|
66
231
|
function BooleanInput({ label, defaultValue, onSubmit, }) {
|
|
67
232
|
useInput(input => {
|
|
@@ -51,6 +51,11 @@ export function MainMenu({ project, onSelect }) {
|
|
|
51
51
|
value: 'deploy-production',
|
|
52
52
|
description: 'Deploy to production server',
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
label: '🔑 Auth Method',
|
|
56
|
+
value: 'change-auth',
|
|
57
|
+
description: 'Change the deploy authentication method',
|
|
58
|
+
},
|
|
54
59
|
{
|
|
55
60
|
label: 'ℹ️ Info',
|
|
56
61
|
value: 'info',
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ProjectInfo } from '../utils/project-detection.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
project: ProjectInfo;
|
|
4
|
+
onReload: () => void;
|
|
4
5
|
onComplete: () => void;
|
|
5
6
|
}
|
|
6
|
-
export declare function SetupFlow({ project, onComplete }: Props): import("react").JSX.Element | null;
|
|
7
|
+
export declare function SetupFlow({ project, onReload, onComplete }: Props): import("react").JSX.Element | null;
|
|
7
8
|
export {};
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { getAppType, localizedText, migrateLegacyPassword, } from '../utils/project-detection.js';
|
|
4
|
+
import { getAppType, localizedText, migrateLegacyPassword, getPackageJsonSyncChanges, syncDevPropertiesToPackageJson, readSvcConfig, writeSvcConfig, writeDevProperties, } from '../utils/project-detection.js';
|
|
5
5
|
import { ProcessRunner } from '../utils/process-runner.js';
|
|
6
6
|
import { ProcessOutputComponent } from './ProcessOutput.js';
|
|
7
7
|
import { StatusIndicator } from './StatusIndicator.js';
|
|
8
8
|
import { DevPropertiesForm } from './DevPropertiesForm.js';
|
|
9
9
|
import { SigningPropertiesForm } from './SigningPropertiesForm.js';
|
|
10
|
-
export function SetupFlow({ project, onComplete }) {
|
|
10
|
+
export function SetupFlow({ project, onReload, onComplete }) {
|
|
11
11
|
const [step, setStep] = useState('check-node-modules');
|
|
12
12
|
const [runner, setRunner] = useState(null);
|
|
13
13
|
const [commandStatus, setCommandStatus] = useState('running');
|
|
14
|
+
const [syncChanges, setSyncChanges] = useState([]);
|
|
15
|
+
const [syncDecision, setSyncDecision] = useState(false);
|
|
16
|
+
// Set when the user picks OAuth2 for an existing config, so the form opens in
|
|
17
|
+
// the OAuth2 branch without mutating the shared project object.
|
|
18
|
+
const [pendingAuthMethod, setPendingAuthMethod] = useState(undefined);
|
|
14
19
|
const appType = getAppType(project.manifest);
|
|
15
20
|
// Auto-advance through checks
|
|
16
21
|
useEffect(() => {
|
|
@@ -27,14 +32,36 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
27
32
|
if (project.hasLegacyPassword) {
|
|
28
33
|
setStep('confirm-password-migration');
|
|
29
34
|
}
|
|
35
|
+
else if (project.devProperties?.authMethod === undefined) {
|
|
36
|
+
setStep('confirm-auth-method');
|
|
37
|
+
}
|
|
30
38
|
else {
|
|
31
|
-
setStep('check-
|
|
39
|
+
setStep('check-package-sync');
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
else {
|
|
35
43
|
setStep('confirm-dev-setup');
|
|
36
44
|
}
|
|
37
45
|
}
|
|
46
|
+
else if (step === 'check-package-sync') {
|
|
47
|
+
const preference = readSvcConfig(project.root).syncPackageJson;
|
|
48
|
+
const properties = project.devProperties;
|
|
49
|
+
const changes = preference !== false && properties
|
|
50
|
+
? getPackageJsonSyncChanges(project.root, properties)
|
|
51
|
+
: [];
|
|
52
|
+
if (changes.length === 0 || !properties) {
|
|
53
|
+
setStep('check-signing-properties');
|
|
54
|
+
}
|
|
55
|
+
else if (preference === true) {
|
|
56
|
+
syncDevPropertiesToPackageJson(project.root, properties);
|
|
57
|
+
onReload();
|
|
58
|
+
setStep('check-signing-properties');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
setSyncChanges(changes);
|
|
62
|
+
setStep('confirm-package-sync');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
else if (step === 'check-signing-properties') {
|
|
39
66
|
if (project.hasSigningProperties) {
|
|
40
67
|
setStep('show-info');
|
|
@@ -84,6 +111,29 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
84
111
|
else if (step === 'confirm-password-migration') {
|
|
85
112
|
if (input === 'y' || input === 'Y') {
|
|
86
113
|
migrateLegacyPassword(project);
|
|
114
|
+
// The file was rewritten (plaintext stripped, password moved to
|
|
115
|
+
// keychain) — re-detect so hasLegacyPassword/password reflect that.
|
|
116
|
+
onReload();
|
|
117
|
+
setStep('check-package-sync');
|
|
118
|
+
}
|
|
119
|
+
else if (input === 'n' || input === 'N') {
|
|
120
|
+
setStep('check-package-sync');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (step === 'confirm-package-sync') {
|
|
124
|
+
if (['y', 'Y', 'n', 'N'].includes(input)) {
|
|
125
|
+
const accepted = input.toLowerCase() === 'y';
|
|
126
|
+
if (accepted && project.devProperties) {
|
|
127
|
+
syncDevPropertiesToPackageJson(project.root, project.devProperties);
|
|
128
|
+
onReload();
|
|
129
|
+
}
|
|
130
|
+
setSyncDecision(accepted);
|
|
131
|
+
setStep('confirm-save-sync-choice');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (step === 'confirm-save-sync-choice') {
|
|
135
|
+
if (input === 'y' || input === 'Y') {
|
|
136
|
+
writeSvcConfig(project.root, { syncPackageJson: syncDecision });
|
|
87
137
|
setStep('check-signing-properties');
|
|
88
138
|
}
|
|
89
139
|
else if (input === 'n' || input === 'N') {
|
|
@@ -98,21 +148,46 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
98
148
|
setStep('show-info');
|
|
99
149
|
}
|
|
100
150
|
}
|
|
151
|
+
else if (step === 'confirm-auth-method') {
|
|
152
|
+
if (input === 'o' || input === 'O') {
|
|
153
|
+
// Choosing OAuth2 needs endpoints — collect them in the full form.
|
|
154
|
+
setPendingAuthMethod('oauth2');
|
|
155
|
+
setStep('setup-dev-properties');
|
|
156
|
+
}
|
|
157
|
+
else if (input === 'c' || input === 'C') {
|
|
158
|
+
setPendingAuthMethod('cookie');
|
|
159
|
+
setStep('setup-dev-properties');
|
|
160
|
+
}
|
|
161
|
+
else if (['b', 'B', '\r'].includes(input)) {
|
|
162
|
+
if (project.devProperties) {
|
|
163
|
+
writeDevProperties(project.root, {
|
|
164
|
+
...project.devProperties,
|
|
165
|
+
authMethod: 'basic',
|
|
166
|
+
});
|
|
167
|
+
onReload();
|
|
168
|
+
}
|
|
169
|
+
setStep('check-package-sync');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
101
172
|
});
|
|
102
173
|
// Setup Dev Properties Form
|
|
103
174
|
if (step === 'setup-dev-properties') {
|
|
104
|
-
return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
175
|
+
return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties: pendingAuthMethod && project.devProperties
|
|
176
|
+
? { ...project.devProperties, authMethod: pendingAuthMethod }
|
|
177
|
+
: project.devProperties, packageJson: project.packageJson, onComplete: () => {
|
|
178
|
+
// Re-detect from disk/keychain so devProperties (incl. the keychain
|
|
179
|
+
// password) populate in memory — otherwise the rest of this flow and
|
|
180
|
+
// the menu would see stale state until the CLI is restarted.
|
|
181
|
+
onReload();
|
|
182
|
+
setStep('check-package-sync');
|
|
110
183
|
}, onCancel: () => setStep('check-signing-properties') }));
|
|
111
184
|
}
|
|
112
185
|
// Setup Signing Properties Form
|
|
113
186
|
if (step === 'setup-signing-properties') {
|
|
114
187
|
return (_jsx(SigningPropertiesForm, { projectRoot: project.root, onComplete: () => {
|
|
115
|
-
|
|
188
|
+
// Re-detect so signing credentials are reflected in memory before
|
|
189
|
+
// the info screen / menu render.
|
|
190
|
+
onReload();
|
|
116
191
|
setStep('show-info');
|
|
117
192
|
}, onCancel: () => setStep('show-info') }));
|
|
118
193
|
}
|
|
@@ -132,6 +207,20 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
132
207
|
if (step === 'confirm-password-migration') {
|
|
133
208
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 Plaintext password found in .dev_properties.json" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Move it to the OS keychain and remove it from the file? (y/n)" }), _jsx(Text, { dimColor: true, children: "Recommended \u2014 storing passwords in project files is insecure." })] })] }));
|
|
134
209
|
}
|
|
210
|
+
// Ask which auth method an existing config should use (setting is missing)
|
|
211
|
+
if (step === 'confirm-auth-method') {
|
|
212
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 Deploy authentication method not set" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Which method should deploys use? [B]asic / [O]Auth2 / [C]ookie" }), _jsx(Text, { dimColor: true, children: "B = Basic (default), O = OAuth2 bearer, C = session cookie (SSO)." })] })] }));
|
|
213
|
+
}
|
|
214
|
+
// Confirm package.json sync
|
|
215
|
+
if (step === 'confirm-package-sync') {
|
|
216
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 package.json is out of sync with .dev_properties.json" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", marginLeft: 2, children: syncChanges.map(change => (_jsxs(Box, { children: [_jsx(Text, { color: change.from === undefined ? 'green' : 'yellow', children: change.from === undefined ? '+ ' : '~ ' }), _jsxs(Text, { bold: true, children: [change.key, ": "] }), change.from !== undefined && (_jsxs(Text, { dimColor: true, children: [change.from, " \u2192 "] })), _jsx(Text, { children: change.to })] }, change.key))) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Update package.json from .dev_properties.json? (y/n)" }), _jsx(Text, { dimColor: true, children: "sitevision-scripts reads these fields from package.json." })] })] }));
|
|
217
|
+
}
|
|
218
|
+
// Offer to persist the sync decision in .svcconfig
|
|
219
|
+
if (step === 'confirm-save-sync-choice') {
|
|
220
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { children: "Remember this choice in .svcconfig? (y/n)" }), _jsx(Text, { dimColor: true, children: syncDecision
|
|
221
|
+
? 'svc will update package.json automatically from now on.'
|
|
222
|
+
: 'svc will stop asking about package.json sync.' })] })] }));
|
|
223
|
+
}
|
|
135
224
|
// Confirm signing setup
|
|
136
225
|
if (step === 'confirm-signing-setup') {
|
|
137
226
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision CLI" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 signing credentials not configured" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Signing credentials are required for signing apps on developer.sitevision.se" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Would you like to set up signing credentials? (y/n)" }) })] }));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SimpleAppType } from '../types/index.js';
|
|
2
|
+
import { type AddonNode } from '../utils/sitevision-api.js';
|
|
3
|
+
export declare function AddonPicker({ domain, appType, initialQuery, load, onSelect, onClose, height, }: {
|
|
4
|
+
domain: string;
|
|
5
|
+
appType?: SimpleAppType;
|
|
6
|
+
initialQuery: string;
|
|
7
|
+
load: () => Promise<{
|
|
8
|
+
addons?: AddonNode[];
|
|
9
|
+
error?: string;
|
|
10
|
+
}>;
|
|
11
|
+
onSelect: (name: string) => void;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
height: number;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CHANGELOG.md without its title, or undefined when it isn't shipped. With
|
|
3
|
+
* `since`, only the releases above that version's heading.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readChangelog(since?: string): string[] | undefined;
|
|
6
|
+
export declare function ChangelogPanel({ since, height, onClose, }: {
|
|
7
|
+
since?: string;
|
|
8
|
+
height: number;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { Box, Text, useInput } from 'ink';
|
|
5
|
+
import { t } from '../utils/i18n.js';
|
|
6
|
+
import { ACCENT } from './Frame.js';
|
|
7
|
+
/**
|
|
8
|
+
* CHANGELOG.md without its title, or undefined when it isn't shipped. With
|
|
9
|
+
* `since`, only the releases above that version's heading.
|
|
10
|
+
*/
|
|
11
|
+
export function readChangelog(since) {
|
|
12
|
+
try {
|
|
13
|
+
const lines = readFileSync(new URL('../../CHANGELOG.md', import.meta.url), 'utf8')
|
|
14
|
+
.replace(/^# .*\n/, '')
|
|
15
|
+
.trim()
|
|
16
|
+
.split('\n');
|
|
17
|
+
const end = since ? lines.indexOf(`## ${since}`) : -1;
|
|
18
|
+
return end > 0 ? lines.slice(0, end) : lines;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function ChangelogPanel({ since, height, onClose, }) {
|
|
25
|
+
const lines = useMemo(() => readChangelog(since), [since]);
|
|
26
|
+
const [top, setTop] = useState(0);
|
|
27
|
+
const visible = Math.max(1, height - 1);
|
|
28
|
+
const max = Math.max(0, (lines?.length ?? 0) - visible);
|
|
29
|
+
useInput((input, key) => {
|
|
30
|
+
if (key.escape || key.return || input === 'q')
|
|
31
|
+
onClose();
|
|
32
|
+
else if (key.upArrow)
|
|
33
|
+
setTop(n => Math.max(0, n - 1));
|
|
34
|
+
else if (key.downArrow)
|
|
35
|
+
setTop(n => Math.min(max, n + 1));
|
|
36
|
+
else if (key.pageUp)
|
|
37
|
+
setTop(n => Math.max(0, n - visible));
|
|
38
|
+
else if (key.pageDown)
|
|
39
|
+
setTop(n => Math.min(max, n + visible));
|
|
40
|
+
});
|
|
41
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: since
|
|
42
|
+
? t("What's new since {version}", { version: since })
|
|
43
|
+
: t('Changelog') }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t('↑↓ scroll · Esc close')] })] }), lines ? (lines.slice(top, top + visible).map((line, i) => line.startsWith('## ') ? (_jsx(Text, { bold: true, color: ACCENT, children: line.slice(3) }, top + i)) : (_jsx(Text, { wrap: "truncate", children: line || ' ' }, top + i)))) : (_jsx(Text, { dimColor: true, children: t('No changelog found.') }))] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProjectInfo } from '../types/index.js';
|
|
2
|
+
import { type Action } from './actions.js';
|
|
3
|
+
export declare const GROUPS: {
|
|
4
|
+
id: Action['group'];
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare function CommandPalette({ project, onRun, onClose, height, }: {
|
|
8
|
+
project: ProjectInfo;
|
|
9
|
+
onRun: (action: Action) => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
height: number;
|
|
12
|
+
}): import("react").JSX.Element;
|