sitevision-cli 1.0.0-beta.31 → 1.0.0-beta.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +73 -34
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +85 -238
  7. package/dist/commands/dev.js +34 -37
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +43 -28
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +0 -8
  13. package/dist/components/AuthLoginScreen.js +27 -12
  14. package/dist/scaffold/inquirer-stub.js +119 -0
  15. package/dist/scaffold/register.js +15 -0
  16. package/dist/shell/ConfigForm.js +174 -29
  17. package/dist/shell/Frame.js +4 -1
  18. package/dist/shell/Help.js +2 -1
  19. package/dist/shell/Settings.js +59 -6
  20. package/dist/shell/Shell.js +361 -43
  21. package/dist/shell/Tabs.js +8 -2
  22. package/dist/shell/actions.js +0 -0
  23. package/dist/utils/config.js +46 -3
  24. package/dist/utils/create-app.js +226 -0
  25. package/dist/utils/i18n.js +68 -13
  26. package/dist/utils/jsonc.js +2 -1
  27. package/dist/utils/keychain.js +44 -4
  28. package/dist/utils/oauth2-auth.js +35 -7
  29. package/dist/utils/password-prompt.js +23 -3
  30. package/dist/utils/process-runner.js +8 -3
  31. package/dist/utils/project-detection.js +172 -44
  32. package/dist/utils/session-cookie-auth.js +5 -4
  33. package/dist/utils/sitevision-api.js +21 -14
  34. package/dist/utils/sitevision-scripts-runner.js +32 -15
  35. package/dist/utils/tasks.js +132 -66
  36. package/dist/utils/version-check.js +3 -3
  37. package/dist/utils/webpack-runner.js +6 -5
  38. package/dist/utils/workspace.js +41 -3
  39. package/dist/utils/zip.js +18 -19
  40. package/package.json +24 -3
  41. package/readme.md +11 -4
  42. package/dist/app.d.ts +0 -6
  43. package/dist/app.js +0 -321
  44. package/dist/cli.d.ts +0 -2
  45. package/dist/commands/build.d.ts +0 -12
  46. package/dist/commands/deploy.d.ts +0 -17
  47. package/dist/commands/dev.d.ts +0 -14
  48. package/dist/commands/index.d.ts +0 -4
  49. package/dist/commands/info.d.ts +0 -2
  50. package/dist/commands/setup-signing.d.ts +0 -2
  51. package/dist/commands/sign.d.ts +0 -14
  52. package/dist/commands/types.d.ts +0 -18
  53. package/dist/commands/watch.d.ts +0 -2
  54. package/dist/components/AnimatedLogo.d.ts +0 -10
  55. package/dist/components/AuthLoginScreen.d.ts +0 -21
  56. package/dist/components/DevPropertiesForm.d.ts +0 -11
  57. package/dist/components/DevPropertiesForm.js +0 -246
  58. package/dist/components/InfoScreen.d.ts +0 -7
  59. package/dist/components/InfoScreen.js +0 -14
  60. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  61. package/dist/components/KeychainPasswordChoice.js +0 -30
  62. package/dist/components/MainMenu.d.ts +0 -7
  63. package/dist/components/MainMenu.js +0 -88
  64. package/dist/components/PasswordInput.d.ts +0 -10
  65. package/dist/components/ProcessOutput.d.ts +0 -6
  66. package/dist/components/ProcessOutput.js +0 -30
  67. package/dist/components/SetupFlow.d.ts +0 -8
  68. package/dist/components/SetupFlow.js +0 -233
  69. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  70. package/dist/components/SigningPropertiesForm.js +0 -38
  71. package/dist/components/StatusIndicator.d.ts +0 -8
  72. package/dist/components/TextInput.d.ts +0 -10
  73. package/dist/components/TextInput.js +0 -29
  74. package/dist/components/WelcomeScreen.d.ts +0 -14
  75. package/dist/shell/AddonPicker.d.ts +0 -14
  76. package/dist/shell/Changelog.d.ts +0 -10
  77. package/dist/shell/CommandPalette.d.ts +0 -12
  78. package/dist/shell/ConfigForm.d.ts +0 -36
  79. package/dist/shell/Frame.d.ts +0 -59
  80. package/dist/shell/Help.d.ts +0 -9
  81. package/dist/shell/Popover.d.ts +0 -12
  82. package/dist/shell/Settings.d.ts +0 -6
  83. package/dist/shell/Shell.d.ts +0 -10
  84. package/dist/shell/Tabs.d.ts +0 -36
  85. package/dist/shell/actions.d.ts +0 -48
  86. package/dist/types/index.d.ts +0 -295
  87. package/dist/utils/branding.d.ts +0 -28
  88. package/dist/utils/config.d.ts +0 -27
  89. package/dist/utils/environments.d.ts +0 -20
  90. package/dist/utils/i18n.d.ts +0 -12
  91. package/dist/utils/jsonc.d.ts +0 -19
  92. package/dist/utils/keychain.d.ts +0 -15
  93. package/dist/utils/oauth2-auth.d.ts +0 -64
  94. package/dist/utils/password-prompt.d.ts +0 -14
  95. package/dist/utils/process-runner.d.ts +0 -31
  96. package/dist/utils/project-detection.d.ts +0 -202
  97. package/dist/utils/session-cookie-auth.d.ts +0 -35
  98. package/dist/utils/signing-password.d.ts +0 -12
  99. package/dist/utils/signing-step.d.ts +0 -15
  100. package/dist/utils/signing-step.js +0 -16
  101. package/dist/utils/sitevision-api.d.ts +0 -155
  102. package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
  103. package/dist/utils/tasks.d.ts +0 -50
  104. package/dist/utils/version-check.d.ts +0 -9
  105. package/dist/utils/webpack-runner.d.ts +0 -76
  106. package/dist/utils/workspace.d.ts +0 -17
  107. package/dist/utils/zip.d.ts +0 -71
@@ -1,92 +1,81 @@
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 { getDeployZipPath, 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, deleteOAuth2RefreshToken, } 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, onChangeAuthMethod, }) {
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
+ /** Standalone `svc deploy`: log in if needed, then run the shell's deploy task. */
14
+ export function DeployScreen({ project, force, production, activate, }) {
15
+ const devProperties = project.devProperties;
16
+ const { exit } = useApp();
17
+ // Undefined, not false, on a pipe; and useInput reads undefined as active.
18
+ const interactive = Boolean(useStdin().isRawModeSupported);
19
+ // 'init' resolves cached credentials, 'login' shows the login screen,
20
+ // 'deploy' starts the task.
21
+ const [phase, setPhase] = useState('init');
22
+ const [credential, setCredential] = useState({
22
23
  accessToken: devProperties.accessToken,
23
24
  sessionCookie: devProperties.sessionCookie,
24
25
  });
25
- const deployStartedRef = React.useRef(false);
26
+ const [task, setTask] = useState();
27
+ // Anything that goes wrong before there is a task to show it.
28
+ const [failure, setFailure] = useState();
26
29
  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).
30
+ // OAuth2 and cookie can re-authenticate in place, given a terminal to do it in.
31
+ const canRelogin = interactive && (authMethod === 'oauth2' || authMethod === 'cookie');
32
+ const live = useTasks().find(t => t.id === task?.id) ?? task;
33
+ const failed = Boolean(failure) || live?.status === 'error';
34
+ useFinish(failure && !task ? 'error' : undefined, canRelogin);
35
+ // A browser login needs a terminal; without one only env credentials work.
36
+ const startLogin = () => {
37
+ if (interactive) {
38
+ setPhase('login');
39
+ }
40
+ else {
41
+ setFailure('No stored login and no terminal to log in from. Set SITEVISION_ACCESS_TOKEN or SITEVISION_SESSION_COOKIE.');
42
+ }
43
+ };
44
+ // Drop the stored credential and log in again: the usual fix for an expired
45
+ // session or token, which Sitevision reports as a 400 rather than a 401.
34
46
  const retryWithFreshLogin = () => {
35
47
  const { domain, username } = devProperties;
36
- if (authMethod === 'cookie' && domain && username) {
48
+ if (authMethod === 'cookie') {
37
49
  deleteSessionCookie(domain, username);
38
- devProperties.sessionCookie = undefined;
39
50
  }
40
- else if (authMethod === 'oauth2' &&
41
- domain &&
42
- devProperties.oauth2?.clientId) {
51
+ else if (devProperties.oauth2?.clientId) {
43
52
  deleteOAuth2RefreshToken(domain, devProperties.oauth2.clientId);
44
- devProperties.accessToken = undefined;
45
53
  }
46
54
  setCredential({});
47
- deployStartedRef.current = false;
48
- setState({
49
- status: 'deploying',
50
- message: production
51
- ? 'Deploying to production...'
52
- : 'Deploying to dev...',
53
- });
55
+ setFailure(undefined);
56
+ setTask(undefined);
54
57
  setPhase('login');
55
58
  };
56
59
  useInput((input, key) => {
57
- if (state.status !== 'deploying') {
58
- if (onBack && (key.escape || input === 'q')) {
59
- onBack();
60
- }
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();
71
- }
72
- }
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');
60
+ if (!failed)
78
61
  return;
62
+ if (input === 'r')
63
+ retryWithFreshLogin();
64
+ if (key.escape || input === 'q')
65
+ exit();
66
+ }, { isActive: canRelogin });
67
+ // Decide once whether to deploy straight away or log in first.
68
+ useEffect(() => {
69
+ if (authMethod === 'basic' || credential.sessionCookie) {
70
+ setPhase('deploy');
79
71
  }
80
- if (authMethod === 'cookie') {
81
- // env/keychain cookie is already loaded in devProperties; none here.
82
- setPhase('login');
83
- return;
72
+ else if (authMethod === 'cookie') {
73
+ startLogin();
84
74
  }
85
- if (authMethod === 'oauth2') {
86
- if (devProperties.accessToken) {
87
- setPhase('deploy');
88
- return;
89
- }
75
+ else if (credential.accessToken) {
76
+ setPhase('deploy');
77
+ }
78
+ else {
90
79
  void (async () => {
91
80
  const token = await resolveOAuth2AccessToken(devProperties);
92
81
  if (token) {
@@ -94,194 +83,52 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
94
83
  setPhase('deploy');
95
84
  }
96
85
  else {
97
- setPhase('login');
86
+ startLogin();
98
87
  }
99
88
  })();
100
- return;
101
89
  }
102
- setPhase('deploy');
103
90
  // eslint-disable-next-line react-hooks/exhaustive-deps
104
91
  }, []);
105
- React.useEffect(() => {
106
- if (phase !== 'deploy' || deployStartedRef.current)
92
+ useEffect(() => {
93
+ if (phase !== 'deploy' || task)
94
+ return;
95
+ const complete = toDeployConfig({ ...devProperties, ...credential });
96
+ if ('error' in complete) {
97
+ setFailure(complete.error);
107
98
  return;
108
- deployStartedRef.current = true;
109
- async function runDeploy() {
110
- try {
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
- };
127
- if (production) {
128
- // Production deployment requires a signed zip
129
- const signedZipPath = getSignedZipPath(projectRoot, manifest);
130
- if (!zipExists(signedZipPath)) {
131
- setState({
132
- status: 'error',
133
- error: `Signed zip not found: ${signedZipPath}\nRun 'sign' first to create the signed 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
- activate,
147
- };
148
- const result = await deployProduction(signedZipPath, config, appType);
149
- if (!result.success) {
150
- clearStaleCookie(result);
151
- setState({
152
- status: 'error',
153
- error: result.error || 'Deployment failed',
154
- });
155
- return;
156
- }
157
- setState({
158
- status: 'success',
159
- message: result.message || 'Deployed to production successfully',
160
- executableId: result.executableId,
161
- });
162
- }
163
- else {
164
- const zipPath = getDeployZipPath(projectRoot, manifest);
165
- if (!zipExists(zipPath)) {
166
- setState({
167
- status: 'error',
168
- error: `Zip not found: ${zipPath}\nRun 'build' first to create the zip.`,
169
- });
170
- return;
171
- }
172
- const config = {
173
- domain: devProperties.domain,
174
- siteName: devProperties.siteName,
175
- addonName: devProperties.addonName,
176
- username: devProperties.username,
177
- password: devProperties.password,
178
- accessToken,
179
- sessionCookie,
180
- useHTTP: devProperties.useHTTPForDevDeploy,
181
- };
182
- const result = await deployApp(zipPath, config, appType, force);
183
- if (!result.success) {
184
- clearStaleCookie(result);
185
- setState({
186
- status: 'error',
187
- error: result.error || 'Deployment failed',
188
- });
189
- return;
190
- }
191
- setState({
192
- status: 'success',
193
- message: 'Deployed to dev successfully',
194
- executableId: result.executableId,
195
- });
196
- }
197
- }
198
- catch (error) {
199
- setState({
200
- status: 'error',
201
- error: error instanceof Error ? error.message : String(error),
202
- });
203
- }
204
99
  }
205
- runDeploy();
100
+ setTask(startDeploy(project, complete.config, { force, production, activate }));
206
101
  // eslint-disable-next-line react-hooks/exhaustive-deps
207
- }, [phase]);
208
- if (phase === 'login' && state.status !== 'error') {
209
- return (_jsx(AuthLoginScreen, { method: (devProperties.authMethod ?? 'basic') === 'cookie'
210
- ? 'cookie'
211
- : 'oauth2', devProperties: devProperties, onComplete: cred => {
212
- setCredential(cred);
102
+ }, [phase, task]);
103
+ if (phase === 'login' && !failure) {
104
+ return (_jsx(AuthLoginScreen, { method: authMethod === 'cookie' ? 'cookie' : 'oauth2', devProperties: devProperties, onComplete: next => {
105
+ setCredential(next);
213
106
  setPhase('deploy');
214
- }, onError: message => {
215
- setState({ status: 'error', error: message });
216
- }, onCancel: () => {
217
- if (onBack) {
218
- onBack();
219
- }
220
- else {
221
- setState({ status: 'error', error: 'Login cancelled.' });
222
- }
107
+ }, onError: setFailure, onCancel: () => {
108
+ setFailure('Login cancelled.');
223
109
  } }));
224
110
  }
225
- 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'
226
- ? 'Deploying'
227
- : state.status === 'success'
228
- ? 'Deployed'
229
- : '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" })] }))] }));
111
+ 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" })] }));
112
+ if (task) {
113
+ return (_jsx(TaskScreen, { task: task, stay: canRelogin, children: retryHint }));
114
+ }
115
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(StatusIndicator, { status: failure ? 'error' : 'running', label: failure ? 'Failed' : 'Preparing', message: failure }), retryHint] }));
230
116
  }
231
117
  export const deployCommand = {
232
118
  name: 'deploy',
233
119
  description: 'Deploy the application',
234
120
  requiresProject: true,
235
- flags: {
236
- force: {
237
- type: 'boolean',
238
- description: 'Force deployment (overwrite existing)',
239
- alias: 'f',
240
- default: false,
241
- },
242
- production: {
243
- type: 'boolean',
244
- description: 'Deploy to production (requires signed app)',
245
- alias: 'p',
246
- default: false,
247
- },
248
- activate: {
249
- type: 'boolean',
250
- description: 'Activate the app after production deployment',
251
- alias: 'a',
252
- default: false,
253
- },
254
- },
255
121
  async execute({ project, flags }) {
256
- // Check if dev properties are configured
257
- if (!project.hasDevProperties || !project.devProperties) {
258
- console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
259
- console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
122
+ const complete = toDeployConfig(project.devProperties);
123
+ if ('error' in complete) {
124
+ console.log(`\n\x1b[33m${complete.error}\x1b[0m\n`);
125
+ process.exitCode = 1;
260
126
  return;
261
127
  }
262
- // Basic auth prompts for a password here; OAuth2 and cookie resolve or log
263
- // in inside DeployScreen (Ink-native), so both the TUI and this command
264
- // share one login path. env/--flag token/cookie are already loaded.
265
- const authMethod = project.devProperties.authMethod ?? 'basic';
266
- if (authMethod === 'basic' && !project.devProperties.password) {
267
- const { domain, username } = project.devProperties;
268
- console.log('');
269
- const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
270
- if (!password) {
271
- console.log('\x1b[31mError: Password is required\x1b[0m');
272
- return;
273
- }
274
- const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
275
- if (remember && domain && username) {
276
- setDeployPassword(domain, username, password);
277
- }
278
- project.devProperties.password = password;
279
- }
280
- const production = Boolean(flags['production']);
281
- const force = Boolean(flags['force']);
282
- const activate = Boolean(flags['activate']);
128
+ // OAuth2 and cookie log in inside DeployScreen; basic asks here.
129
+ if (!(await resolveDeployPasswordForCli(project.devProperties)))
130
+ return;
283
131
  // Production deploys use the already-signed zip; `sign` is run separately.
284
- const { waitUntilExit } = render(_jsx(DeployScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, force: force, production: production, activate: activate }));
285
- await waitUntilExit();
132
+ await render(_jsx(DeployScreen, { project: project, force: Boolean(flags['force']), production: Boolean(flags['production']), activate: Boolean(flags['activate']) })).waitUntilExit();
286
133
  },
287
134
  };
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
3
+ import { render, Box, Text, useApp, useInput, useStdin, useStdout } from 'ink';
4
4
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
5
  import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
6
6
  import { setDeployPassword } from '../utils/keychain.js';
7
7
  import { resolveSigningPassword } from '../utils/signing-password.js';
8
8
  import { startDev, useTasks } from '../utils/tasks.js';
9
+ import { toDeployConfig } from '../utils/workspace.js';
9
10
  import { Log } from '../shell/Tabs.js';
10
11
  /** Standalone `svc dev` / `svc watch`: one task, its log, Ctrl+C or q to stop. */
11
12
  export function DevScreen({ project, options }) {
@@ -15,12 +16,15 @@ export function DevScreen({ project, options }) {
15
16
  // eslint-disable-next-line react-hooks/exhaustive-deps
16
17
  const task = React.useMemo(() => startDev(project, options), []);
17
18
  const live = tasks.find(t => t.id === task.id) ?? task;
19
+ const { isRawModeSupported } = useStdin();
18
20
  useInput((input, key) => {
19
21
  if (key.escape || input === 'q') {
20
22
  live.stop();
21
23
  exit();
22
24
  }
23
- });
25
+ },
26
+ // Undefined, not false, on a pipe; and useInput reads undefined as active.
27
+ { isActive: Boolean(isRawModeSupported) });
24
28
  const status = live.status === 'running'
25
29
  ? live.phase === 'error'
26
30
  ? 'error'
@@ -36,12 +40,14 @@ export async function resolveSigningForCli(project) {
36
40
  !project.devProperties?.signingUsername) {
37
41
  console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
38
42
  console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
43
+ process.exitCode = 1;
39
44
  return undefined;
40
45
  }
41
46
  const signingUsername = project.devProperties.signingUsername;
42
47
  const password = await resolveSigningPassword(signingUsername);
43
48
  if (!password) {
44
49
  console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
50
+ process.exitCode = 1;
45
51
  return undefined;
46
52
  }
47
53
  return {
@@ -50,23 +56,34 @@ export async function resolveSigningForCli(project) {
50
56
  certificateName: project.devProperties.certificateName,
51
57
  };
52
58
  }
59
+ /** Basic auth without a stored password: ask for it. False when there is none. */
60
+ export async function resolveDeployPasswordForCli(dev) {
61
+ if ((dev.authMethod ?? 'basic') !== 'basic' || dev.password)
62
+ return true;
63
+ const { domain, username } = dev;
64
+ console.log('');
65
+ const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
66
+ if (!password) {
67
+ console.log('\x1b[31mError: Password is required\x1b[0m');
68
+ process.exitCode = 1;
69
+ return false;
70
+ }
71
+ if (await promptYesNo('Save password to OS keychain? (y/N): ')) {
72
+ setDeployPassword(domain, username, password);
73
+ }
74
+ dev.password = password;
75
+ return true;
76
+ }
53
77
  export const devCommand = {
54
78
  name: 'dev',
55
79
  description: 'Start development server with watch mode',
56
80
  requiresProject: true,
57
- flags: {
58
- signed: {
59
- type: 'boolean',
60
- description: 'Use signed mode (sign before each deploy)',
61
- alias: 's',
62
- default: false,
63
- },
64
- },
65
81
  async execute({ project, flags }) {
66
82
  const dev = project.devProperties;
67
- if (!project.hasDevProperties || !dev) {
68
- console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
69
- console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
83
+ const incomplete = toDeployConfig(dev);
84
+ if (!dev || 'error' in incomplete) {
85
+ console.log(`\n\x1b[33m${'error' in incomplete ? incomplete.error : ''}\x1b[0m\n`);
86
+ process.exitCode = 1;
70
87
  return;
71
88
  }
72
89
  // The standalone command only prompts for a basic password; OAuth2/cookie
@@ -75,22 +92,11 @@ export const devCommand = {
75
92
  !dev.accessToken &&
76
93
  !dev.sessionCookie) {
77
94
  console.log('\n\x1b[33mNo token/cookie available. Run `svc` and use Dev from the shell, or set SITEVISION_ACCESS_TOKEN / SITEVISION_SESSION_COOKIE.\x1b[0m\n');
95
+ process.exitCode = 1;
78
96
  return;
79
97
  }
80
- if ((dev.authMethod ?? 'basic') === 'basic' && !dev.password) {
81
- const { domain, username } = dev;
82
- console.log('');
83
- const pw = await promptPassword(`Deploy password for ${username}@${domain}: `);
84
- if (!pw) {
85
- console.log('\x1b[31mError: Password is required\x1b[0m');
86
- return;
87
- }
88
- const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
89
- if (remember && domain && username) {
90
- setDeployPassword(domain, username, pw);
91
- }
92
- dev.password = pw;
93
- }
98
+ if (!(await resolveDeployPasswordForCli(dev)))
99
+ return;
94
100
  let signingCredentials;
95
101
  if (flags['signed']) {
96
102
  signingCredentials = await resolveSigningForCli(project);
@@ -100,16 +106,7 @@ export const devCommand = {
100
106
  const { waitUntilExit } = render(_jsx(DevScreen, { project: project, options: {
101
107
  deploy: true,
102
108
  signingCredentials,
103
- deployConfig: {
104
- domain: dev.domain,
105
- siteName: dev.siteName,
106
- addonName: dev.addonName,
107
- username: dev.username,
108
- password: dev.password,
109
- accessToken: dev.accessToken,
110
- sessionCookie: dev.sessionCookie,
111
- useHTTP: dev.useHTTPForDevDeploy,
112
- },
109
+ deployConfig: { ...incomplete.config, password: dev.password },
113
110
  } }));
114
111
  await waitUntilExit();
115
112
  },
@@ -4,7 +4,7 @@ import { Box, Text } from 'ink';
4
4
  import { getAppType, localizedText } from '../utils/project-detection.js';
5
5
  function InfoScreen({ project }) {
6
6
  const appType = getAppType(project.manifest);
7
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision Project Information" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: localizedText(project.manifest.name) })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ID: " }), _jsx(Text, { children: project.manifest.id })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Version: " }), _jsx(Text, { children: project.manifest.version })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Type: " }), _jsx(Text, { color: "green", children: project.manifest.type }), _jsxs(Text, { dimColor: true, children: [" (", appType, ")"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Bundled: " }), _jsx(Text, { children: project.manifest.bundled ? 'Yes' : 'No' })] })] }), project.hasDevProperties && project.devProperties && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Development Configuration" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Domain: " }), _jsx(Text, { children: project.devProperties.domain })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Site: " }), _jsx(Text, { children: project.devProperties.siteName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Addon: " }), _jsx(Text, { children: project.devProperties.addonName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Username: " }), _jsx(Text, { children: project.devProperties.username })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Use HTTP: " }), _jsx(Text, { children: project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No' })] })] })] })), !project.hasDevProperties && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 No dev properties found. Run", ' ', _jsx(Text, { bold: true, children: "setup-dev-properties" }), " to configure."] }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Project Root: " }), _jsx(Text, { dimColor: true, children: project.root })] })] }));
7
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision Project Information" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: localizedText(project.manifest.name) })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ID: " }), _jsx(Text, { children: project.manifest.id })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Version: " }), _jsx(Text, { children: project.manifest.version })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Type: " }), _jsx(Text, { color: "green", children: project.manifest.type }), _jsxs(Text, { dimColor: true, children: [" (", appType, ")"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Bundled: " }), _jsx(Text, { children: project.manifest.bundled ? 'Yes' : 'No' })] })] }), project.hasDevProperties && project.devProperties && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Development Configuration" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Domain: " }), _jsx(Text, { children: project.devProperties.domain })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Site: " }), _jsx(Text, { children: project.devProperties.siteName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Addon: " }), _jsx(Text, { children: project.devProperties.addonName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Username: " }), _jsx(Text, { children: project.devProperties.username })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Use HTTP: " }), _jsx(Text, { children: project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No' })] })] })] })), !project.hasDevProperties && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 No dev properties found. Run ", _jsx(Text, { bold: true, children: "svc" }), " and press", ' ', _jsx(Text, { bold: true, children: "e" }), " to configure."] }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Project Root: " }), _jsx(Text, { dimColor: true, children: project.root })] })] }));
8
8
  }
9
9
  export const infoCommand = {
10
10
  name: 'info',