sitevision-cli 0.4.0-beta.0 → 0.4.0-beta.2

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
@@ -1,20 +1,48 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useMemo, useState } from 'react';
3
3
  import { MainMenu } from './components/MainMenu.js';
4
4
  import { InfoScreen } from './components/InfoScreen.js';
5
5
  import { SetupFlow } from './components/SetupFlow.js';
6
6
  import { PasswordInput } from './components/PasswordInput.js';
7
+ import { KeychainPasswordChoice } from './components/KeychainPasswordChoice.js';
8
+ import { decideSigningStep } from './utils/signing-step.js';
7
9
  import { DevScreen } from './commands/dev.js';
8
10
  import { BuildScreen } from './commands/build.js';
9
11
  import { DeployScreen } from './commands/deploy.js';
10
12
  import { SignScreen } from './commands/sign.js';
11
13
  import { SigningPropertiesForm } from './components/SigningPropertiesForm.js';
12
- import { setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword, } from './utils/keychain.js';
14
+ import { getSigningPassword, setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword, } from './utils/keychain.js';
13
15
  export default function App({ project }) {
14
16
  const [state, setState] = useState('setup');
15
17
  const [currentCommand, setCurrentCommand] = useState('');
16
18
  const [signingPassword, setSigningPassword] = useState('');
17
19
  const [devPassword, setDevPassword] = useState('');
20
+ // When true, skip the keychain "use saved / enter new" choice and go straight
21
+ // to manual entry (e.g. the saved password just failed and we're retrying).
22
+ const [signingRetry, setSigningRetry] = useState(false);
23
+ // Read the saved signing password from the keychain once (keychain access is
24
+ // slow and this component re-renders frequently).
25
+ const signingUsername = project.devProperties?.signingUsername;
26
+ const storedSigningPassword = useMemo(() => (signingUsername ? getSigningPassword(signingUsername) : null), [signingUsername]);
27
+ // Decide the next step once a signing password is needed: proceed if we already
28
+ // have one this session, offer the keychain choice if one is saved, otherwise
29
+ // prompt for manual entry.
30
+ const routeToSigningStep = (command = currentCommand) => {
31
+ const step = decideSigningStep({
32
+ hasSessionPassword: Boolean(signingPassword),
33
+ hasStoredPassword: Boolean(storedSigningPassword),
34
+ isRetry: signingRetry,
35
+ });
36
+ if (step === 'proceed') {
37
+ setState(command === 'dev-signed' ? 'dev' : 'sign');
38
+ }
39
+ else if (step === 'choice') {
40
+ setState('signing-password-choice');
41
+ }
42
+ else {
43
+ setState('signing-password-input');
44
+ }
45
+ };
18
46
  // Check if dev password is available (either from file or session)
19
47
  const hasDevPassword = Boolean(project.devProperties?.password || devPassword);
20
48
  // Get effective dev properties with session password if needed
@@ -35,8 +63,8 @@ export default function App({ project }) {
35
63
  }
36
64
  // Continue to the intended command
37
65
  if (currentCommand === 'dev' || currentCommand === 'dev-signed') {
38
- if (currentCommand === 'dev-signed' && !signingPassword) {
39
- setState('signing-password-input');
66
+ if (currentCommand === 'dev-signed') {
67
+ routeToSigningStep();
40
68
  }
41
69
  else {
42
70
  setState('dev');
@@ -46,6 +74,15 @@ export default function App({ project }) {
46
74
  setState('deploy');
47
75
  }
48
76
  };
77
+ const handleUseSavedSigning = () => {
78
+ if (storedSigningPassword) {
79
+ setSigningPassword(storedSigningPassword);
80
+ }
81
+ setState(currentCommand === 'dev-signed' ? 'dev' : 'sign');
82
+ };
83
+ const handleEnterNewSigning = () => {
84
+ setState('signing-password-input');
85
+ };
49
86
  const handleSigningPasswordSubmit = (password, remember) => {
50
87
  setSigningPassword(password);
51
88
  if (remember && project.devProperties?.signingUsername && password) {
@@ -60,6 +97,8 @@ export default function App({ project }) {
60
97
  };
61
98
  const handleCommandSelect = (command) => {
62
99
  setCurrentCommand(command);
100
+ // Fresh selection from the menu — re-offer the saved keychain password.
101
+ setSigningRetry(false);
63
102
  switch (command) {
64
103
  case 'info':
65
104
  setState('info');
@@ -92,11 +131,8 @@ export default function App({ project }) {
92
131
  if (!hasDevPassword) {
93
132
  setState('dev-password-input');
94
133
  }
95
- else if (!signingPassword) {
96
- setState('signing-password-input');
97
- }
98
134
  else {
99
- setState('dev');
135
+ routeToSigningStep(command);
100
136
  }
101
137
  break;
102
138
  case 'sign':
@@ -108,12 +144,7 @@ export default function App({ project }) {
108
144
  console.log('\x1b[31mSigning credentials not configured. Run svc setup-signing first.\x1b[0m');
109
145
  return;
110
146
  }
111
- if (signingPassword) {
112
- setState('sign');
113
- }
114
- else {
115
- setState('signing-password-input');
116
- }
147
+ routeToSigningStep(command);
117
148
  break;
118
149
  case 'build':
119
150
  setState('build');
@@ -146,8 +177,13 @@ export default function App({ project }) {
146
177
  if (state === 'dev-password-input') {
147
178
  return (_jsx(PasswordInput, { label: "Enter Development Password (usually Sitevision Cloud Password)", showRememberOption: Boolean(project.devProperties?.domain && project.devProperties?.username), onSubmit: handleDevPasswordSubmit, onCancel: () => setState('menu') }, "dev-password"));
148
179
  }
180
+ if (state === 'signing-password-choice') {
181
+ return (_jsx(KeychainPasswordChoice, { onUseSaved: handleUseSavedSigning, onEnterNew: handleEnterNewSigning, onCancel: () => setState('menu') }, "signing-password-choice"));
182
+ }
149
183
  if (state === 'signing-password-input') {
150
- return (_jsx(PasswordInput, { label: "Enter Signing Password (developer.sitevision.se)", showRememberOption: Boolean(project.devProperties?.signingUsername), onSubmit: handleSigningPasswordSubmit, onCancel: () => setState('menu') }, "signing-password"));
184
+ return (_jsx(PasswordInput, { label: "Enter Signing Password (developer.sitevision.se)", showRememberOption: Boolean(project.devProperties?.signingUsername), defaultRemember: Boolean(storedSigningPassword), rememberLabel: storedSigningPassword
185
+ ? 'Update saved password in OS keychain: '
186
+ : 'Save to OS keychain: ', onSubmit: handleSigningPasswordSubmit, onCancel: () => setState('menu') }, "signing-password"));
151
187
  }
152
188
  if (state === 'dev') {
153
189
  return (_jsx(DevScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: getEffectiveDevProperties(), signed: currentCommand === 'dev-signed', onBack: () => setState('menu'), onRetryCredentials: () => {
@@ -156,6 +192,8 @@ export default function App({ project }) {
156
192
  project.devProperties.password = undefined;
157
193
  if (currentCommand === 'dev-signed') {
158
194
  setSigningPassword('');
195
+ // The saved password may be what failed — don't re-offer it.
196
+ setSigningRetry(true);
159
197
  }
160
198
  setState('dev-password-input');
161
199
  }, signingCredentials: currentCommand === 'dev-signed' &&
@@ -5,7 +5,8 @@ import { StatusIndicator } from '../components/StatusIndicator.js';
5
5
  import { WebpackRunner } from '../utils/webpack-runner.js';
6
6
  import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
7
7
  import { signApp, deployApp } from '../utils/sitevision-api.js';
8
- import { getSigningPassword, setSigningPassword, setDeployPassword, } from '../utils/keychain.js';
8
+ import { setDeployPassword } from '../utils/keychain.js';
9
+ import { resolveSigningPassword } from '../utils/signing-password.js';
9
10
  import { copyStaticToBuild, createBuildZip, cleanBuild } from '../utils/zip.js';
10
11
  import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, } from '../utils/project-detection.js';
11
12
  export function DevScreen({ projectRoot, manifest, devProperties, signed, signingCredentials, onBack, onRetryCredentials, }) {
@@ -268,25 +269,11 @@ export const devCommand = {
268
269
  return;
269
270
  }
270
271
  const signingUsername = project.devProperties.signingUsername;
271
- let password = getSigningPassword(signingUsername) ||
272
- process.env['SITEVISION_SIGNING_PASSWORD'] ||
273
- '';
274
- let promptedManually = false;
275
- if (!password) {
276
- console.log('');
277
- password = await promptPassword('Signing password (developer.sitevision.se): ');
278
- promptedManually = true;
279
- }
272
+ const password = await resolveSigningPassword(signingUsername);
280
273
  if (!password) {
281
274
  console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
282
275
  return;
283
276
  }
284
- if (promptedManually) {
285
- const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
286
- if (remember) {
287
- setSigningPassword(signingUsername, password);
288
- }
289
- }
290
277
  signingCredentials = {
291
278
  username: signingUsername,
292
279
  password,
@@ -3,8 +3,7 @@ import React from 'react';
3
3
  import { render, Box, Text, useInput } from 'ink';
4
4
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
5
  import { signApp } from '../utils/sitevision-api.js';
6
- import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
7
- import { getSigningPassword, setSigningPassword } from '../utils/keychain.js';
6
+ import { resolveSigningPassword } from '../utils/signing-password.js';
8
7
  import { getZipPath, getSignedZipPath } from '../utils/project-detection.js';
9
8
  import { formatFileSize, getZipSize, zipExists } from '../utils/zip.js';
10
9
  export function SignScreen({ projectRoot, manifest, devProperties, password, onBack, onRetryCredentials, }) {
@@ -84,26 +83,11 @@ export const signCommand = {
84
83
  return;
85
84
  }
86
85
  const signingUsername = project.devProperties.signingUsername;
87
- // Try keychain first, then env var, then prompt
88
- let password = getSigningPassword(signingUsername) ||
89
- process.env['SITEVISION_SIGNING_PASSWORD'] ||
90
- '';
91
- let promptedManually = false;
92
- if (!password) {
93
- console.log('');
94
- password = await promptPassword('Signing password (developer.sitevision.se): ');
95
- promptedManually = true;
96
- }
86
+ const password = await resolveSigningPassword(signingUsername);
97
87
  if (!password) {
98
88
  console.log('\x1b[31mError: Password is required\x1b[0m');
99
89
  return;
100
90
  }
101
- if (promptedManually) {
102
- const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
103
- if (remember) {
104
- setSigningPassword(signingUsername, password);
105
- }
106
- }
107
91
  const { waitUntilExit } = render(_jsx(SignScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, password: password }));
108
92
  await waitUntilExit();
109
93
  },
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ label?: string;
3
+ onUseSaved: () => void;
4
+ onEnterNew: () => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function KeychainPasswordChoice({ label, onUseSaved, onEnterNew, onCancel, }: Props): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ const OPTIONS = [
5
+ { label: 'Use saved password from keychain', value: 'saved' },
6
+ { label: 'Enter a new password', value: 'new' },
7
+ ];
8
+ export function KeychainPasswordChoice({ label = 'A signing password is saved in your OS keychain.', onUseSaved, onEnterNew, onCancel, }) {
9
+ const [selectedIndex, setSelectedIndex] = useState(0);
10
+ useInput((_input, key) => {
11
+ if (key.upArrow) {
12
+ setSelectedIndex(prev => (prev === 0 ? OPTIONS.length - 1 : prev - 1));
13
+ }
14
+ else if (key.downArrow) {
15
+ setSelectedIndex(prev => (prev === OPTIONS.length - 1 ? 0 : prev + 1));
16
+ }
17
+ else if (key.return) {
18
+ if (selectedIndex === 0) {
19
+ onUseSaved();
20
+ }
21
+ else {
22
+ onEnterNew();
23
+ }
24
+ }
25
+ else if (key.escape) {
26
+ onCancel();
27
+ }
28
+ });
29
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: label }) }), OPTIONS.map((option, index) => (_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', option.label] }, option.value))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move, Enter to select, Esc to cancel" }) })] }));
30
+ }
@@ -1,8 +1,10 @@
1
1
  interface Props {
2
2
  label?: string;
3
3
  showRememberOption?: boolean;
4
+ defaultRemember?: boolean;
5
+ rememberLabel?: string;
4
6
  onSubmit: (password: string, remember: boolean) => void;
5
7
  onCancel: () => void;
6
8
  }
7
- export declare function PasswordInput({ label, showRememberOption, onSubmit, onCancel, }: Props): import("react").JSX.Element;
9
+ export declare function PasswordInput({ label, showRememberOption, defaultRemember, rememberLabel, onSubmit, onCancel, }: Props): import("react").JSX.Element;
8
10
  export {};
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
- export function PasswordInput({ label = 'Enter Signing Password', showRememberOption = false, onSubmit, onCancel, }) {
4
+ export function PasswordInput({ label = 'Enter Signing Password', showRememberOption = false, defaultRemember = false, rememberLabel = 'Save to OS keychain: ', onSubmit, onCancel, }) {
5
5
  const [password, setPassword] = useState('');
6
- const [remember, setRemember] = useState(false);
6
+ const [remember, setRemember] = useState(defaultRemember);
7
7
  useInput((input, key) => {
8
8
  if (key.return) {
9
9
  onSubmit(password, remember);
@@ -26,5 +26,5 @@ export function PasswordInput({ label = 'Enter Signing Password', showRememberOp
26
26
  setPassword(prev => prev + input);
27
27
  }
28
28
  });
29
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: label }) }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: '*'.repeat(password.length) }) }), showRememberOption && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Save to OS keychain: " }), _jsxs(Text, { color: remember ? 'green' : 'gray', children: ["[", remember ? 'x' : ' ', "]"] }), _jsx(Text, { dimColor: true, children: " (Tab to toggle)" })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to submit, Esc to cancel" }) })] }));
29
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: label }) }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: '*'.repeat(password.length) }) }), showRememberOption && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: rememberLabel }), _jsxs(Text, { color: remember ? 'green' : 'gray', children: ["[", remember ? 'x' : ' ', "]"] }), _jsx(Text, { dimColor: true, children: " (Tab to toggle)" })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to submit, Esc to cancel" }) })] }));
30
30
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Prompt for a yes/no answer. Returns true for y/Y, false otherwise (incl. empty / Enter).
2
+ * Prompt for a yes/no answer. Returns true for y/Y.
3
+ * Pressing Enter (empty answer) returns `defaultYes` (default: false).
3
4
  */
4
- export declare function promptYesNo(prompt: string): Promise<boolean>;
5
+ export declare function promptYesNo(prompt: string, defaultYes?: boolean): Promise<boolean>;
5
6
  /**
6
7
  * Prompt for password input with masked display
7
8
  */
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Prompt for a yes/no answer. Returns true for y/Y, false otherwise (incl. empty / Enter).
2
+ * Prompt for a yes/no answer. Returns true for y/Y.
3
+ * Pressing Enter (empty answer) returns `defaultYes` (default: false).
3
4
  */
4
- export function promptYesNo(prompt) {
5
+ export function promptYesNo(prompt, defaultYes = false) {
5
6
  return new Promise(resolve => {
6
7
  process.stdout.write(prompt);
7
8
  const stdin = process.stdin;
@@ -14,9 +15,15 @@ export function promptYesNo(prompt) {
14
15
  stdin.removeListener('data', onData);
15
16
  stdin.pause();
16
17
  process.stdout.write(`${char}\n`);
17
- if (char.charCodeAt(0) === 3) {
18
+ const charCode = char.charCodeAt(0);
19
+ if (charCode === 3) {
18
20
  process.exit();
19
21
  }
22
+ // Enter (CR/LF) or empty input → use the default
23
+ if (char === '' || charCode === 13 || charCode === 10) {
24
+ resolve(defaultYes);
25
+ return;
26
+ }
20
27
  resolve(char === 'y' || char === 'Y');
21
28
  };
22
29
  stdin.on('data', onData);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolve the signing password for the given user.
3
+ *
4
+ * Priority:
5
+ * 1. `SITEVISION_SIGNING_PASSWORD` env var (non-interactive override, no prompts).
6
+ * 2. A password saved in the OS keychain — the user is asked whether to use it
7
+ * or set a new one. Choosing "new" optionally updates the saved password.
8
+ * 3. An interactive prompt — optionally saved to the keychain.
9
+ *
10
+ * Returns an empty string if no password could be obtained.
11
+ */
12
+ export declare function resolveSigningPassword(signingUsername: string): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { getSigningPassword, setSigningPassword } from './keychain.js';
2
+ import { promptPassword, promptYesNo } from './password-prompt.js';
3
+ const SIGNING_PROMPT = 'Signing password (developer.sitevision.se): ';
4
+ /**
5
+ * Resolve the signing password for the given user.
6
+ *
7
+ * Priority:
8
+ * 1. `SITEVISION_SIGNING_PASSWORD` env var (non-interactive override, no prompts).
9
+ * 2. A password saved in the OS keychain — the user is asked whether to use it
10
+ * or set a new one. Choosing "new" optionally updates the saved password.
11
+ * 3. An interactive prompt — optionally saved to the keychain.
12
+ *
13
+ * Returns an empty string if no password could be obtained.
14
+ */
15
+ export async function resolveSigningPassword(signingUsername) {
16
+ const envPassword = process.env['SITEVISION_SIGNING_PASSWORD'];
17
+ if (envPassword)
18
+ return envPassword;
19
+ const stored = getSigningPassword(signingUsername);
20
+ if (stored) {
21
+ const useStored = await promptYesNo('Use saved signing password from keychain? (Y/n): ', true);
22
+ if (useStored)
23
+ return stored;
24
+ }
25
+ console.log('');
26
+ const password = await promptPassword(SIGNING_PROMPT);
27
+ if (!password)
28
+ return '';
29
+ const remember = await promptYesNo(stored
30
+ ? 'Update saved password in OS keychain? (y/N): '
31
+ : 'Save password to OS keychain? (y/N): ');
32
+ if (remember) {
33
+ setSigningPassword(signingUsername, password);
34
+ }
35
+ return password;
36
+ }
@@ -0,0 +1,15 @@
1
+ export type SigningStep = 'proceed' | 'choice' | 'input';
2
+ /**
3
+ * Decide what to do when a signing password is needed.
4
+ *
5
+ * - `proceed`: a password is already available this session — use it.
6
+ * - `choice`: a password is saved in the keychain — ask whether to use it or
7
+ * enter a new one.
8
+ * - `input`: prompt for a password (nothing saved, or a saved one just failed
9
+ * and we're retrying).
10
+ */
11
+ export declare function decideSigningStep(options: {
12
+ hasSessionPassword: boolean;
13
+ hasStoredPassword: boolean;
14
+ isRetry: boolean;
15
+ }): SigningStep;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Decide what to do when a signing password is needed.
3
+ *
4
+ * - `proceed`: a password is already available this session — use it.
5
+ * - `choice`: a password is saved in the keychain — ask whether to use it or
6
+ * enter a new one.
7
+ * - `input`: prompt for a password (nothing saved, or a saved one just failed
8
+ * and we're retrying).
9
+ */
10
+ export function decideSigningStep(options) {
11
+ if (options.hasSessionPassword)
12
+ return 'proceed';
13
+ if (options.hasStoredPassword && !options.isRetry)
14
+ return 'choice';
15
+ return 'input';
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "0.4.0-beta.0",
3
+ "version": "0.4.0-beta.2",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"