sitevision-cli 1.0.0-beta.9 → 1.0.0

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +194 -70
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +95 -212
  7. package/dist/commands/dev.js +87 -416
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +50 -33
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +5 -31
  13. package/dist/components/AnimatedLogo.js +8 -2
  14. package/dist/components/AuthLoginScreen.js +33 -15
  15. package/dist/components/PasswordInput.js +2 -1
  16. package/dist/scaffold/inquirer-stub.js +119 -0
  17. package/dist/scaffold/register.js +15 -0
  18. package/dist/shell/AddonPicker.js +54 -0
  19. package/dist/shell/Changelog.js +44 -0
  20. package/dist/shell/CommandPalette.js +63 -0
  21. package/dist/shell/ConfigForm.js +703 -0
  22. package/dist/shell/Frame.js +138 -0
  23. package/dist/shell/Help.js +96 -0
  24. package/dist/shell/Popover.js +15 -0
  25. package/dist/shell/Settings.js +149 -0
  26. package/dist/shell/Shell.js +953 -0
  27. package/dist/shell/Tabs.js +96 -0
  28. package/dist/shell/actions.js +493 -0
  29. package/dist/utils/config.js +62 -2
  30. package/dist/utils/create-app.js +231 -0
  31. package/dist/utils/debug.js +32 -0
  32. package/dist/utils/environments.js +74 -0
  33. package/dist/utils/i18n.js +369 -0
  34. package/dist/utils/jsonc.js +2 -1
  35. package/dist/utils/keychain.js +54 -6
  36. package/dist/utils/oauth2-auth.js +146 -43
  37. package/dist/utils/password-prompt.js +37 -3
  38. package/dist/utils/process-runner.js +77 -38
  39. package/dist/utils/project-detection.js +457 -112
  40. package/dist/utils/session-cookie-auth.js +10 -6
  41. package/dist/utils/sitevision-api.js +134 -25
  42. package/dist/utils/sitevision-scripts-runner.js +50 -22
  43. package/dist/utils/tasks.js +510 -0
  44. package/dist/utils/version-check.js +3 -3
  45. package/dist/utils/webpack-runner.js +9 -5
  46. package/dist/utils/workspace.js +105 -0
  47. package/dist/utils/zip.js +18 -19
  48. package/package.json +27 -4
  49. package/readme.md +113 -141
  50. package/dist/app.d.ts +0 -6
  51. package/dist/app.js +0 -307
  52. package/dist/cli.d.ts +0 -2
  53. package/dist/commands/build.d.ts +0 -12
  54. package/dist/commands/deploy.d.ts +0 -16
  55. package/dist/commands/dev.d.ts +0 -16
  56. package/dist/commands/index.d.ts +0 -4
  57. package/dist/commands/info.d.ts +0 -2
  58. package/dist/commands/setup-signing.d.ts +0 -2
  59. package/dist/commands/sign.d.ts +0 -14
  60. package/dist/commands/types.d.ts +0 -18
  61. package/dist/commands/watch.d.ts +0 -2
  62. package/dist/components/AnimatedLogo.d.ts +0 -10
  63. package/dist/components/AuthLoginScreen.d.ts +0 -21
  64. package/dist/components/DevPropertiesForm.d.ts +0 -10
  65. package/dist/components/DevPropertiesForm.js +0 -210
  66. package/dist/components/InfoScreen.d.ts +0 -7
  67. package/dist/components/InfoScreen.js +0 -14
  68. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  69. package/dist/components/KeychainPasswordChoice.js +0 -30
  70. package/dist/components/MainMenu.d.ts +0 -7
  71. package/dist/components/MainMenu.js +0 -83
  72. package/dist/components/PasswordInput.d.ts +0 -10
  73. package/dist/components/ProcessOutput.d.ts +0 -6
  74. package/dist/components/ProcessOutput.js +0 -30
  75. package/dist/components/SetupFlow.d.ts +0 -8
  76. package/dist/components/SetupFlow.js +0 -233
  77. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  78. package/dist/components/SigningPropertiesForm.js +0 -38
  79. package/dist/components/StatusIndicator.d.ts +0 -8
  80. package/dist/components/TextInput.d.ts +0 -10
  81. package/dist/components/TextInput.js +0 -29
  82. package/dist/components/WelcomeScreen.d.ts +0 -14
  83. package/dist/types/index.d.ts +0 -284
  84. package/dist/utils/branding.d.ts +0 -28
  85. package/dist/utils/config.d.ts +0 -17
  86. package/dist/utils/jsonc.d.ts +0 -19
  87. package/dist/utils/keychain.d.ts +0 -15
  88. package/dist/utils/oauth2-auth.d.ts +0 -27
  89. package/dist/utils/password-prompt.d.ts +0 -14
  90. package/dist/utils/process-runner.d.ts +0 -22
  91. package/dist/utils/project-detection.d.ts +0 -151
  92. package/dist/utils/session-cookie-auth.d.ts +0 -34
  93. package/dist/utils/signing-password.d.ts +0 -12
  94. package/dist/utils/signing-step.d.ts +0 -15
  95. package/dist/utils/signing-step.js +0 -16
  96. package/dist/utils/sitevision-api.d.ts +0 -112
  97. package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
  98. package/dist/utils/version-check.d.ts +0 -9
  99. package/dist/utils/webpack-runner.d.ts +0 -76
  100. package/dist/utils/zip.d.ts +0 -71
@@ -1,192 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { render, Box, Text, useInput } from 'ink';
4
- import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
6
- import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
7
- import { copyStaticToBuild, copySrcToBuild, cleanBuild, formatFileSize, createBuildZip, getZipSize, zipExists, } from '../utils/zip.js';
8
- import { isBundledApp, getAppType, getFullAppId, } from '../utils/project-detection.js';
9
- export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }) {
10
- const [state, setState] = React.useState({
11
- status: 'cleaning',
12
- message: 'Cleaning build directory...',
13
- });
14
- useInput((input, key) => {
15
- if (onBack &&
16
- (key.escape || input === 'q') &&
17
- (state.status === 'success' || state.status === 'error')) {
18
- onBack();
19
- }
20
- });
21
- const isBundled = isBundledApp(manifest);
22
- React.useEffect(() => {
23
- async function runBuild() {
24
- try {
25
- // Step 1: Clean build directory
26
- setState({ status: 'cleaning', message: 'Cleaning build directory...' });
27
- cleanBuild(projectRoot);
28
- // Step 2: Build or copy files
29
- if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
30
- // No project-local webpack config: delegate the whole build to
31
- // @sitevision/sitevision-scripts, which compiles + zips to
32
- // dist/<appId>.zip (the path the CLI's sign/deploy already use).
33
- if (!hasSitevisionScripts(projectRoot)) {
34
- setState({
35
- status: 'error',
36
- error: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
37
- });
38
- return;
39
- }
40
- // Warn if the installed sitevision-scripts is outside the range
41
- // the delegated build was validated against.
42
- const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
43
- setState({
44
- status: 'building',
45
- message: 'Building via sitevision-scripts...',
46
- warning,
47
- });
48
- const sitevisionResult = await runSitevisionScriptsBuild(projectRoot);
49
- if (!sitevisionResult.success) {
50
- setState({
51
- status: 'error',
52
- error: `${sitevisionResult.error}\n${sitevisionResult.output.slice(-1000)}`,
53
- warning,
54
- });
55
- return;
56
- }
57
- // sitevision-scripts already produced the zip; report it directly.
58
- const zipPath = getDelegatedZipPath(projectRoot, manifest.id);
59
- if (!zipExists(zipPath)) {
60
- setState({
61
- status: 'error',
62
- error: `Build reported success but no zip was found at ${zipPath}.`,
63
- warning,
64
- });
65
- return;
66
- }
67
- setState({
68
- status: 'success',
69
- message: 'Build complete',
70
- zipPath,
71
- zipSize: getZipSize(zipPath),
72
- warning,
73
- });
74
- return;
75
- }
76
- if (isBundled) {
77
- // Project ships its own webpack config: build it in-process.
78
- if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
79
- setState({
80
- status: 'error',
81
- error: 'webpack not found. Run npm install to install dependencies.',
82
- });
83
- return;
84
- }
85
- setState({ status: 'building', message: 'Compiling with webpack...' });
86
- const appType = getAppType(manifest);
87
- const runner = new WebpackRunner(projectRoot, {
88
- mode: 'production',
89
- cssPrefix: manifest.id,
90
- restApp: appType === 'rest',
91
- });
92
- const result = await runner.run();
93
- await runner.close();
94
- if (!result.success) {
95
- setState({
96
- status: 'error',
97
- error: result.errors?.join('\n') || 'Build failed',
98
- result,
99
- });
100
- return;
101
- }
102
- // Copy static files after webpack build
103
- setState({ status: 'copying', message: 'Copying static files...' });
104
- copyStaticToBuild(projectRoot);
105
- setState(prev => ({ ...prev, result }));
106
- }
107
- else {
108
- // Non-bundled app: just copy files
109
- setState({ status: 'copying', message: 'Copying source files...' });
110
- copySrcToBuild(projectRoot);
111
- copyStaticToBuild(projectRoot);
112
- }
113
- // Step 3: Create zip if requested
114
- if (createZip) {
115
- setState({ status: 'zipping', message: 'Creating zip archive...' });
116
- const appId = getFullAppId(manifest.id);
117
- const zipPath = await createBuildZip(projectRoot, appId);
118
- const zipSize = getZipSize(zipPath);
119
- setState(prev => ({
120
- ...prev,
121
- status: 'success',
122
- message: 'Build complete',
123
- zipPath,
124
- zipSize,
125
- }));
126
- }
127
- else {
128
- setState(prev => ({
129
- ...prev,
130
- status: 'success',
131
- message: 'Build complete',
132
- }));
133
- }
134
- }
135
- catch (error) {
136
- setState({
137
- status: 'error',
138
- error: error instanceof Error ? error.message : String(error),
139
- });
140
- }
141
- }
142
- runBuild();
143
- }, [projectRoot, manifest, isBundled, createZip]);
144
- const getStatusIndicator = () => {
145
- switch (state.status) {
146
- case 'cleaning':
147
- case 'building':
148
- case 'copying':
149
- case 'zipping':
150
- return 'running';
151
- case 'success':
152
- return 'success';
153
- case 'error':
154
- return 'error';
155
- }
156
- };
157
- const getStatusLabel = () => {
158
- switch (state.status) {
159
- case 'cleaning':
160
- return 'Cleaning';
161
- case 'building':
162
- return 'Building';
163
- case 'copying':
164
- return 'Copying files';
165
- case 'zipping':
166
- return 'Creating zip';
167
- case 'success':
168
- return 'Build complete';
169
- case 'error':
170
- return 'Build failed';
171
- }
172
- };
173
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
174
- state.result.stats.assets.length > 0 && (_jsxs(Text, { dimColor: true, children: ["Assets: ", state.result.stats.assets.join(', ')] }))] })), state.status === 'success' && state.zipPath && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 Created: ", state.zipPath] }), state.zipSize !== undefined && (_jsxs(Text, { dimColor: true, children: [" Size: ", formatFileSize(state.zipSize)] }))] })), state.result?.warnings && state.result.warnings.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "Warnings:" }), state.result.warnings.slice(0, 5).map((warning, i) => (_jsx(Text, { color: "yellow", dimColor: true, children: warning.substring(0, 200) }, i))), state.result.warnings.length > 5 && (_jsxs(Text, { color: "yellow", dimColor: true, children: ["...and ", state.result.warnings.length - 5, " more"] }))] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), onBack && (state.status === 'success' || state.status === 'error') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" }) }))] }));
175
- }
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { TaskScreen } from './TaskScreen.js';
4
+ import { startBuild } from '../utils/tasks.js';
176
5
  export const buildCommand = {
177
6
  name: 'build',
178
7
  description: 'Build the application for production',
179
8
  requiresProject: true,
180
- flags: {
181
- 'no-zip': {
182
- type: 'boolean',
183
- description: 'Skip creating the zip archive',
184
- default: false,
185
- },
186
- },
187
9
  async execute({ project, flags }) {
188
- const createZip = !flags['no-zip'];
189
- const { waitUntilExit } = render(_jsx(BuildScreen, { projectRoot: project.root, manifest: project.manifest, createZip: createZip }));
190
- await waitUntilExit();
10
+ const task = startBuild(project, { zip: flags['zip'] !== false });
11
+ await render(_jsx(TaskScreen, { task: task })).waitUntilExit();
191
12
  },
192
13
  };
@@ -1,55 +1,82 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { render, Box, Text, useInput } from 'ink';
2
+ import { useEffect, useState } from 'react';
3
+ import { render, Box, Text, useApp, useInput, useStdin } from 'ink';
4
+ import { TaskScreen } from './TaskScreen.js';
5
+ import { useFinish } from './use-finish.js';
6
+ import { resolveDeployPasswordForCli } from './dev.js';
4
7
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { deployApp, deployProduction } from '../utils/sitevision-api.js';
6
- import { getZipPath, getSignedZipPath, getAppType, } from '../utils/project-detection.js';
7
- import { zipExists } from '../utils/zip.js';
8
- import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
9
- import { setDeployPassword, deleteSessionCookie } from '../utils/keychain.js';
10
- import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
11
8
  import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
12
- export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, onBack, onRetryCredentials, }) {
13
- const [state, setState] = React.useState({
14
- status: 'deploying',
15
- message: production ? 'Deploying to production...' : 'Deploying to dev...',
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({
9
+ import { deleteSessionCookie, deleteOAuth2RefreshToken, } from '../utils/keychain.js';
10
+ import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
11
+ import { startDeploy, useTasks } from '../utils/tasks.js';
12
+ import { toDeployConfig } from '../utils/workspace.js';
13
+ import { say } from '../utils/debug.js';
14
+ /** Standalone `svc deploy`: log in if needed, then run the shell's deploy task. */
15
+ export function DeployScreen({ project, force, production, activate, }) {
16
+ const devProperties = project.devProperties;
17
+ const { exit } = useApp();
18
+ // Undefined, not false, on a pipe; and useInput reads undefined as active.
19
+ const interactive = Boolean(useStdin().isRawModeSupported);
20
+ // 'init' resolves cached credentials, 'login' shows the login screen,
21
+ // 'deploy' starts the task.
22
+ const [phase, setPhase] = useState('init');
23
+ const [credential, setCredential] = useState({
22
24
  accessToken: devProperties.accessToken,
23
25
  sessionCookie: devProperties.sessionCookie,
24
26
  });
25
- const deployStartedRef = React.useRef(false);
26
- useInput((input, key) => {
27
- if (state.status !== 'deploying') {
28
- if (onBack && (key.escape || input === 'q')) {
29
- onBack();
30
- }
31
- if (onRetryCredentials && state.status === 'error' && input === 'r') {
32
- onRetryCredentials();
33
- }
27
+ const [task, setTask] = useState();
28
+ // Anything that goes wrong before there is a task to show it.
29
+ const [failure, setFailure] = useState();
30
+ const authMethod = devProperties.authMethod ?? 'basic';
31
+ // OAuth2 and cookie can re-authenticate in place, given a terminal to do it in.
32
+ const canRelogin = interactive && (authMethod === 'oauth2' || authMethod === 'cookie');
33
+ const live = useTasks().find(t => t.id === task?.id) ?? task;
34
+ const failed = Boolean(failure) || live?.status === 'error';
35
+ useFinish(failure && !task ? 'error' : undefined, canRelogin);
36
+ // A browser login needs a terminal; without one only env credentials work.
37
+ const startLogin = () => {
38
+ if (interactive) {
39
+ setPhase('login');
34
40
  }
35
- });
36
- // Decide once whether we can deploy straight away or must log in first.
37
- React.useEffect(() => {
38
- const authMethod = devProperties.authMethod ?? 'basic';
39
- if (authMethod === 'basic' || devProperties.sessionCookie) {
40
- setPhase('deploy');
41
- return;
41
+ else {
42
+ setFailure('No stored login and no terminal to log in from. Set SITEVISION_ACCESS_TOKEN or SITEVISION_SESSION_COOKIE.');
42
43
  }
44
+ };
45
+ // Drop the stored credential and log in again: the usual fix for an expired
46
+ // session or token, which Sitevision reports as a 400 rather than a 401.
47
+ const retryWithFreshLogin = () => {
48
+ const { domain, username } = devProperties;
43
49
  if (authMethod === 'cookie') {
44
- // env/keychain cookie is already loaded in devProperties; none here.
45
- setPhase('login');
50
+ deleteSessionCookie(domain, username);
51
+ }
52
+ else if (devProperties.oauth2?.clientId) {
53
+ deleteOAuth2RefreshToken(domain, devProperties.oauth2.clientId);
54
+ }
55
+ setCredential({});
56
+ setFailure(undefined);
57
+ setTask(undefined);
58
+ setPhase('login');
59
+ };
60
+ useInput((input, key) => {
61
+ if (!failed)
46
62
  return;
63
+ if (input === 'r')
64
+ retryWithFreshLogin();
65
+ if (key.escape || input === 'q')
66
+ exit();
67
+ }, { isActive: canRelogin });
68
+ // Decide once whether to deploy straight away or log in first.
69
+ useEffect(() => {
70
+ if (authMethod === 'basic' || credential.sessionCookie) {
71
+ setPhase('deploy');
47
72
  }
48
- if (authMethod === 'oauth2') {
49
- if (devProperties.accessToken) {
50
- setPhase('deploy');
51
- return;
52
- }
73
+ else if (authMethod === 'cookie') {
74
+ startLogin();
75
+ }
76
+ else if (credential.accessToken) {
77
+ setPhase('deploy');
78
+ }
79
+ else {
53
80
  void (async () => {
54
81
  const token = await resolveOAuth2AccessToken(devProperties);
55
82
  if (token) {
@@ -57,196 +84,52 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
57
84
  setPhase('deploy');
58
85
  }
59
86
  else {
60
- setPhase('login');
87
+ startLogin();
61
88
  }
62
89
  })();
63
- return;
64
90
  }
65
- setPhase('deploy');
66
91
  // eslint-disable-next-line react-hooks/exhaustive-deps
67
92
  }, []);
68
- React.useEffect(() => {
69
- if (phase !== 'deploy' || deployStartedRef.current)
93
+ useEffect(() => {
94
+ if (phase !== 'deploy' || task)
95
+ return;
96
+ const complete = toDeployConfig({ ...devProperties, ...credential });
97
+ if ('error' in complete) {
98
+ setFailure(complete.error);
70
99
  return;
71
- deployStartedRef.current = true;
72
- async function runDeploy() {
73
- try {
74
- const appType = getAppType(manifest);
75
- const authMethod = devProperties.authMethod ?? 'basic';
76
- // Credential resolved in the init effect / login screen.
77
- const { accessToken, sessionCookie } = credential;
78
- // A stale session fails without a clean 401 — clear the stored cookie
79
- // so the next run re-authenticates. Skip when SITEVISION_SESSION_COOKIE
80
- // is set: detection re-reads it first, so clearing would just replay
81
- // the same dead cookie in a loop.
82
- const clearStaleCookie = (result) => {
83
- if (result.authExpired &&
84
- authMethod === 'cookie' &&
85
- !process.env['SITEVISION_SESSION_COOKIE'] &&
86
- devProperties.domain &&
87
- devProperties.username) {
88
- deleteSessionCookie(devProperties.domain, devProperties.username);
89
- }
90
- };
91
- if (production) {
92
- // Production deployment requires a signed zip
93
- const signedZipPath = getSignedZipPath(projectRoot, manifest);
94
- if (!zipExists(signedZipPath)) {
95
- setState({
96
- status: 'error',
97
- error: `Signed zip not found: ${signedZipPath}\nRun 'sign' first to create the signed zip.`,
98
- });
99
- return;
100
- }
101
- const config = {
102
- domain: devProperties.domain,
103
- siteName: devProperties.siteName,
104
- addonName: devProperties.addonName,
105
- username: devProperties.username,
106
- password: devProperties.password,
107
- accessToken,
108
- sessionCookie,
109
- useHTTP: devProperties.useHTTPForDevDeploy,
110
- activate,
111
- };
112
- const result = await deployProduction(signedZipPath, config, appType);
113
- if (!result.success) {
114
- clearStaleCookie(result);
115
- setState({
116
- status: 'error',
117
- error: result.error || 'Deployment failed',
118
- });
119
- return;
120
- }
121
- setState({
122
- status: 'success',
123
- message: result.message || 'Deployed to production successfully',
124
- executableId: result.executableId,
125
- });
126
- }
127
- else {
128
- // Dev deployment can use unsigned zip
129
- const zipPath = getZipPath(projectRoot, manifest);
130
- if (!zipExists(zipPath)) {
131
- setState({
132
- status: 'error',
133
- error: `Zip not found: ${zipPath}\nRun 'build' first to create the zip.`,
134
- });
135
- return;
136
- }
137
- const config = {
138
- domain: devProperties.domain,
139
- siteName: devProperties.siteName,
140
- addonName: devProperties.addonName,
141
- username: devProperties.username,
142
- password: devProperties.password,
143
- accessToken,
144
- sessionCookie,
145
- useHTTP: devProperties.useHTTPForDevDeploy,
146
- };
147
- const result = await deployApp(zipPath, config, appType, force);
148
- if (!result.success) {
149
- clearStaleCookie(result);
150
- setState({
151
- status: 'error',
152
- error: result.error || 'Deployment failed',
153
- });
154
- return;
155
- }
156
- setState({
157
- status: 'success',
158
- message: 'Deployed to dev successfully',
159
- executableId: result.executableId,
160
- });
161
- }
162
- }
163
- catch (error) {
164
- setState({
165
- status: 'error',
166
- error: error instanceof Error ? error.message : String(error),
167
- });
168
- }
169
100
  }
170
- runDeploy();
101
+ setTask(startDeploy(project, complete.config, { force, production, activate }));
171
102
  // eslint-disable-next-line react-hooks/exhaustive-deps
172
- }, [phase]);
173
- if (phase === 'login' && state.status !== 'error') {
174
- return (_jsx(AuthLoginScreen, { method: (devProperties.authMethod ?? 'basic') === 'cookie'
175
- ? 'cookie'
176
- : 'oauth2', devProperties: devProperties, onComplete: cred => {
177
- setCredential(cred);
103
+ }, [phase, task]);
104
+ if (phase === 'login' && !failure) {
105
+ return (_jsx(AuthLoginScreen, { method: authMethod === 'cookie' ? 'cookie' : 'oauth2', devProperties: devProperties, onComplete: next => {
106
+ setCredential(next);
178
107
  setPhase('deploy');
179
- }, onError: message => {
180
- setState({ status: 'error', error: message });
181
- }, onCancel: () => {
182
- if (onBack) {
183
- onBack();
184
- }
185
- else {
186
- setState({ status: 'error', error: 'Login cancelled.' });
187
- }
108
+ }, onError: setFailure, onCancel: () => {
109
+ setFailure('Login cancelled.');
188
110
  } }));
189
111
  }
190
- 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'
191
- ? 'Deploying'
192
- : state.status === 'success'
193
- ? 'Deployed'
194
- : '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 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), state.status !== 'deploying' && (_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" })] }))] }));
112
+ const retryHint = failed && canRelogin && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "This can happen when your session or token has expired." }), _jsx(Text, { dimColor: true, children: "Press r to log in again with fresh credentials, q to quit" })] }));
113
+ if (task) {
114
+ return (_jsx(TaskScreen, { task: task, stay: canRelogin, children: retryHint }));
115
+ }
116
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(StatusIndicator, { status: failure ? 'error' : 'running', label: failure ? 'Failed' : 'Preparing', message: failure }), retryHint] }));
195
117
  }
196
118
  export const deployCommand = {
197
119
  name: 'deploy',
198
120
  description: 'Deploy the application',
199
121
  requiresProject: true,
200
- flags: {
201
- force: {
202
- type: 'boolean',
203
- description: 'Force deployment (overwrite existing)',
204
- alias: 'f',
205
- default: false,
206
- },
207
- production: {
208
- type: 'boolean',
209
- description: 'Deploy to production (requires signed app)',
210
- alias: 'p',
211
- default: false,
212
- },
213
- activate: {
214
- type: 'boolean',
215
- description: 'Activate the app after production deployment',
216
- alias: 'a',
217
- default: false,
218
- },
219
- },
220
122
  async execute({ project, flags }) {
221
- // Check if dev properties are configured
222
- if (!project.hasDevProperties || !project.devProperties) {
223
- console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
224
- console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
123
+ const complete = toDeployConfig(project.devProperties);
124
+ if ('error' in complete) {
125
+ say(`\n\x1b[33m${complete.error}\x1b[0m\n`);
126
+ process.exitCode = 1;
225
127
  return;
226
128
  }
227
- // Basic auth prompts for a password here; OAuth2 and cookie resolve or log
228
- // in inside DeployScreen (Ink-native), so both the TUI and this command
229
- // share one login path. env/--flag token/cookie are already loaded.
230
- const authMethod = project.devProperties.authMethod ?? 'basic';
231
- if (authMethod === 'basic' && !project.devProperties.password) {
232
- const { domain, username } = project.devProperties;
233
- console.log('');
234
- const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
235
- if (!password) {
236
- console.log('\x1b[31mError: Password is required\x1b[0m');
237
- return;
238
- }
239
- const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
240
- if (remember && domain && username) {
241
- setDeployPassword(domain, username, password);
242
- }
243
- project.devProperties.password = password;
244
- }
245
- const production = Boolean(flags['production']);
246
- const force = Boolean(flags['force']);
247
- const activate = Boolean(flags['activate']);
129
+ // OAuth2 and cookie log in inside DeployScreen; basic asks here.
130
+ if (!(await resolveDeployPasswordForCli(project.devProperties)))
131
+ return;
248
132
  // Production deploys use the already-signed zip; `sign` is run separately.
249
- const { waitUntilExit } = render(_jsx(DeployScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, force: force, production: production, activate: activate }));
250
- await waitUntilExit();
133
+ await render(_jsx(DeployScreen, { project: project, force: Boolean(flags['force']), production: Boolean(flags['production']), activate: Boolean(flags['activate']) })).waitUntilExit();
251
134
  },
252
135
  };