sitevision-cli 0.4.0-beta.1 → 0.5.0-beta.0

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' &&
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { render, Box, Text, useInput } from 'ink';
4
4
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { WebpackRunner } from '../utils/webpack-runner.js';
6
- import { copyStaticToBuild, copySrcToBuild, cleanBuild, formatFileSize, createBuildZip, getZipSize, } from '../utils/zip.js';
5
+ import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
6
+ import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
7
+ import { copyStaticToBuild, copySrcToBuild, cleanBuild, formatFileSize, createBuildZip, getZipSize, zipExists, } from '../utils/zip.js';
7
8
  import { isBundledApp, getAppType, getFullAppId, } from '../utils/project-detection.js';
8
9
  export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }) {
9
10
  const [state, setState] = React.useState({
@@ -25,8 +26,55 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
25
26
  setState({ status: 'cleaning', message: 'Cleaning build directory...' });
26
27
  cleanBuild(projectRoot);
27
28
  // Step 2: Build or copy files
29
+ if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
30
+ // No project-local webpack config: delegate the whole build to
31
+ // @sitevision/sitevision-scripts, which compiles + zips to
32
+ // dist/<appId>.zip (the path the CLI's sign/deploy already use).
33
+ if (!hasSitevisionScripts(projectRoot)) {
34
+ setState({
35
+ status: 'error',
36
+ error: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
37
+ });
38
+ return;
39
+ }
40
+ // Warn if the installed sitevision-scripts is outside the range
41
+ // the delegated build was validated against.
42
+ const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
43
+ setState({
44
+ status: 'building',
45
+ message: 'Building via sitevision-scripts...',
46
+ warning,
47
+ });
48
+ const sitevisionResult = await runSitevisionScriptsBuild(projectRoot);
49
+ if (!sitevisionResult.success) {
50
+ setState({
51
+ status: 'error',
52
+ error: `${sitevisionResult.error}\n${sitevisionResult.output.slice(-1000)}`,
53
+ warning,
54
+ });
55
+ return;
56
+ }
57
+ // sitevision-scripts already produced the zip; report it directly.
58
+ const zipPath = getDelegatedZipPath(projectRoot, manifest.id);
59
+ if (!zipExists(zipPath)) {
60
+ setState({
61
+ status: 'error',
62
+ error: `Build reported success but no zip was found at ${zipPath}.`,
63
+ warning,
64
+ });
65
+ return;
66
+ }
67
+ setState({
68
+ status: 'success',
69
+ message: 'Build complete',
70
+ zipPath,
71
+ zipSize: getZipSize(zipPath),
72
+ warning,
73
+ });
74
+ return;
75
+ }
28
76
  if (isBundled) {
29
- // Check if webpack is available
77
+ // Project ships its own webpack config: build it in-process.
30
78
  if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
31
79
  setState({
32
80
  status: 'error',
@@ -122,7 +170,7 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
122
170
  return 'Build failed';
123
171
  }
124
172
  };
125
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
173
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
126
174
  state.result.stats.assets.length > 0 && (_jsxs(Text, { dimColor: true, children: ["Assets: ", state.result.stats.assets.join(', ')] }))] })), state.status === 'success' && state.zipPath && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 Created: ", state.zipPath] }), state.zipSize !== undefined && (_jsxs(Text, { dimColor: true, children: [" Size: ", formatFileSize(state.zipSize)] }))] })), state.result?.warnings && state.result.warnings.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "Warnings:" }), state.result.warnings.slice(0, 5).map((warning, i) => (_jsx(Text, { color: "yellow", dimColor: true, children: warning.substring(0, 200) }, i))), state.result.warnings.length > 5 && (_jsxs(Text, { color: "yellow", dimColor: true, children: ["...and ", state.result.warnings.length - 5, " more"] }))] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), onBack && (state.status === 'success' || state.status === 'error') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" }) }))] }));
127
175
  }
128
176
  export const buildCommand = {
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import fs from 'fs';
3
+ import path from 'path';
2
4
  import React from 'react';
3
5
  import { render, Box, Text, useApp, useInput } from 'ink';
4
6
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { WebpackRunner } from '../utils/webpack-runner.js';
7
+ import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
8
+ import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
6
9
  import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
7
10
  import { signApp, deployApp } from '../utils/sitevision-api.js';
8
11
  import { setDeployPassword } from '../utils/keychain.js';
@@ -26,23 +29,13 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
26
29
  }
27
30
  });
28
31
  const webpackRunnerRef = React.useRef(null);
29
- const handleBuildComplete = React.useCallback(async (result) => {
30
- if (!result.success) {
31
- setState(prev => ({
32
- ...prev,
33
- status: 'error',
34
- message: result.errors?.join('\n') || 'Build failed',
35
- error: result.errors?.join('\n'),
36
- }));
37
- return;
38
- }
32
+ const watchersRef = React.useRef([]);
33
+ const debounceTimerRef = React.useRef(null);
34
+ const isBuildingRef = React.useRef(false);
35
+ const pendingRebuildRef = React.useRef(false);
36
+ // Sign (if needed) and deploy an already-built zip, updating UI state.
37
+ const signAndDeploy = React.useCallback(async (zipPath, buildTime) => {
39
38
  try {
40
- // Copy static files
41
- copyStaticToBuild(projectRoot);
42
- // Create zip
43
- const appId = getFullAppId(manifest.id);
44
- await createBuildZip(projectRoot, appId);
45
- const zipPath = getZipPath(projectRoot, manifest);
46
39
  let deployZipPath = zipPath;
47
40
  // Sign if needed
48
41
  if (signed && signingCredentials) {
@@ -94,7 +87,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
94
87
  status: 'ready',
95
88
  message: 'Deployed. Watching for changes...',
96
89
  buildCount: prev.buildCount + 1,
97
- lastBuildTime: result.stats?.time,
90
+ lastBuildTime: buildTime,
98
91
  error: undefined,
99
92
  }));
100
93
  }
@@ -107,14 +100,122 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
107
100
  }));
108
101
  }
109
102
  }, [projectRoot, manifest, devProperties, signed, signingCredentials]);
103
+ // In-house webpack path: copy static, zip, then sign + deploy.
104
+ const handleBuildComplete = React.useCallback(async (result) => {
105
+ if (!result.success) {
106
+ setState(prev => ({
107
+ ...prev,
108
+ status: 'error',
109
+ message: result.errors?.join('\n') || 'Build failed',
110
+ error: result.errors?.join('\n'),
111
+ }));
112
+ return;
113
+ }
114
+ try {
115
+ copyStaticToBuild(projectRoot);
116
+ const appId = getFullAppId(manifest.id);
117
+ await createBuildZip(projectRoot, appId);
118
+ await signAndDeploy(getZipPath(projectRoot, manifest), result.stats?.time);
119
+ }
120
+ catch (error) {
121
+ setState(prev => ({
122
+ ...prev,
123
+ status: 'error',
124
+ message: error instanceof Error ? error.message : String(error),
125
+ error: error instanceof Error ? error.message : String(error),
126
+ }));
127
+ }
128
+ }, [projectRoot, manifest, signAndDeploy]);
129
+ // Delegated path: full `sitevision-scripts build` then sign + deploy.
130
+ // Coalesces overlapping triggers so a save mid-build queues one rebuild.
131
+ const runDelegatedBuild = React.useCallback(async () => {
132
+ if (isBuildingRef.current) {
133
+ pendingRebuildRef.current = true;
134
+ return;
135
+ }
136
+ isBuildingRef.current = true;
137
+ const buildOnce = async () => {
138
+ pendingRebuildRef.current = false;
139
+ setState(prev => ({
140
+ ...prev,
141
+ status: 'building',
142
+ message: 'Building via sitevision-scripts...',
143
+ }));
144
+ const result = await runSitevisionScriptsBuild(projectRoot);
145
+ if (result.success) {
146
+ await signAndDeploy(getDelegatedZipPath(projectRoot, manifest.id));
147
+ }
148
+ else {
149
+ setState(prev => ({
150
+ ...prev,
151
+ status: 'error',
152
+ message: result.error ?? 'Build failed',
153
+ error: `${result.error}\n${result.output.slice(-1000)}`,
154
+ }));
155
+ }
156
+ if (pendingRebuildRef.current) {
157
+ await buildOnce();
158
+ }
159
+ };
160
+ try {
161
+ await buildOnce();
162
+ }
163
+ finally {
164
+ isBuildingRef.current = false;
165
+ }
166
+ }, [projectRoot, manifest, signAndDeploy]);
167
+ // Watch source files and trigger a delegated rebuild (debounced).
168
+ const startFileWatcher = React.useCallback(() => {
169
+ const targets = [
170
+ 'src',
171
+ 'static',
172
+ 'i18n',
173
+ 'resource',
174
+ 'config',
175
+ 'manifest.json',
176
+ ]
177
+ .map(name => path.join(projectRoot, name))
178
+ .filter(target => fs.existsSync(target));
179
+ for (const target of targets) {
180
+ const isDir = fs.statSync(target).isDirectory();
181
+ const watcher = fs.watch(target, { recursive: isDir }, () => {
182
+ if (debounceTimerRef.current) {
183
+ clearTimeout(debounceTimerRef.current);
184
+ }
185
+ debounceTimerRef.current = setTimeout(() => {
186
+ void runDelegatedBuild();
187
+ }, 300);
188
+ });
189
+ watchersRef.current.push(watcher);
190
+ }
191
+ }, [projectRoot, runDelegatedBuild]);
110
192
  React.useEffect(() => {
111
193
  const isBundled = isBundledApp(manifest);
112
194
  async function startWatch() {
113
195
  try {
114
196
  // Clean build directory
115
197
  cleanBuild(projectRoot);
116
- if (isBundled) {
117
- // Check if webpack is available
198
+ if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
199
+ // No local webpack config: delegate each build to
200
+ // sitevision-scripts (full rebuild) and watch source files
201
+ // ourselves, keeping the CLI's own sign + deploy flow.
202
+ if (!hasSitevisionScripts(projectRoot)) {
203
+ setState({
204
+ status: 'error',
205
+ message: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
206
+ buildCount: 0,
207
+ webpackReady: false,
208
+ error: 'No build pipeline available',
209
+ });
210
+ return;
211
+ }
212
+ const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
213
+ setState(prev => ({ ...prev, webpackReady: true, warning }));
214
+ startFileWatcher();
215
+ await runDelegatedBuild();
216
+ }
217
+ else if (isBundled) {
218
+ // Project ships its own webpack config: incremental in-process watch.
118
219
  if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
119
220
  setState({
120
221
  status: 'error',
@@ -175,14 +276,30 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
175
276
  if (webpackRunnerRef.current) {
176
277
  webpackRunnerRef.current.close().catch(() => { });
177
278
  }
279
+ if (debounceTimerRef.current) {
280
+ clearTimeout(debounceTimerRef.current);
281
+ }
282
+ for (const watcher of watchersRef.current) {
283
+ watcher.close();
284
+ }
285
+ watchersRef.current = [];
178
286
  };
179
- }, [projectRoot, manifest, handleBuildComplete]);
287
+ }, [
288
+ projectRoot,
289
+ manifest,
290
+ handleBuildComplete,
291
+ runDelegatedBuild,
292
+ startFileWatcher,
293
+ ]);
180
294
  // Handle Ctrl+C
181
295
  React.useEffect(() => {
182
296
  const handleExit = () => {
183
297
  if (webpackRunnerRef.current) {
184
298
  webpackRunnerRef.current.close().catch(() => { });
185
299
  }
300
+ for (const watcher of watchersRef.current) {
301
+ watcher.close();
302
+ }
186
303
  exit();
187
304
  };
188
305
  process.on('SIGINT', handleExit);
@@ -220,7 +337,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
220
337
  return 'Error';
221
338
  }
222
339
  };
223
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
340
+ 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
224
341
  ? ` | Last build: ${state.lastBuildTime}ms`
225
342
  : '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [manifest.name, " v", manifest.version] }) }), 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" }))] })] }));
226
343
  }
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { getAppType } from '../utils/project-detection.js';
4
+ import { checkSitevisionScriptsCompatibility } from '../utils/sitevision-scripts-runner.js';
4
5
  export function InfoScreen({ project, onBack }) {
5
6
  const appType = getAppType(project.manifest);
7
+ const scriptsCompat = checkSitevisionScriptsCompatibility(project.root);
6
8
  useInput((input, key) => {
7
9
  if (key.escape || input === 'q' || key.return) {
8
10
  onBack();
9
11
  }
10
12
  });
11
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision Project Information" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: project.manifest.name })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ID: " }), _jsx(Text, { children: project.manifest.id })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Version: " }), _jsx(Text, { children: project.manifest.version })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Type: " }), _jsx(Text, { color: "green", children: project.manifest.type }), _jsxs(Text, { dimColor: true, children: [" (", appType, ")"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Bundled: " }), _jsx(Text, { children: project.manifest.bundled ? 'Yes' : 'No' })] })] }), project.hasDevProperties && project.devProperties && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Development Configuration" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Domain: " }), _jsx(Text, { children: project.devProperties.domain })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Site: " }), _jsx(Text, { children: project.devProperties.siteName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Addon: " }), _jsx(Text, { children: project.devProperties.addonName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Username: " }), _jsx(Text, { children: project.devProperties.username })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Use HTTP: " }), _jsx(Text, { children: project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No' })] })] })] })), !project.hasDevProperties && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 No dev properties found. Run setup-dev-properties to configure." }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Project Root: " }), _jsx(Text, { dimColor: true, children: project.root })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: "Press Enter or ESC to return to menu" }) })] }));
13
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision Project Information" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: project.manifest.name })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ID: " }), _jsx(Text, { children: project.manifest.id })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Version: " }), _jsx(Text, { children: project.manifest.version })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Type: " }), _jsx(Text, { color: "green", children: project.manifest.type }), _jsxs(Text, { dimColor: true, children: [" (", appType, ")"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Bundled: " }), _jsx(Text, { children: project.manifest.bundled ? 'Yes' : 'No' })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Build Tooling" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "sitevision-scripts: " }), _jsx(Text, { children: scriptsCompat.installed ?? 'not installed' }), _jsxs(Text, { dimColor: true, children: [" (supported ", scriptsCompat.supportedRange, ")"] })] }), scriptsCompat.warning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", scriptsCompat.warning] }) }))] }), project.hasDevProperties && project.devProperties && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Development Configuration" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Domain: " }), _jsx(Text, { children: project.devProperties.domain })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Site: " }), _jsx(Text, { children: project.devProperties.siteName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Addon: " }), _jsx(Text, { children: project.devProperties.addonName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Username: " }), _jsx(Text, { children: project.devProperties.username })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Use HTTP: " }), _jsx(Text, { children: project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No' })] })] })] })), !project.hasDevProperties && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 No dev properties found. Run setup-dev-properties to configure." }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Project Root: " }), _jsx(Text, { dimColor: true, children: project.root })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: "Press Enter or ESC to return to menu" }) })] }));
12
14
  }
@@ -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
  }
@@ -20,11 +20,3 @@ export declare class ProcessRunner extends EventEmitter {
20
20
  kill(): void;
21
21
  getOutput(): ProcessOutput[];
22
22
  }
23
- /**
24
- * Run a sitevision-scripts command in the project directory
25
- */
26
- export declare function runSitevisionScript(scriptName: string, args?: string[], projectRoot?: string): ProcessRunner;
27
- /**
28
- * Run an NPM script
29
- */
30
- export declare function runNpmScript(scriptName: string, args?: string[], projectRoot?: string, customEnv?: Record<string, string>): ProcessRunner;
@@ -72,18 +72,3 @@ export class ProcessRunner extends EventEmitter {
72
72
  return this.output;
73
73
  }
74
74
  }
75
- /**
76
- * Run a sitevision-scripts command in the project directory
77
- */
78
- export function runSitevisionScript(scriptName, args = [], projectRoot) {
79
- // Check if sitevision-scripts is available locally
80
- const runner = new ProcessRunner('npm', ['run', scriptName, '--', ...args], projectRoot);
81
- return runner;
82
- }
83
- /**
84
- * Run an NPM script
85
- */
86
- export function runNpmScript(scriptName, args = [], projectRoot, customEnv) {
87
- const runner = new ProcessRunner('npm', ['run', scriptName, ...args], projectRoot, false, customEnv);
88
- return runner;
89
- }
@@ -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
+ }
@@ -13,6 +13,38 @@ import type { SigningCredentials, DeployConfig, ProductionDeployConfig, SigningR
13
13
  * Create Basic Auth header value
14
14
  */
15
15
  declare function createBasicAuth(username: string, password: string): string;
16
+ /**
17
+ * Make an HTTP/HTTPS request
18
+ */
19
+ export declare function makeRequest(url: string, options: {
20
+ method: string;
21
+ headers?: Record<string, string>;
22
+ body?: Buffer;
23
+ auth?: {
24
+ username: string;
25
+ password: string;
26
+ };
27
+ timeoutMs?: number;
28
+ }): Promise<{
29
+ statusCode: number;
30
+ body: Buffer;
31
+ headers: Record<string, string>;
32
+ }>;
33
+ /**
34
+ * Whether an HTTP status is worth retrying (transient server-side failures).
35
+ */
36
+ export declare function isRetryableStatus(statusCode: number): boolean;
37
+ /**
38
+ * Summarize a non-success response body for error messages.
39
+ * Avoids dumping raw bytes (e.g. an HTML error page or a binary blob) by
40
+ * trimming text bodies and labelling binary ones by their content type.
41
+ */
42
+ export declare function summarizeErrorBody(body: Buffer, headers: Record<string, string>): string;
43
+ /**
44
+ * Check that a buffer begins with the ZIP magic bytes. Used to fail fast when
45
+ * the signing endpoint returns an error page with HTTP 200.
46
+ */
47
+ export declare function looksLikeZip(body: Buffer): boolean;
16
48
  /**
17
49
  * Sign an app via developer.sitevision.se
18
50
  *