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.
Files changed (63) hide show
  1. package/dist/app.d.ts +1 -1
  2. package/dist/app.js +59 -8
  3. package/dist/cli.js +96 -39
  4. package/dist/commands/build.js +1 -1
  5. package/dist/commands/deploy.d.ts +2 -2
  6. package/dist/commands/deploy.js +135 -25
  7. package/dist/commands/dev.d.ts +8 -10
  8. package/dist/commands/dev.js +77 -366
  9. package/dist/commands/info.js +2 -2
  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/InfoScreen.js +2 -2
  17. package/dist/components/MainMenu.js +7 -2
  18. package/dist/components/PasswordInput.js +2 -1
  19. package/dist/components/SetupFlow.d.ts +2 -1
  20. package/dist/components/SetupFlow.js +100 -11
  21. package/dist/shell/AddonPicker.d.ts +14 -0
  22. package/dist/shell/AddonPicker.js +54 -0
  23. package/dist/shell/CommandPalette.d.ts +8 -0
  24. package/dist/shell/CommandPalette.js +63 -0
  25. package/dist/shell/ConfigForm.d.ts +36 -0
  26. package/dist/shell/ConfigForm.js +558 -0
  27. package/dist/shell/Frame.d.ts +59 -0
  28. package/dist/shell/Frame.js +134 -0
  29. package/dist/shell/Settings.d.ts +6 -0
  30. package/dist/shell/Settings.js +96 -0
  31. package/dist/shell/Shell.d.ts +9 -0
  32. package/dist/shell/Shell.js +586 -0
  33. package/dist/shell/Tabs.d.ts +36 -0
  34. package/dist/shell/Tabs.js +90 -0
  35. package/dist/shell/actions.d.ts +45 -0
  36. package/dist/shell/actions.js +0 -0
  37. package/dist/types/index.d.ts +44 -5
  38. package/dist/utils/config.d.ts +10 -0
  39. package/dist/utils/config.js +14 -0
  40. package/dist/utils/environments.d.ts +20 -0
  41. package/dist/utils/environments.js +74 -0
  42. package/dist/utils/i18n.d.ts +12 -0
  43. package/dist/utils/i18n.js +279 -0
  44. package/dist/utils/jsonc.d.ts +19 -0
  45. package/dist/utils/jsonc.js +74 -0
  46. package/dist/utils/keychain.d.ts +9 -0
  47. package/dist/utils/keychain.js +54 -0
  48. package/dist/utils/oauth2-auth.d.ts +64 -0
  49. package/dist/utils/oauth2-auth.js +242 -0
  50. package/dist/utils/password-prompt.d.ts +5 -0
  51. package/dist/utils/password-prompt.js +28 -0
  52. package/dist/utils/project-detection.d.ts +105 -6
  53. package/dist/utils/project-detection.js +411 -54
  54. package/dist/utils/session-cookie-auth.d.ts +35 -0
  55. package/dist/utils/session-cookie-auth.js +99 -0
  56. package/dist/utils/sitevision-api.d.ts +64 -5
  57. package/dist/utils/sitevision-api.js +195 -33
  58. package/dist/utils/tasks.d.ts +48 -0
  59. package/dist/utils/tasks.js +371 -0
  60. package/dist/utils/workspace.d.ts +17 -0
  61. package/dist/utils/workspace.js +67 -0
  62. package/package.json +3 -1
  63. 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
- export default function App({ project }) {
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 (!hasDevPassword) {
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
- // We can't easily update project info here without full reload,
264
- // but since we are just returning to menu, it's fine.
265
- // The user might need to restart CLI or we implement a reload mechanism.
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 App from './app.js';
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 Start interactive menu
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 # Interactive menu
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 (caller then falls back to the static masthead).
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 (!introArt) {
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
- render(_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", error.message] }), _jsx(Text, { dimColor: true, children: "Make sure you're in a Sitevision project directory" })] }));
149
- process.exit(1);
212
+ return fail(error.message, "Make sure you're in a Sitevision project directory");
150
213
  }
151
214
  })();
152
- // First run: show the welcome (branding + optional signing-password save),
153
- // then continue to the normal flow once the user dismisses it.
154
- if (firstRun) {
155
- await new Promise(resolve => {
156
- const app = render(_jsx(WelcomeScreen, { project: project, onComplete: () => {
157
- markFirstRunComplete();
158
- setLastSeenVersion(pkg.version);
159
- app.unmount();
160
- } }));
161
- app.waitUntilExit().then(() => resolve(), () => resolve());
162
- });
163
- }
164
- // If no command, show interactive menu (with the animated intro first when
165
- // the terminal can fit it).
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
- // Interactive `svc` handles this in SetupFlow; this covers direct commands
181
- // (svc deploy/dev/sign/…). Skip on non-TTY stdin (e.g. CI) where prompting
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): ');
@@ -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 === 'rest',
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, signingPassword, onBack, onRetryCredentials, }: DeployScreenProps): React.JSX.Element;
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 {};
@@ -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
- 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',
@@ -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
- projectRoot,
105
- manifest,
106
- devProperties,
107
- force,
108
- production,
109
- activate,
110
- signingPassword,
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 && (_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" })] }))] }));
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
- // Resolve deploy password (already loaded from keychain/env in detectProject prompt if missing)
150
- if (!project.devProperties.password) {
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
- // 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 }));
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
  };