sitevision-cli 1.0.0-beta.2 → 1.0.0-beta.21
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/dist/app.d.ts +1 -1
- package/dist/app.js +59 -8
- package/dist/cli.js +96 -39
- package/dist/commands/build.js +1 -1
- package/dist/commands/deploy.d.ts +2 -2
- package/dist/commands/deploy.js +135 -25
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +77 -366
- package/dist/commands/info.js +2 -2
- 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/InfoScreen.js +2 -2
- package/dist/components/MainMenu.js +7 -2
- package/dist/components/PasswordInput.js +2 -1
- package/dist/components/SetupFlow.d.ts +2 -1
- package/dist/components/SetupFlow.js +100 -11
- package/dist/shell/AddonPicker.d.ts +14 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/CommandPalette.d.ts +8 -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 +134 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +9 -0
- package/dist/shell/Shell.js +586 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +90 -0
- package/dist/shell/actions.d.ts +45 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +44 -5
- 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 +279 -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/project-detection.d.ts +105 -6
- package/dist/utils/project-detection.js +411 -54
- 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 +64 -5
- package/dist/utils/sitevision-api.js +195 -33
- package/dist/utils/tasks.d.ts +48 -0
- package/dist/utils/tasks.js +371 -0
- package/dist/utils/workspace.d.ts +17 -0
- package/dist/utils/workspace.js +67 -0
- package/package.json +3 -1
- package/readme.md +102 -121
package/dist/app.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import { type ProjectInfo } from './utils/project-detection.js';
|
|
|
2
2
|
type Props = {
|
|
3
3
|
project: ProjectInfo;
|
|
4
4
|
};
|
|
5
|
-
export default function App({ project }: Props): import("react").JSX.Element | null;
|
|
5
|
+
export default function App({ project: initialProject }: Props): import("react").JSX.Element | null;
|
|
6
6
|
export {};
|
package/dist/app.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { detectProject } from './utils/project-detection.js';
|
|
3
4
|
import { MainMenu } from './components/MainMenu.js';
|
|
4
5
|
import { InfoScreen } from './components/InfoScreen.js';
|
|
5
6
|
import { SetupFlow } from './components/SetupFlow.js';
|
|
7
|
+
import { DevPropertiesForm } from './components/DevPropertiesForm.js';
|
|
6
8
|
import { PasswordInput } from './components/PasswordInput.js';
|
|
7
9
|
import { KeychainPasswordChoice } from './components/KeychainPasswordChoice.js';
|
|
8
10
|
import { decideSigningStep } from './utils/signing-step.js';
|
|
@@ -12,7 +14,23 @@ import { DeployScreen } from './commands/deploy.js';
|
|
|
12
14
|
import { SignScreen } from './commands/sign.js';
|
|
13
15
|
import { SigningPropertiesForm } from './components/SigningPropertiesForm.js';
|
|
14
16
|
import { getSigningPassword, setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword, } from './utils/keychain.js';
|
|
15
|
-
|
|
17
|
+
const NONBASIC_DEV_UNSUPPORTED = '\x1b[33mdev/watch support only basic auth. Use `svc deploy` for OAuth2/cookie.\x1b[0m';
|
|
18
|
+
export default function App({ project: initialProject }) {
|
|
19
|
+
// The project is loaded once at startup, but setup flows write new values to
|
|
20
|
+
// disk and the OS keychain. Hold it in state so we can re-detect after setup
|
|
21
|
+
// and pick up those changes (e.g. saved passwords) without restarting the CLI.
|
|
22
|
+
const [project, setProject] = useState(initialProject);
|
|
23
|
+
const reloadProject = useCallback(() => {
|
|
24
|
+
try {
|
|
25
|
+
const refreshed = detectProject(initialProject.root);
|
|
26
|
+
if (refreshed)
|
|
27
|
+
setProject(refreshed);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Re-detection failed (e.g. manifest became unparseable mid-session) —
|
|
31
|
+
// keep the existing in-memory project rather than crashing.
|
|
32
|
+
}
|
|
33
|
+
}, [initialProject.root]);
|
|
16
34
|
const [state, setState] = useState('setup');
|
|
17
35
|
const [currentCommand, setCurrentCommand] = useState('');
|
|
18
36
|
const [signingPassword, setSigningPassword] = useState('');
|
|
@@ -54,10 +72,18 @@ export default function App({ project }) {
|
|
|
54
72
|
};
|
|
55
73
|
// Check if dev password is available (either from file or session)
|
|
56
74
|
const hasDevPassword = Boolean(project.devProperties?.password || devPassword);
|
|
75
|
+
// OAuth2 / cookie configs authenticate with a token or session resolved at
|
|
76
|
+
// deploy time, so they need no basic password. `svc dev`/`watch` stay basic.
|
|
77
|
+
const authMethod = project.devProperties?.authMethod ?? 'basic';
|
|
78
|
+
const isTokenAuth = authMethod === 'oauth2' || authMethod === 'cookie';
|
|
79
|
+
const deployAuthReady = isTokenAuth || hasDevPassword;
|
|
57
80
|
// Get effective dev properties with session password if needed
|
|
58
81
|
const getEffectiveDevProperties = () => {
|
|
59
82
|
if (!project.devProperties)
|
|
60
83
|
return undefined;
|
|
84
|
+
// Non-basic configs authenticate by token/cookie — never graft a password.
|
|
85
|
+
if (isTokenAuth)
|
|
86
|
+
return project.devProperties;
|
|
61
87
|
if (project.devProperties.password)
|
|
62
88
|
return project.devProperties;
|
|
63
89
|
return { ...project.devProperties, password: devPassword };
|
|
@@ -110,11 +136,22 @@ export default function App({ project }) {
|
|
|
110
136
|
case 'setup-signing':
|
|
111
137
|
setState('setup-signing');
|
|
112
138
|
break;
|
|
139
|
+
case 'change-auth':
|
|
140
|
+
if (!project.hasDevProperties || !project.devProperties) {
|
|
141
|
+
console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
setState('change-auth-method');
|
|
145
|
+
break;
|
|
113
146
|
case 'dev':
|
|
114
147
|
if (!project.hasDevProperties || !project.devProperties) {
|
|
115
148
|
console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
|
|
116
149
|
return;
|
|
117
150
|
}
|
|
151
|
+
if (isTokenAuth) {
|
|
152
|
+
console.log(NONBASIC_DEV_UNSUPPORTED);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
118
155
|
if (!hasDevPassword) {
|
|
119
156
|
setState('dev-password-input');
|
|
120
157
|
}
|
|
@@ -131,6 +168,10 @@ export default function App({ project }) {
|
|
|
131
168
|
console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
|
|
132
169
|
return;
|
|
133
170
|
}
|
|
171
|
+
if (isTokenAuth) {
|
|
172
|
+
console.log(NONBASIC_DEV_UNSUPPORTED);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
134
175
|
// Need both dev password and signing password
|
|
135
176
|
if (!hasDevPassword) {
|
|
136
177
|
setState('dev-password-input');
|
|
@@ -152,6 +193,10 @@ export default function App({ project }) {
|
|
|
152
193
|
console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
|
|
153
194
|
return;
|
|
154
195
|
}
|
|
196
|
+
if (isTokenAuth) {
|
|
197
|
+
console.log(NONBASIC_DEV_UNSUPPORTED);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
155
200
|
routeToSigningStep(command);
|
|
156
201
|
break;
|
|
157
202
|
case 'sign':
|
|
@@ -175,7 +220,7 @@ export default function App({ project }) {
|
|
|
175
220
|
console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
|
|
176
221
|
return;
|
|
177
222
|
}
|
|
178
|
-
if (!
|
|
223
|
+
if (!deployAuthReady) {
|
|
179
224
|
setState('dev-password-input');
|
|
180
225
|
}
|
|
181
226
|
else {
|
|
@@ -184,8 +229,14 @@ export default function App({ project }) {
|
|
|
184
229
|
break;
|
|
185
230
|
}
|
|
186
231
|
};
|
|
232
|
+
if (state === 'change-auth-method') {
|
|
233
|
+
return (_jsx(DevPropertiesForm, { projectRoot: project.root, initialProperties: project.devProperties, packageJson: project.packageJson, authOnly: true, onComplete: () => {
|
|
234
|
+
reloadProject();
|
|
235
|
+
setState('menu');
|
|
236
|
+
}, onCancel: () => setState('menu') }));
|
|
237
|
+
}
|
|
187
238
|
if (state === 'setup') {
|
|
188
|
-
return _jsx(SetupFlow, { project: project, onComplete: () => setState('menu') });
|
|
239
|
+
return (_jsx(SetupFlow, { project: project, onReload: reloadProject, onComplete: () => setState('menu') }));
|
|
189
240
|
}
|
|
190
241
|
if (state === 'menu') {
|
|
191
242
|
return _jsx(MainMenu, { project: project, onSelect: handleCommandSelect });
|
|
@@ -256,13 +307,13 @@ export default function App({ project }) {
|
|
|
256
307
|
if (project.devProperties)
|
|
257
308
|
project.devProperties.password = undefined;
|
|
258
309
|
setState('dev-password-input');
|
|
259
|
-
} }));
|
|
310
|
+
}, onChangeAuthMethod: () => setState('change-auth-method') }));
|
|
260
311
|
}
|
|
261
312
|
if (state === 'setup-signing') {
|
|
262
313
|
return (_jsx(SigningPropertiesForm, { projectRoot: project.root, onComplete: () => {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
314
|
+
// Re-detect so the newly written signing credentials are reflected
|
|
315
|
+
// in memory (hasSigningProperties, keychain password) without a restart.
|
|
316
|
+
reloadProject();
|
|
266
317
|
setState('menu');
|
|
267
318
|
}, onCancel: () => setState('menu') }));
|
|
268
319
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { render } from 'ink';
|
|
4
4
|
import { Text, Box } from 'ink';
|
|
5
5
|
import meow from 'meow';
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
|
-
import
|
|
7
|
+
import { Shell } from './shell/Shell.js';
|
|
8
8
|
import { getCommand } from './commands/index.js';
|
|
9
|
-
import { requireProject, migrateLegacyPassword, } from './utils/project-detection.js';
|
|
9
|
+
import { requireProject, detectProject, migrateLegacyPassword, } from './utils/project-detection.js';
|
|
10
|
+
import { discoverApps } from './utils/workspace.js';
|
|
10
11
|
import { promptYesNo } from './utils/password-prompt.js';
|
|
11
12
|
import { checkForUpdate } from './utils/version-check.js';
|
|
12
|
-
import { isFirstRun, markFirstRunComplete, getLastSeenVersion, setLastSeenVersion, } from './utils/config.js';
|
|
13
|
+
import { isFirstRun, markFirstRunComplete, getLastSeenVersion, setLastSeenVersion, getSettings, } from './utils/config.js';
|
|
14
|
+
import { setLanguage } from './utils/i18n.js';
|
|
13
15
|
import { WelcomeScreen } from './components/WelcomeScreen.js';
|
|
14
16
|
import { AnimatedLogo } from './components/AnimatedLogo.js';
|
|
15
17
|
import { printBranding, BIG_LOGO, BIG_LOGO_WIDTH, SMALL_LOGO, SMALL_LOGO_WIDTH, } from './utils/branding.js';
|
|
16
18
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
17
19
|
const cli = meow(`
|
|
18
20
|
Usage
|
|
19
|
-
$ svc
|
|
21
|
+
$ svc Open the interactive shell (app or workspace)
|
|
20
22
|
$ svc <command> [options]
|
|
21
23
|
|
|
22
24
|
Commands
|
|
@@ -28,11 +30,13 @@ const cli = meow(`
|
|
|
28
30
|
info Show project information
|
|
29
31
|
|
|
30
32
|
Options
|
|
33
|
+
--minimal Shell: compact layout for small terminals
|
|
31
34
|
--help Show this help message
|
|
32
35
|
--version Show version number
|
|
33
36
|
|
|
34
37
|
Examples
|
|
35
|
-
$ svc #
|
|
38
|
+
$ svc # Shell: run inside an app, or at the repo root
|
|
39
|
+
$ svc --minimal # Shell without the sidebar, for a small pane
|
|
36
40
|
$ svc dev
|
|
37
41
|
$ svc dev --signed
|
|
38
42
|
$ svc watch
|
|
@@ -58,9 +62,21 @@ const cli = meow(`
|
|
|
58
62
|
shortFlag: 'p',
|
|
59
63
|
default: false,
|
|
60
64
|
},
|
|
65
|
+
token: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
},
|
|
68
|
+
cookie: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
},
|
|
71
|
+
minimal: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
default: false,
|
|
74
|
+
},
|
|
61
75
|
},
|
|
62
76
|
});
|
|
63
77
|
const [commandName, ...args] = cli.input;
|
|
78
|
+
const settings = getSettings();
|
|
79
|
+
setLanguage(settings.language);
|
|
64
80
|
const CYAN = '\x1b[36m';
|
|
65
81
|
const BOLD = '\x1b[1m';
|
|
66
82
|
const DIM = '\x1b[2m';
|
|
@@ -89,8 +105,12 @@ function printMasthead(version) {
|
|
|
89
105
|
`${spaces(gap)}${DIM}${right}${RESET}${spaces(padding)}${CYAN}│${RESET}`);
|
|
90
106
|
console.log(`${CYAN}╰${border}╯${RESET}`);
|
|
91
107
|
}
|
|
108
|
+
function fail(message, hint) {
|
|
109
|
+
render(_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", message] }), _jsx(Text, { dimColor: true, children: hint })] }));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
92
112
|
// Pick the widest wordmark that fits the terminal, or undefined if even the
|
|
93
|
-
// compact one would wrap
|
|
113
|
+
// compact one would wrap.
|
|
94
114
|
function pickIntroArt(columns) {
|
|
95
115
|
if (columns >= BIG_LOGO_WIDTH)
|
|
96
116
|
return BIG_LOGO;
|
|
@@ -105,6 +125,26 @@ async function playIntro(art) {
|
|
|
105
125
|
app.waitUntilExit().then(() => resolve(), () => resolve());
|
|
106
126
|
});
|
|
107
127
|
}
|
|
128
|
+
// Run the full-screen shell on the alternate screen buffer so the scrollback
|
|
129
|
+
// is untouched, and restore it on exit. The animated wordmark plays first,
|
|
130
|
+
// inside the same buffer, when the terminal is wide enough for it.
|
|
131
|
+
async function runShell(apps, workspaceRoot) {
|
|
132
|
+
process.stdout.write('\x1b[?1049h\x1b[H');
|
|
133
|
+
try {
|
|
134
|
+
const art = process.stdin.isTTY && settings.introAnimation && !cli.flags.minimal
|
|
135
|
+
? pickIntroArt(process.stdout.columns ?? 0)
|
|
136
|
+
: undefined;
|
|
137
|
+
if (art) {
|
|
138
|
+
await playIntro(art);
|
|
139
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
140
|
+
}
|
|
141
|
+
const app = render(_jsx(Shell, { apps: apps, workspaceRoot: workspaceRoot, version: pkg.version, minimal: cli.flags.minimal }));
|
|
142
|
+
await app.waitUntilExit();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
process.stdout.write('\x1b[?1049l');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
108
148
|
async function main() {
|
|
109
149
|
// On the very first run we show a dedicated welcome screen instead of the
|
|
110
150
|
// masthead, so the branding is the moment. Only when stdin is a TTY — the
|
|
@@ -116,19 +156,12 @@ async function main() {
|
|
|
116
156
|
// silently rather than claiming an update happened.
|
|
117
157
|
const lastSeen = getLastSeenVersion();
|
|
118
158
|
const isUpdate = !firstRun && lastSeen !== undefined && lastSeen !== pkg.version;
|
|
119
|
-
// On the plain interactive `svc` (no command), play the animated wordmark
|
|
120
|
-
// instead of the static masthead — sized to the terminal. Only on a TTY so it
|
|
121
|
-
// doesn't run in CI / piped input.
|
|
122
|
-
const introEligible = !firstRun && !isUpdate && !commandName && Boolean(process.stdin.isTTY);
|
|
123
|
-
const introArt = introEligible
|
|
124
|
-
? pickIntroArt(process.stdout.columns ?? 0)
|
|
125
|
-
: undefined;
|
|
126
159
|
if (!firstRun) {
|
|
127
160
|
if (isUpdate) {
|
|
128
161
|
printBranding();
|
|
129
162
|
console.log(`\x1b[32m\n ✨ Updated to v${pkg.version}\x1b[0m \x1b[2m(from v${lastSeen})\x1b[0m\n`);
|
|
130
163
|
}
|
|
131
|
-
else if (
|
|
164
|
+
else if (commandName) {
|
|
132
165
|
printMasthead(pkg.version);
|
|
133
166
|
}
|
|
134
167
|
// Record the current version so the banner shows once per upgrade.
|
|
@@ -139,36 +172,61 @@ async function main() {
|
|
|
139
172
|
console.log(`\x1b[33m ↑ update available: ${pkg.version} → ${latestVersion} (run: npm i -g ${pkg.name})\x1b[0m`);
|
|
140
173
|
}
|
|
141
174
|
}
|
|
175
|
+
// No command: the shell. Inside an app it is single-app mode; anywhere
|
|
176
|
+
// above one or more apps it is workspace mode.
|
|
177
|
+
if (!commandName) {
|
|
178
|
+
let project = null;
|
|
179
|
+
try {
|
|
180
|
+
project = detectProject();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
fail(error.message, 'Fix the manifest and try again');
|
|
184
|
+
}
|
|
185
|
+
if (!project) {
|
|
186
|
+
const apps = discoverApps(process.cwd());
|
|
187
|
+
if (apps.length === 0) {
|
|
188
|
+
fail('No Sitevision apps found here.', 'Run svc inside an app directory (manifest.json) or at the root of a repo that contains apps.');
|
|
189
|
+
}
|
|
190
|
+
await runShell(apps, process.cwd());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (firstRun) {
|
|
194
|
+
await new Promise(resolve => {
|
|
195
|
+
const app = render(_jsx(WelcomeScreen, { project: project, onComplete: () => {
|
|
196
|
+
markFirstRunComplete();
|
|
197
|
+
setLastSeenVersion(pkg.version);
|
|
198
|
+
app.unmount();
|
|
199
|
+
} }));
|
|
200
|
+
app.waitUntilExit().then(() => resolve(), () => resolve());
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
await runShell([project]);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
142
206
|
// Check if we're in a Sitevision project
|
|
143
207
|
const project = (() => {
|
|
144
208
|
try {
|
|
145
209
|
return requireProject();
|
|
146
210
|
}
|
|
147
211
|
catch (error) {
|
|
148
|
-
|
|
149
|
-
process.exit(1);
|
|
212
|
+
return fail(error.message, "Make sure you're in a Sitevision project directory");
|
|
150
213
|
}
|
|
151
214
|
})();
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!commandName) {
|
|
167
|
-
if (introArt) {
|
|
168
|
-
await playIntro(introArt);
|
|
215
|
+
// --token / --cookie override the resolved bearer token / session cookie for
|
|
216
|
+
// this run (manual / CI path, alongside SITEVISION_ACCESS_TOKEN and
|
|
217
|
+
// SITEVISION_SESSION_COOKIE).
|
|
218
|
+
if (cli.flags.token || cli.flags.cookie) {
|
|
219
|
+
if (project.devProperties) {
|
|
220
|
+
if (cli.flags.token) {
|
|
221
|
+
project.devProperties.accessToken = cli.flags.token;
|
|
222
|
+
}
|
|
223
|
+
if (cli.flags.cookie) {
|
|
224
|
+
project.devProperties.sessionCookie = cli.flags.cookie;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.log('\x1b[33m--token/--cookie needs a .dev_properties.json (domain, site, addon) to deploy against.\x1b[0m');
|
|
169
229
|
}
|
|
170
|
-
render(_jsx(App, { project: project }));
|
|
171
|
-
return;
|
|
172
230
|
}
|
|
173
231
|
// Get the command
|
|
174
232
|
const command = getCommand(commandName);
|
|
@@ -177,9 +235,8 @@ async function main() {
|
|
|
177
235
|
process.exit(1);
|
|
178
236
|
}
|
|
179
237
|
// Offer to migrate a legacy plaintext password into the OS keychain.
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// would fail — the plaintext password is still used for this run.
|
|
238
|
+
// Skip on non-TTY stdin (e.g. CI) where prompting would fail — the
|
|
239
|
+
// plaintext password is still used for this run.
|
|
183
240
|
if (project.hasLegacyPassword && process.stdin.isTTY) {
|
|
184
241
|
console.log('\n\x1b[33m⚠ Plaintext password found in .dev_properties.json\x1b[0m');
|
|
185
242
|
const move = await promptYesNo('Move it to the OS keychain and remove it from the file? (y/N): ');
|
package/dist/commands/build.js
CHANGED
|
@@ -87,7 +87,7 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
|
|
|
87
87
|
const runner = new WebpackRunner(projectRoot, {
|
|
88
88
|
mode: 'production',
|
|
89
89
|
cssPrefix: manifest.id,
|
|
90
|
-
restApp: appType
|
|
90
|
+
restApp: appType !== 'web' && appType !== 'widget',
|
|
91
91
|
});
|
|
92
92
|
const result = await runner.run();
|
|
93
93
|
await runner.close();
|
|
@@ -8,10 +8,10 @@ interface DeployScreenProps {
|
|
|
8
8
|
force: boolean;
|
|
9
9
|
production: boolean;
|
|
10
10
|
activate: boolean;
|
|
11
|
-
signingPassword?: string;
|
|
12
11
|
onBack?: () => void;
|
|
13
12
|
onRetryCredentials?: () => void;
|
|
13
|
+
onChangeAuthMethod?: () => void;
|
|
14
14
|
}
|
|
15
|
-
export declare function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate,
|
|
15
|
+
export declare function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, onChangeAuthMethod, }: DeployScreenProps): React.JSX.Element;
|
|
16
16
|
export declare const deployCommand: Command;
|
|
17
17
|
export {};
|
package/dist/commands/deploy.js
CHANGED
|
@@ -6,26 +6,124 @@ import { deployApp, deployProduction } from '../utils/sitevision-api.js';
|
|
|
6
6
|
import { getZipPath, getSignedZipPath, getAppType, } from '../utils/project-detection.js';
|
|
7
7
|
import { zipExists } from '../utils/zip.js';
|
|
8
8
|
import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
|
|
9
|
-
import { setDeployPassword } from '../utils/keychain.js';
|
|
10
|
-
|
|
9
|
+
import { setDeployPassword, deleteSessionCookie, deleteOAuth2RefreshToken, } from '../utils/keychain.js';
|
|
10
|
+
import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
|
|
11
|
+
import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
|
|
12
|
+
export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, onChangeAuthMethod, }) {
|
|
11
13
|
const [state, setState] = React.useState({
|
|
12
14
|
status: 'deploying',
|
|
13
15
|
message: production ? 'Deploying to production...' : 'Deploying to dev...',
|
|
14
16
|
});
|
|
17
|
+
// 'init' resolves cached credentials, 'login' shows the Ink login screen,
|
|
18
|
+
// 'deploy' runs the upload. Token/cookie login now happens here, so both the
|
|
19
|
+
// TUI and the standalone command reach it.
|
|
20
|
+
const [phase, setPhase] = React.useState('init');
|
|
21
|
+
const [credential, setCredential] = React.useState({
|
|
22
|
+
accessToken: devProperties.accessToken,
|
|
23
|
+
sessionCookie: devProperties.sessionCookie,
|
|
24
|
+
});
|
|
25
|
+
const deployStartedRef = React.useRef(false);
|
|
26
|
+
const authMethod = devProperties.authMethod ?? 'basic';
|
|
27
|
+
// OAuth2 and cookie can re-authenticate in-place; basic re-prompts via the
|
|
28
|
+
// parent (TUI password entry).
|
|
29
|
+
const canRelogin = authMethod === 'oauth2' || authMethod === 'cookie';
|
|
30
|
+
// Discard the stored credential and force a fresh login. This is the
|
|
31
|
+
// "retry with new credentials" action for token/cookie auth — the usual fix
|
|
32
|
+
// when a session/token has expired (Sitevision reports that as a 400, not a
|
|
33
|
+
// 401, so it isn't auto-cleared).
|
|
34
|
+
const retryWithFreshLogin = () => {
|
|
35
|
+
const { domain, username } = devProperties;
|
|
36
|
+
if (authMethod === 'cookie' && domain && username) {
|
|
37
|
+
deleteSessionCookie(domain, username);
|
|
38
|
+
devProperties.sessionCookie = undefined;
|
|
39
|
+
}
|
|
40
|
+
else if (authMethod === 'oauth2' &&
|
|
41
|
+
domain &&
|
|
42
|
+
devProperties.oauth2?.clientId) {
|
|
43
|
+
deleteOAuth2RefreshToken(domain, devProperties.oauth2.clientId);
|
|
44
|
+
devProperties.accessToken = undefined;
|
|
45
|
+
}
|
|
46
|
+
setCredential({});
|
|
47
|
+
deployStartedRef.current = false;
|
|
48
|
+
setState({
|
|
49
|
+
status: 'deploying',
|
|
50
|
+
message: production
|
|
51
|
+
? 'Deploying to production...'
|
|
52
|
+
: 'Deploying to dev...',
|
|
53
|
+
});
|
|
54
|
+
setPhase('login');
|
|
55
|
+
};
|
|
15
56
|
useInput((input, key) => {
|
|
16
57
|
if (state.status !== 'deploying') {
|
|
17
58
|
if (onBack && (key.escape || input === 'q')) {
|
|
18
59
|
onBack();
|
|
19
60
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
61
|
+
if (state.status === 'error' && input === 'r') {
|
|
62
|
+
if (canRelogin) {
|
|
63
|
+
retryWithFreshLogin();
|
|
64
|
+
}
|
|
65
|
+
else if (onRetryCredentials) {
|
|
66
|
+
onRetryCredentials();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (state.status === 'error' && input === 'm' && onChangeAuthMethod) {
|
|
70
|
+
onChangeAuthMethod();
|
|
22
71
|
}
|
|
23
72
|
}
|
|
24
73
|
});
|
|
74
|
+
// Decide once whether we can deploy straight away or must log in first.
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (authMethod === 'basic' || devProperties.sessionCookie) {
|
|
77
|
+
setPhase('deploy');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (authMethod === 'cookie') {
|
|
81
|
+
// env/keychain cookie is already loaded in devProperties; none here.
|
|
82
|
+
setPhase('login');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (authMethod === 'oauth2') {
|
|
86
|
+
if (devProperties.accessToken) {
|
|
87
|
+
setPhase('deploy');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
void (async () => {
|
|
91
|
+
const token = await resolveOAuth2AccessToken(devProperties);
|
|
92
|
+
if (token) {
|
|
93
|
+
setCredential({ accessToken: token });
|
|
94
|
+
setPhase('deploy');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
setPhase('login');
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
setPhase('deploy');
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
}, []);
|
|
25
105
|
React.useEffect(() => {
|
|
106
|
+
if (phase !== 'deploy' || deployStartedRef.current)
|
|
107
|
+
return;
|
|
108
|
+
deployStartedRef.current = true;
|
|
26
109
|
async function runDeploy() {
|
|
27
110
|
try {
|
|
28
111
|
const appType = getAppType(manifest);
|
|
112
|
+
// Credential resolved in the init effect / login screen.
|
|
113
|
+
const { accessToken, sessionCookie } = credential;
|
|
114
|
+
// A stale session fails without a clean 401 — clear the stored cookie
|
|
115
|
+
// so the next run re-authenticates. Skip when SITEVISION_SESSION_COOKIE
|
|
116
|
+
// is set: detection re-reads it first, so clearing would just replay
|
|
117
|
+
// the same dead cookie in a loop.
|
|
118
|
+
const clearStaleCookie = (result) => {
|
|
119
|
+
if (result.authExpired &&
|
|
120
|
+
authMethod === 'cookie' &&
|
|
121
|
+
!process.env['SITEVISION_SESSION_COOKIE'] &&
|
|
122
|
+
devProperties.domain &&
|
|
123
|
+
devProperties.username) {
|
|
124
|
+
deleteSessionCookie(devProperties.domain, devProperties.username);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
29
127
|
if (production) {
|
|
30
128
|
// Production deployment requires a signed zip
|
|
31
129
|
const signedZipPath = getSignedZipPath(projectRoot, manifest);
|
|
@@ -42,11 +140,14 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
42
140
|
addonName: devProperties.addonName,
|
|
43
141
|
username: devProperties.username,
|
|
44
142
|
password: devProperties.password,
|
|
143
|
+
accessToken,
|
|
144
|
+
sessionCookie,
|
|
45
145
|
useHTTP: devProperties.useHTTPForDevDeploy,
|
|
46
146
|
activate,
|
|
47
147
|
};
|
|
48
148
|
const result = await deployProduction(signedZipPath, config, appType);
|
|
49
149
|
if (!result.success) {
|
|
150
|
+
clearStaleCookie(result);
|
|
50
151
|
setState({
|
|
51
152
|
status: 'error',
|
|
52
153
|
error: result.error || 'Deployment failed',
|
|
@@ -75,10 +176,13 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
75
176
|
addonName: devProperties.addonName,
|
|
76
177
|
username: devProperties.username,
|
|
77
178
|
password: devProperties.password,
|
|
179
|
+
accessToken,
|
|
180
|
+
sessionCookie,
|
|
78
181
|
useHTTP: devProperties.useHTTPForDevDeploy,
|
|
79
182
|
};
|
|
80
183
|
const result = await deployApp(zipPath, config, appType, force);
|
|
81
184
|
if (!result.success) {
|
|
185
|
+
clearStaleCookie(result);
|
|
82
186
|
setState({
|
|
83
187
|
status: 'error',
|
|
84
188
|
error: result.error || 'Deployment failed',
|
|
@@ -100,20 +204,30 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
100
204
|
}
|
|
101
205
|
}
|
|
102
206
|
runDeploy();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
devProperties
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
207
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
208
|
+
}, [phase]);
|
|
209
|
+
if (phase === 'login' && state.status !== 'error') {
|
|
210
|
+
return (_jsx(AuthLoginScreen, { method: (devProperties.authMethod ?? 'basic') === 'cookie'
|
|
211
|
+
? 'cookie'
|
|
212
|
+
: 'oauth2', devProperties: devProperties, onComplete: cred => {
|
|
213
|
+
setCredential(cred);
|
|
214
|
+
setPhase('deploy');
|
|
215
|
+
}, onError: message => {
|
|
216
|
+
setState({ status: 'error', error: message });
|
|
217
|
+
}, onCancel: () => {
|
|
218
|
+
if (onBack) {
|
|
219
|
+
onBack();
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
setState({ status: 'error', error: 'Login cancelled.' });
|
|
223
|
+
}
|
|
224
|
+
} }));
|
|
225
|
+
}
|
|
112
226
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: state.status === 'deploying' ? 'running' : state.status, label: state.status === 'deploying'
|
|
113
227
|
? 'Deploying'
|
|
114
228
|
: state.status === 'success'
|
|
115
229
|
? 'Deployed'
|
|
116
|
-
: 'Failed', message: state.message }) }), state.status === 'success' && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: [production ? 'Production deployment' : 'Dev deployment', " complete"] }), state.executableId && (_jsxs(Text, { dimColor: true, children: ["Executable ID: ", state.executableId] })), force && _jsx(Text, { dimColor: true, children: "(Force mode - overwrote existing)" }), activate && production && _jsx(Text, { dimColor: true, children: "(Activated)" })] })), state.status === 'error' && state.error && (
|
|
230
|
+
: 'Failed', message: state.message }) }), state.status === 'success' && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: [production ? 'Production deployment' : 'Dev deployment', " complete"] }), state.executableId && (_jsxs(Text, { dimColor: true, children: ["Executable ID: ", state.executableId] })), force && _jsx(Text, { dimColor: true, children: "(Force mode - overwrote existing)" }), activate && production && _jsx(Text, { dimColor: true, children: "(Activated)" })] })), state.status === 'error' && state.error && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "red", children: state.error }), canRelogin && (_jsx(Text, { color: "yellow", children: "This can happen when your session or token has expired \u2014 log in again to get fresh credentials." }))] })), state.status !== 'deploying' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && canRelogin && (_jsx(Text, { dimColor: true, children: "Press r to log in again with fresh credentials" })), state.status === 'error' && !canRelogin && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), state.status === 'error' && onChangeAuthMethod && (_jsx(Text, { dimColor: true, children: "Press m to change auth method" })), onBack && _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" })] }))] }));
|
|
117
231
|
}
|
|
118
232
|
export const deployCommand = {
|
|
119
233
|
name: 'deploy',
|
|
@@ -146,8 +260,11 @@ export const deployCommand = {
|
|
|
146
260
|
console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
|
|
147
261
|
return;
|
|
148
262
|
}
|
|
149
|
-
//
|
|
150
|
-
|
|
263
|
+
// Basic auth prompts for a password here; OAuth2 and cookie resolve or log
|
|
264
|
+
// in inside DeployScreen (Ink-native), so both the TUI and this command
|
|
265
|
+
// share one login path. env/--flag token/cookie are already loaded.
|
|
266
|
+
const authMethod = project.devProperties.authMethod ?? 'basic';
|
|
267
|
+
if (authMethod === 'basic' && !project.devProperties.password) {
|
|
151
268
|
const { domain, username } = project.devProperties;
|
|
152
269
|
console.log('');
|
|
153
270
|
const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
|
|
@@ -164,15 +281,8 @@ export const deployCommand = {
|
|
|
164
281
|
const production = Boolean(flags['production']);
|
|
165
282
|
const force = Boolean(flags['force']);
|
|
166
283
|
const activate = Boolean(flags['activate']);
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
if (production &&
|
|
170
|
-
project.hasSigningProperties &&
|
|
171
|
-
project.devProperties.signingUsername) {
|
|
172
|
-
// We already have a signed zip, no need to prompt for password here
|
|
173
|
-
// The sign command should have been run separately
|
|
174
|
-
}
|
|
175
|
-
const { waitUntilExit } = render(_jsx(DeployScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, force: force, production: production, activate: activate, signingPassword: signingPassword }));
|
|
284
|
+
// Production deploys use the already-signed zip; `sign` is run separately.
|
|
285
|
+
const { waitUntilExit } = render(_jsx(DeployScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, force: force, production: production, activate: activate }));
|
|
176
286
|
await waitUntilExit();
|
|
177
287
|
},
|
|
178
288
|
};
|