sitevision-cli 1.0.0-beta.4 → 1.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -13,6 +13,7 @@ import { DeployScreen } from './commands/deploy.js';
13
13
  import { SignScreen } from './commands/sign.js';
14
14
  import { SigningPropertiesForm } from './components/SigningPropertiesForm.js';
15
15
  import { getSigningPassword, setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword, } from './utils/keychain.js';
16
+ const NONBASIC_DEV_UNSUPPORTED = '\x1b[33mdev/watch support only basic auth. Use `svc deploy` for OAuth2/cookie.\x1b[0m';
16
17
  export default function App({ project: initialProject }) {
17
18
  // The project is loaded once at startup, but setup flows write new values to
18
19
  // disk and the OS keychain. Hold it in state so we can re-detect after setup
@@ -70,10 +71,18 @@ export default function App({ project: initialProject }) {
70
71
  };
71
72
  // Check if dev password is available (either from file or session)
72
73
  const hasDevPassword = Boolean(project.devProperties?.password || devPassword);
74
+ // OAuth2 / cookie configs authenticate with a token or session resolved at
75
+ // deploy time, so they need no basic password. `svc dev`/`watch` stay basic.
76
+ const authMethod = project.devProperties?.authMethod ?? 'basic';
77
+ const isTokenAuth = authMethod === 'oauth2' || authMethod === 'cookie';
78
+ const deployAuthReady = isTokenAuth || hasDevPassword;
73
79
  // Get effective dev properties with session password if needed
74
80
  const getEffectiveDevProperties = () => {
75
81
  if (!project.devProperties)
76
82
  return undefined;
83
+ // Non-basic configs authenticate by token/cookie — never graft a password.
84
+ if (isTokenAuth)
85
+ return project.devProperties;
77
86
  if (project.devProperties.password)
78
87
  return project.devProperties;
79
88
  return { ...project.devProperties, password: devPassword };
@@ -131,6 +140,10 @@ export default function App({ project: initialProject }) {
131
140
  console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
132
141
  return;
133
142
  }
143
+ if (isTokenAuth) {
144
+ console.log(NONBASIC_DEV_UNSUPPORTED);
145
+ return;
146
+ }
134
147
  if (!hasDevPassword) {
135
148
  setState('dev-password-input');
136
149
  }
@@ -147,6 +160,10 @@ export default function App({ project: initialProject }) {
147
160
  console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
148
161
  return;
149
162
  }
163
+ if (isTokenAuth) {
164
+ console.log(NONBASIC_DEV_UNSUPPORTED);
165
+ return;
166
+ }
150
167
  // Need both dev password and signing password
151
168
  if (!hasDevPassword) {
152
169
  setState('dev-password-input');
@@ -168,6 +185,10 @@ export default function App({ project: initialProject }) {
168
185
  console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
169
186
  return;
170
187
  }
188
+ if (isTokenAuth) {
189
+ console.log(NONBASIC_DEV_UNSUPPORTED);
190
+ return;
191
+ }
171
192
  routeToSigningStep(command);
172
193
  break;
173
194
  case 'sign':
@@ -191,7 +212,7 @@ export default function App({ project: initialProject }) {
191
212
  console.log('\x1b[31mDevelopment properties not configured. Create a .dev_properties.json file first.\x1b[0m');
192
213
  return;
193
214
  }
194
- if (!hasDevPassword) {
215
+ if (!deployAuthReady) {
195
216
  setState('dev-password-input');
196
217
  }
197
218
  else {
package/dist/cli.js CHANGED
@@ -58,6 +58,12 @@ const cli = meow(`
58
58
  shortFlag: 'p',
59
59
  default: false,
60
60
  },
61
+ token: {
62
+ type: 'string',
63
+ },
64
+ cookie: {
65
+ type: 'string',
66
+ },
61
67
  },
62
68
  });
63
69
  const [commandName, ...args] = cli.input;
@@ -149,6 +155,22 @@ async function main() {
149
155
  process.exit(1);
150
156
  }
151
157
  })();
158
+ // --token / --cookie override the resolved bearer token / session cookie for
159
+ // this run (manual / CI path, alongside SITEVISION_ACCESS_TOKEN and
160
+ // SITEVISION_SESSION_COOKIE).
161
+ if (cli.flags.token || cli.flags.cookie) {
162
+ if (project.devProperties) {
163
+ if (cli.flags.token) {
164
+ project.devProperties.accessToken = cli.flags.token;
165
+ }
166
+ if (cli.flags.cookie) {
167
+ project.devProperties.sessionCookie = cli.flags.cookie;
168
+ }
169
+ }
170
+ else {
171
+ console.log('\x1b[33m--token/--cookie needs a .dev_properties.json (domain, site, addon) to deploy against.\x1b[0m');
172
+ }
173
+ }
152
174
  // First run: show the welcome (branding + optional signing-password save),
153
175
  // then continue to the normal flow once the user dismisses it.
154
176
  if (firstRun) {
@@ -6,7 +6,9 @@ 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';
9
+ import { setDeployPassword, deleteSessionCookie } from '../utils/keychain.js';
10
+ import { resolveOAuth2AccessToken } from '../utils/oauth2-auth.js';
11
+ import { resolveSessionCookie } from '../utils/session-cookie-auth.js';
10
12
  export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, signingPassword, onBack, onRetryCredentials, }) {
11
13
  const [state, setState] = React.useState({
12
14
  status: 'deploying',
@@ -26,6 +28,52 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
26
28
  async function runDeploy() {
27
29
  try {
28
30
  const appType = getAppType(manifest);
31
+ // Resolve an OAuth2 token silently (env/keychain refresh). A fresh
32
+ // browser login happens only in the direct `svc deploy` command, which
33
+ // sets accessToken before rendering — the Ink menu can't own the
34
+ // terminal for a login, so it resolves refresh-only here.
35
+ const authMethod = devProperties.authMethod ?? 'basic';
36
+ let accessToken = devProperties.accessToken;
37
+ if (authMethod === 'oauth2' && !accessToken) {
38
+ accessToken =
39
+ (await resolveOAuth2AccessToken(devProperties, {
40
+ interactive: false,
41
+ })) ?? undefined;
42
+ if (!accessToken) {
43
+ setState({
44
+ status: 'error',
45
+ error: 'No OAuth2 access token. Run `svc deploy` from a terminal to log in, or set SITEVISION_ACCESS_TOKEN.',
46
+ });
47
+ return;
48
+ }
49
+ }
50
+ let sessionCookie = devProperties.sessionCookie;
51
+ if (authMethod === 'cookie' && !sessionCookie) {
52
+ sessionCookie =
53
+ (await resolveSessionCookie(devProperties, {
54
+ interactive: false,
55
+ })) ?? undefined;
56
+ if (!sessionCookie) {
57
+ setState({
58
+ status: 'error',
59
+ error: 'No session cookie. Run `svc deploy` from a terminal to log in, or set SITEVISION_SESSION_COOKIE.',
60
+ });
61
+ return;
62
+ }
63
+ }
64
+ // A stale session fails without a clean 401 — clear the stored cookie
65
+ // so the next run re-authenticates. Skip when SITEVISION_SESSION_COOKIE
66
+ // is set: detection re-reads it first, so clearing would just replay
67
+ // the same dead cookie in a loop.
68
+ const clearStaleCookie = (result) => {
69
+ if (result.authExpired &&
70
+ authMethod === 'cookie' &&
71
+ !process.env['SITEVISION_SESSION_COOKIE'] &&
72
+ devProperties.domain &&
73
+ devProperties.username) {
74
+ deleteSessionCookie(devProperties.domain, devProperties.username);
75
+ }
76
+ };
29
77
  if (production) {
30
78
  // Production deployment requires a signed zip
31
79
  const signedZipPath = getSignedZipPath(projectRoot, manifest);
@@ -42,11 +90,14 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
42
90
  addonName: devProperties.addonName,
43
91
  username: devProperties.username,
44
92
  password: devProperties.password,
93
+ accessToken,
94
+ sessionCookie,
45
95
  useHTTP: devProperties.useHTTPForDevDeploy,
46
96
  activate,
47
97
  };
48
98
  const result = await deployProduction(signedZipPath, config, appType);
49
99
  if (!result.success) {
100
+ clearStaleCookie(result);
50
101
  setState({
51
102
  status: 'error',
52
103
  error: result.error || 'Deployment failed',
@@ -75,10 +126,13 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
75
126
  addonName: devProperties.addonName,
76
127
  username: devProperties.username,
77
128
  password: devProperties.password,
129
+ accessToken,
130
+ sessionCookie,
78
131
  useHTTP: devProperties.useHTTPForDevDeploy,
79
132
  };
80
133
  const result = await deployApp(zipPath, config, appType, force);
81
134
  if (!result.success) {
135
+ clearStaleCookie(result);
82
136
  setState({
83
137
  status: 'error',
84
138
  error: result.error || 'Deployment failed',
@@ -146,8 +200,32 @@ export const deployCommand = {
146
200
  console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
147
201
  return;
148
202
  }
149
- // Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
150
- if (!project.devProperties.password) {
203
+ // Resolve deploy credentials. A token/cookie from env/--flag (already
204
+ // loaded in detectProject / cli) short-circuits everything.
205
+ const authMethod = project.devProperties.authMethod ?? 'basic';
206
+ if (project.devProperties.accessToken ||
207
+ project.devProperties.sessionCookie) {
208
+ // Nothing to acquire — a bearer token or session cookie is in hand.
209
+ }
210
+ else if (authMethod === 'oauth2') {
211
+ const token = await resolveOAuth2AccessToken(project.devProperties);
212
+ if (!token) {
213
+ console.log('\n\x1b[31mError: No OAuth2 access token available.\x1b[0m');
214
+ console.log('Set SITEVISION_ACCESS_TOKEN, pass --token, or configure the oauth2 endpoints in .dev_properties.json to log in.\n');
215
+ return;
216
+ }
217
+ project.devProperties.accessToken = token;
218
+ }
219
+ else if (authMethod === 'cookie') {
220
+ const cookie = await resolveSessionCookie(project.devProperties);
221
+ if (!cookie) {
222
+ console.log('\n\x1b[31mError: No session cookie available.\x1b[0m');
223
+ console.log('Log in when the browser opens, or set SITEVISION_SESSION_COOKIE / pass --cookie.\n');
224
+ return;
225
+ }
226
+ project.devProperties.sessionCookie = cookie;
227
+ }
228
+ else if (!project.devProperties.password) {
151
229
  const { domain, username } = project.devProperties;
152
230
  console.log('');
153
231
  const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
@@ -11,9 +11,10 @@ import { signApp, deployApp } from '../utils/sitevision-api.js';
11
11
  import { setDeployPassword } from '../utils/keychain.js';
12
12
  import { resolveSigningPassword } from '../utils/signing-password.js';
13
13
  import { copyStaticToBuild, createBuildZip, cleanBuild } from '../utils/zip.js';
14
- import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, } from '../utils/project-detection.js';
14
+ import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, readManifest, } from '../utils/project-detection.js';
15
15
  export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy = true, signingCredentials, onBack, onRetryCredentials, }) {
16
16
  const { exit } = useApp();
17
+ const [version, setVersion] = React.useState(manifest.version);
17
18
  const [state, setState] = React.useState({
18
19
  status: 'initializing',
19
20
  message: 'Starting webpack watch...',
@@ -34,6 +35,16 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
34
35
  const isBuildingRef = React.useRef(false);
35
36
  const pendingRebuildRef = React.useRef(false);
36
37
  // Sign (if needed) and deploy an already-built zip, updating UI state.
38
+ const refreshVersion = React.useCallback(() => {
39
+ try {
40
+ const fresh = readManifest(projectRoot)?.manifest.version;
41
+ if (fresh)
42
+ setVersion(fresh);
43
+ }
44
+ catch {
45
+ // keep last known version
46
+ }
47
+ }, [projectRoot]);
37
48
  const signAndDeploy = React.useCallback(async (zipPath, buildTime) => {
38
49
  try {
39
50
  let deployZipPath = zipPath;
@@ -57,6 +68,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
57
68
  }
58
69
  deployZipPath = signedZipPath;
59
70
  }
71
+ refreshVersion();
60
72
  // Watch/build-only mode: stop after building (and signing).
61
73
  if (!deploy || !devProperties) {
62
74
  setState(prev => ({
@@ -113,7 +125,15 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
113
125
  error: error instanceof Error ? error.message : String(error),
114
126
  }));
115
127
  }
116
- }, [projectRoot, manifest, devProperties, signed, deploy, signingCredentials]);
128
+ }, [
129
+ projectRoot,
130
+ manifest,
131
+ devProperties,
132
+ signed,
133
+ deploy,
134
+ signingCredentials,
135
+ refreshVersion,
136
+ ]);
117
137
  // In-house webpack path: copy static, zip, then sign + deploy.
118
138
  const handleBuildComplete = React.useCallback(async (result) => {
119
139
  if (!result.success) {
@@ -353,7 +373,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy
353
373
  };
354
374
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
355
375
  ? ` | Last build: ${state.lastBuildTime}ms`
356
- : '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [localizedText(manifest.name), " v", manifest.version] }) }), deploy && devProperties && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Target: ", devProperties.domain, "/", devProperties.siteName, "/", devProperties.addonName] }) })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), _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 (Ctrl+C to stop process)" })) : (_jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop" }))] })] }));
376
+ : '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [localizedText(manifest.name), " v", version] }) }), deploy && devProperties && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Target: ", devProperties.domain, "/", devProperties.siteName, "/", devProperties.addonName] }) })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), _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 (Ctrl+C to stop process)" })) : (_jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop" }))] })] }));
357
377
  }
358
378
  export const devCommand = {
359
379
  name: 'dev',
@@ -374,6 +394,12 @@ export const devCommand = {
374
394
  console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
375
395
  return;
376
396
  }
397
+ // dev/watch continuously redeploy and only support basic auth. Fail clearly
398
+ // on an OAuth2/cookie config instead of prompting for an unusable password.
399
+ if ((project.devProperties.authMethod ?? 'basic') !== 'basic') {
400
+ console.log('\n\x1b[33mdev/watch support only basic auth. Use `svc deploy` for OAuth2/cookie.\x1b[0m\n');
401
+ return;
402
+ }
377
403
  // Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
378
404
  if (!project.devProperties.password) {
379
405
  const { domain, username } = project.devProperties;
@@ -3,15 +3,35 @@ import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { TextInput } from './TextInput.js';
5
5
  import { writeDevProperties } from '../utils/project-detection.js';
6
- import { setDeployPassword, deleteDeployPassword } from '../utils/keychain.js';
7
- const STEPS = [
8
- { id: 'domain', label: 'Domain' },
9
- { id: 'siteName', label: 'Site Name' },
10
- { id: 'addonName', label: 'Addon Name' },
11
- { id: 'username', label: 'Username' },
12
- { id: 'password', label: 'Password' },
13
- { id: 'useHTTP', label: 'Use HTTP' },
6
+ import { setDeployPassword, deleteDeployPassword, setOAuth2ClientSecret, } from '../utils/keychain.js';
7
+ import { DEFAULT_REDIRECT_PORT } from '../utils/oauth2-auth.js';
8
+ const LABELS = {
9
+ domain: 'Domain',
10
+ siteName: 'Site Name',
11
+ addonName: 'Addon Name',
12
+ username: 'Username',
13
+ authMethod: 'Auth Method',
14
+ password: 'Password',
15
+ oauthClientId: 'Client ID',
16
+ oauthAuthEndpoint: 'Authorization Endpoint',
17
+ oauthTokenEndpoint: 'Token Endpoint',
18
+ oauthScopes: 'Scopes',
19
+ oauthClientSecret: 'Client Secret',
20
+ sessionLoginUrl: 'Login URL',
21
+ useHTTP: 'Use HTTP',
22
+ };
23
+ const AUTH_METHODS = [
24
+ { value: 'basic', label: 'Basic auth (username + password)' },
25
+ { value: 'oauth2', label: 'OAuth2 bearer token (PKCE)' },
26
+ { value: 'cookie', label: 'Session cookie (SAML / SSO login)' },
14
27
  ];
28
+ function parseScopes(raw) {
29
+ const scopes = raw
30
+ .split(/[\s,]+/)
31
+ .map(s => s.trim())
32
+ .filter(Boolean);
33
+ return scopes.length > 0 ? scopes : undefined;
34
+ }
15
35
  export function DevPropertiesForm({ projectRoot, initialProperties, packageJson, onComplete, onCancel, }) {
16
36
  const [stepIndex, setStepIndex] = useState(0);
17
37
  const [properties, setProperties] = useState(() => {
@@ -19,49 +39,158 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
19
39
  domain: packageJson.developmentDomain || '',
20
40
  addonName: packageJson.addonName || '',
21
41
  siteName: packageJson.siteName || '',
42
+ authMethod: 'basic',
22
43
  };
23
44
  return { ...defaults, ...initialProperties };
24
45
  });
25
- const currentStep = STEPS[stepIndex];
26
- const handleNext = (key, value) => {
27
- const newProperties = { ...properties, [key]: value };
28
- setProperties(newProperties);
29
- if (stepIndex < STEPS.length - 1) {
46
+ const [oauth, setOauth] = useState(() => ({
47
+ authorizationEndpoint: initialProperties?.oauth2?.authorizationEndpoint ?? '',
48
+ tokenEndpoint: initialProperties?.oauth2?.tokenEndpoint ?? '',
49
+ clientId: initialProperties?.oauth2?.clientId ?? '',
50
+ scopes: initialProperties?.oauth2?.scopes?.join(' ') ?? '',
51
+ clientSecret: '',
52
+ }));
53
+ const method = properties.authMethod ?? 'basic';
54
+ const isOAuth = method === 'oauth2';
55
+ const methodSteps = method === 'oauth2'
56
+ ? [
57
+ 'oauthClientId',
58
+ 'oauthAuthEndpoint',
59
+ 'oauthTokenEndpoint',
60
+ 'oauthScopes',
61
+ 'oauthClientSecret',
62
+ ]
63
+ : method === 'cookie'
64
+ ? ['sessionLoginUrl']
65
+ : ['password'];
66
+ const steps = [
67
+ 'domain',
68
+ 'siteName',
69
+ 'addonName',
70
+ 'username',
71
+ 'authMethod',
72
+ ...methodSteps,
73
+ 'useHTTP',
74
+ ];
75
+ const currentStep = steps[stepIndex];
76
+ const redirectPort = initialProperties?.oauth2?.redirectPort ?? DEFAULT_REDIRECT_PORT;
77
+ const advance = () => {
78
+ if (stepIndex < steps.length - 1) {
30
79
  setStepIndex(stepIndex + 1);
31
80
  }
32
- else {
33
- const finalProperties = newProperties;
34
- const { password, domain, username } = finalProperties;
35
- if (password && domain && username) {
36
- setDeployPassword(domain, username, password);
81
+ };
82
+ const finalize = (props, fields) => {
83
+ const authMethod = props.authMethod ?? 'basic';
84
+ const domain = props.domain ?? '';
85
+ const username = props.username ?? '';
86
+ const finalProps = {
87
+ domain,
88
+ siteName: props.siteName ?? '',
89
+ addonName: props.addonName ?? '',
90
+ username,
91
+ authMethod,
92
+ useHTTPForDevDeploy: props.useHTTPForDevDeploy ?? false,
93
+ };
94
+ if (authMethod === 'oauth2') {
95
+ finalProps.oauth2 = {
96
+ authorizationEndpoint: fields.authorizationEndpoint,
97
+ tokenEndpoint: fields.tokenEndpoint,
98
+ clientId: fields.clientId,
99
+ scopes: parseScopes(fields.scopes),
100
+ ...(initialProperties?.oauth2?.redirectPort && {
101
+ redirectPort: initialProperties.oauth2.redirectPort,
102
+ }),
103
+ };
104
+ if (fields.clientSecret && domain && fields.clientId) {
105
+ setOAuth2ClientSecret(domain, fields.clientId, fields.clientSecret);
37
106
  }
38
- else if (domain && username) {
39
- // Empty password clear any stale keychain entry so deploy falls through to prompt
40
- deleteDeployPassword(domain, username);
107
+ }
108
+ else if (authMethod === 'cookie') {
109
+ if (props.sessionLoginUrl) {
110
+ finalProps.sessionLoginUrl = props.sessionLoginUrl;
41
111
  }
42
- writeDevProperties(projectRoot, finalProperties);
43
- onComplete();
112
+ }
113
+ else if (props.password && domain && username) {
114
+ setDeployPassword(domain, username, props.password);
115
+ }
116
+ else if (domain && username) {
117
+ // Empty password — clear any stale keychain entry so deploy prompts.
118
+ deleteDeployPassword(domain, username);
119
+ }
120
+ writeDevProperties(projectRoot, finalProps);
121
+ onComplete();
122
+ };
123
+ // Update a top-level DevProperties field, then advance or finalize.
124
+ const submitProperty = (key, value) => {
125
+ const next = { ...properties, [key]: value };
126
+ setProperties(next);
127
+ if (steps[stepIndex] === steps[steps.length - 1]) {
128
+ finalize(next, oauth);
129
+ }
130
+ else {
131
+ advance();
132
+ }
133
+ };
134
+ // Update an OAuth field, then advance or finalize.
135
+ const submitOAuth = (key, value) => {
136
+ const next = { ...oauth, [key]: value };
137
+ setOauth(next);
138
+ if (steps[stepIndex] === steps[steps.length - 1]) {
139
+ finalize(properties, next);
140
+ }
141
+ else {
142
+ advance();
44
143
  }
45
144
  };
46
145
  const renderInput = () => {
47
- switch (currentStep?.id) {
146
+ switch (currentStep) {
48
147
  case 'domain':
49
- return (_jsx(TextInput, { label: "Development Domain (e.g. www.sitevision.se)", defaultValue: properties.domain, placeholder: "sitevision.se", onSubmit: (value) => handleNext('domain', value), onCancel: onCancel }, "domain"));
148
+ return (_jsx(TextInput, { label: "Development Domain (e.g. www.sitevision.se)", defaultValue: properties.domain, placeholder: "sitevision.se", onSubmit: value => submitProperty('domain', value), onCancel: onCancel }, "domain"));
50
149
  case 'siteName':
51
- return (_jsx(TextInput, { label: "Site Name (Root node name)", defaultValue: properties.siteName, onSubmit: (value) => handleNext('siteName', value), onCancel: onCancel }, "siteName"));
150
+ return (_jsx(TextInput, { label: "Site Name (Root node name)", defaultValue: properties.siteName, onSubmit: value => submitProperty('siteName', value), onCancel: onCancel }, "siteName"));
52
151
  case 'addonName':
53
- return (_jsx(TextInput, { label: "Addon Name", defaultValue: properties.addonName, onSubmit: (value) => handleNext('addonName', value), onCancel: onCancel }, "addonName"));
152
+ return (_jsx(TextInput, { label: "Addon Name", defaultValue: properties.addonName, onSubmit: value => submitProperty('addonName', value), onCancel: onCancel }, "addonName"));
54
153
  case 'username':
55
- return (_jsx(TextInput, { label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit: (value) => handleNext('username', value), onCancel: onCancel }, "username"));
154
+ return (_jsx(TextInput, { label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit: value => submitProperty('username', value), onCancel: onCancel }, "username"));
155
+ case 'authMethod':
156
+ return (_jsx(MethodSelect, { defaultValue: method, onSubmit: value => submitProperty('authMethod', value) }, "authMethod"));
56
157
  case 'password':
57
- return (_jsx(TextInput, { label: "Password (saved in OS keychain \u2014 leave empty to prompt on each run)", type: "password", defaultValue: properties.password, onSubmit: (value) => handleNext('password', value), onCancel: onCancel }, "password"));
158
+ return (_jsx(TextInput, { label: "Password (saved in OS keychain \u2014 leave empty to prompt on each run)", type: "password", defaultValue: properties.password, onSubmit: value => submitProperty('password', value), onCancel: onCancel }, "password"));
159
+ case 'oauthClientId':
160
+ return (_jsx(TextInput, { label: "OAuth2 Client ID", defaultValue: oauth.clientId, onSubmit: value => submitOAuth('clientId', value), onCancel: onCancel }, "oauthClientId"));
161
+ case 'oauthAuthEndpoint':
162
+ return (_jsx(TextInput, { label: "Authorization Endpoint URL", defaultValue: oauth.authorizationEndpoint, onSubmit: value => submitOAuth('authorizationEndpoint', value), onCancel: onCancel }, "oauthAuthEndpoint"));
163
+ case 'oauthTokenEndpoint':
164
+ return (_jsx(TextInput, { label: "Token Endpoint URL", defaultValue: oauth.tokenEndpoint, onSubmit: value => submitOAuth('tokenEndpoint', value), onCancel: onCancel }, "oauthTokenEndpoint"));
165
+ case 'oauthScopes':
166
+ return (_jsx(TextInput, { label: "Scopes (space-separated, optional)", defaultValue: oauth.scopes, onSubmit: value => submitOAuth('scopes', value), onCancel: onCancel }, "oauthScopes"));
167
+ case 'oauthClientSecret':
168
+ return (_jsx(TextInput, { label: "Client Secret (OS keychain \u2014 leave empty for a public/PKCE client)", type: "password", defaultValue: oauth.clientSecret, onSubmit: value => submitOAuth('clientSecret', value), onCancel: onCancel }, "oauthClientSecret"));
169
+ case 'sessionLoginUrl':
170
+ return (_jsx(TextInput, { label: "Login URL (opened in a browser; blank = site root)", defaultValue: properties.sessionLoginUrl ??
171
+ (properties.domain ? `https://${properties.domain}/` : ''), onSubmit: value => submitProperty('sessionLoginUrl', value), onCancel: onCancel }, "sessionLoginUrl"));
58
172
  case 'useHTTP':
59
- return (_jsx(BooleanInput, { label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy ?? false, onSubmit: (value) => handleNext('useHTTPForDevDeploy', value) }, "useHTTP"));
173
+ return (_jsx(BooleanInput, { label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy ?? false, onSubmit: value => submitProperty('useHTTPForDevDeploy', value) }, "useHTTP"));
60
174
  default:
61
175
  return null;
62
176
  }
63
177
  };
64
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Development Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ", STEPS.length, ": ", currentStep?.label] })] }), _jsx(Box, { marginBottom: 1, children: STEPS.map((s, i) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: i === stepIndex ? 'green' : i < stepIndex ? 'green' : 'gray', children: i < stepIndex ? '✓' : i === stepIndex ? '●' : '○' }) }, s.id))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: renderInput() })] }));
178
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Development Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ", steps.length, ":", ' ', currentStep ? LABELS[currentStep] : ''] })] }), isOAuth && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Whitelist this redirect URI on the OAuth2 client: http://127.0.0.1:", redirectPort, "/callback"] }) })), _jsx(Box, { marginBottom: 1, children: steps.map((s, i) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: i <= stepIndex ? 'green' : 'gray', children: i < stepIndex ? '✓' : i === stepIndex ? '●' : '○' }) }, s))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: renderInput() })] }));
179
+ }
180
+ function MethodSelect({ defaultValue, onSubmit, }) {
181
+ const [index, setIndex] = useState(() => Math.max(0, AUTH_METHODS.findIndex(m => m.value === defaultValue)));
182
+ useInput((_input, key) => {
183
+ if (key.upArrow) {
184
+ setIndex(p => (p === 0 ? AUTH_METHODS.length - 1 : p - 1));
185
+ }
186
+ else if (key.downArrow) {
187
+ setIndex(p => (p === AUTH_METHODS.length - 1 ? 0 : p + 1));
188
+ }
189
+ else if (key.return) {
190
+ onSubmit(AUTH_METHODS[index].value);
191
+ }
192
+ });
193
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Authentication method" }) }), AUTH_METHODS.map((m, i) => (_jsxs(Text, { color: i === index ? 'green' : undefined, children: [i === index ? '❯ ' : ' ', m.label] }, m.value))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move, Enter to select" }) })] }));
65
194
  }
66
195
  function BooleanInput({ label, defaultValue, onSubmit, }) {
67
196
  useInput(input => {