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

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 (72) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +59 -8
  4. package/dist/cli.js +125 -40
  5. package/dist/commands/build.js +1 -1
  6. package/dist/commands/deploy.d.ts +2 -2
  7. package/dist/commands/deploy.js +137 -28
  8. package/dist/commands/dev.d.ts +8 -10
  9. package/dist/commands/dev.js +70 -371
  10. package/dist/commands/watch.js +5 -23
  11. package/dist/components/AnimatedLogo.js +8 -2
  12. package/dist/components/AuthLoginScreen.d.ts +21 -0
  13. package/dist/components/AuthLoginScreen.js +90 -0
  14. package/dist/components/DevPropertiesForm.d.ts +2 -1
  15. package/dist/components/DevPropertiesForm.js +198 -33
  16. package/dist/components/MainMenu.js +5 -0
  17. package/dist/components/PasswordInput.js +2 -1
  18. package/dist/components/SetupFlow.d.ts +2 -1
  19. package/dist/components/SetupFlow.js +99 -10
  20. package/dist/shell/AddonPicker.d.ts +14 -0
  21. package/dist/shell/AddonPicker.js +54 -0
  22. package/dist/shell/Changelog.d.ts +10 -0
  23. package/dist/shell/Changelog.js +44 -0
  24. package/dist/shell/CommandPalette.d.ts +12 -0
  25. package/dist/shell/CommandPalette.js +63 -0
  26. package/dist/shell/ConfigForm.d.ts +36 -0
  27. package/dist/shell/ConfigForm.js +558 -0
  28. package/dist/shell/Frame.d.ts +59 -0
  29. package/dist/shell/Frame.js +135 -0
  30. package/dist/shell/Help.d.ts +9 -0
  31. package/dist/shell/Help.js +95 -0
  32. package/dist/shell/Popover.d.ts +12 -0
  33. package/dist/shell/Popover.js +15 -0
  34. package/dist/shell/Settings.d.ts +6 -0
  35. package/dist/shell/Settings.js +96 -0
  36. package/dist/shell/Shell.d.ts +10 -0
  37. package/dist/shell/Shell.js +622 -0
  38. package/dist/shell/Tabs.d.ts +36 -0
  39. package/dist/shell/Tabs.js +90 -0
  40. package/dist/shell/actions.d.ts +48 -0
  41. package/dist/shell/actions.js +0 -0
  42. package/dist/types/index.d.ts +37 -3
  43. package/dist/utils/config.d.ts +10 -0
  44. package/dist/utils/config.js +14 -0
  45. package/dist/utils/environments.d.ts +20 -0
  46. package/dist/utils/environments.js +74 -0
  47. package/dist/utils/i18n.d.ts +12 -0
  48. package/dist/utils/i18n.js +314 -0
  49. package/dist/utils/jsonc.d.ts +19 -0
  50. package/dist/utils/jsonc.js +74 -0
  51. package/dist/utils/keychain.d.ts +9 -0
  52. package/dist/utils/keychain.js +54 -0
  53. package/dist/utils/oauth2-auth.d.ts +64 -0
  54. package/dist/utils/oauth2-auth.js +242 -0
  55. package/dist/utils/password-prompt.d.ts +5 -0
  56. package/dist/utils/password-prompt.js +28 -0
  57. package/dist/utils/process-runner.d.ts +13 -4
  58. package/dist/utils/process-runner.js +63 -37
  59. package/dist/utils/project-detection.d.ts +90 -4
  60. package/dist/utils/project-detection.js +389 -63
  61. package/dist/utils/session-cookie-auth.d.ts +35 -0
  62. package/dist/utils/session-cookie-auth.js +99 -0
  63. package/dist/utils/sitevision-api.d.ts +72 -5
  64. package/dist/utils/sitevision-api.js +220 -38
  65. package/dist/utils/sitevision-scripts-runner.d.ts +2 -1
  66. package/dist/utils/sitevision-scripts-runner.js +18 -7
  67. package/dist/utils/tasks.d.ts +50 -0
  68. package/dist/utils/tasks.js +439 -0
  69. package/dist/utils/workspace.d.ts +17 -0
  70. package/dist/utils/workspace.js +67 -0
  71. package/package.json +5 -2
  72. package/readme.md +103 -121
@@ -3,29 +3,127 @@ import React from 'react';
3
3
  import { render, Box, Text, useInput } from 'ink';
4
4
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
5
  import { deployApp, deployProduction } from '../utils/sitevision-api.js';
6
- import { getZipPath, getSignedZipPath, getAppType, } from '../utils/project-detection.js';
6
+ import { getDeployZipPath, 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
- export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, signingPassword, onBack, onRetryCredentials, }) {
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 (onRetryCredentials && state.status === 'error' && input === 'r') {
21
- onRetryCredentials();
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',
@@ -60,8 +161,7 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
60
161
  });
61
162
  }
62
163
  else {
63
- // Dev deployment can use unsigned zip
64
- const zipPath = getZipPath(projectRoot, manifest);
164
+ const zipPath = getDeployZipPath(projectRoot, manifest);
65
165
  if (!zipExists(zipPath)) {
66
166
  setState({
67
167
  status: 'error',
@@ -75,10 +175,13 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
75
175
  addonName: devProperties.addonName,
76
176
  username: devProperties.username,
77
177
  password: devProperties.password,
178
+ accessToken,
179
+ sessionCookie,
78
180
  useHTTP: devProperties.useHTTPForDevDeploy,
79
181
  };
80
182
  const result = await deployApp(zipPath, config, appType, force);
81
183
  if (!result.success) {
184
+ clearStaleCookie(result);
82
185
  setState({
83
186
  status: 'error',
84
187
  error: result.error || 'Deployment failed',
@@ -100,20 +203,30 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
100
203
  }
101
204
  }
102
205
  runDeploy();
103
- }, [
104
- projectRoot,
105
- manifest,
106
- devProperties,
107
- force,
108
- production,
109
- activate,
110
- signingPassword,
111
- ]);
206
+ // 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);
213
+ 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
+ }
223
+ } }));
224
+ }
112
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'
113
226
  ? 'Deploying'
114
227
  : state.status === 'success'
115
228
  ? '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 && (_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" })] }))] }));
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" })] }))] }));
117
230
  }
118
231
  export const deployCommand = {
119
232
  name: 'deploy',
@@ -146,8 +259,11 @@ export const deployCommand = {
146
259
  console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
147
260
  return;
148
261
  }
149
- // Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
150
- if (!project.devProperties.password) {
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) {
151
267
  const { domain, username } = project.devProperties;
152
268
  console.log('');
153
269
  const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
@@ -164,15 +280,8 @@ export const deployCommand = {
164
280
  const production = Boolean(flags['production']);
165
281
  const force = Boolean(flags['force']);
166
282
  const activate = Boolean(flags['activate']);
167
- // For production, we need the signed zip, which requires signing credentials
168
- let signingPassword;
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 }));
283
+ // 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 }));
176
285
  await waitUntilExit();
177
286
  },
178
287
  };
@@ -1,16 +1,14 @@
1
1
  import React from 'react';
2
2
  import { type Command } from './types.js';
3
- import type { SitevisionManifest, DevProperties, SigningCredentials } from '../types/index.js';
3
+ import { type DevOptions } from '../utils/tasks.js';
4
+ import type { ProjectInfo, SigningCredentials } from '../types/index.js';
4
5
  interface DevScreenProps {
5
- projectRoot: string;
6
- manifest: SitevisionManifest;
7
- devProperties?: DevProperties;
8
- signed: boolean;
9
- deploy?: boolean;
10
- signingCredentials?: SigningCredentials;
11
- onBack?: () => void;
12
- onRetryCredentials?: () => void;
6
+ project: ProjectInfo;
7
+ options: DevOptions;
13
8
  }
14
- export declare function DevScreen({ projectRoot, manifest, devProperties, signed, deploy, signingCredentials, onBack, onRetryCredentials, }: DevScreenProps): React.JSX.Element;
9
+ /** Standalone `svc dev` / `svc watch`: one task, its log, Ctrl+C or q to stop. */
10
+ export declare function DevScreen({ project, options }: DevScreenProps): React.JSX.Element;
11
+ /** Resolve signing credentials for signed dev/watch, prompting if needed. */
12
+ export declare function resolveSigningForCli(project: ProjectInfo): Promise<SigningCredentials | undefined>;
15
13
  export declare const devCommand: Command;
16
14
  export {};