sitevision-cli 0.1.1 → 0.3.1-beta.1
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 +15 -4
- package/dist/cli.js +25 -4
- package/dist/commands/deploy.js +18 -1
- package/dist/commands/dev.js +34 -6
- package/dist/commands/setup-signing.js +2 -2
- package/dist/commands/sign.js +17 -4
- package/dist/components/DevPropertiesForm.js +12 -3
- package/dist/components/PasswordInput.d.ts +3 -2
- package/dist/components/PasswordInput.js +14 -2
- package/dist/components/SetupFlow.js +27 -2
- package/dist/types/index.d.ts +7 -2
- package/dist/utils/keychain.d.ts +6 -0
- package/dist/utils/keychain.js +63 -0
- package/dist/utils/password-prompt.d.ts +4 -0
- package/dist/utils/password-prompt.js +24 -0
- package/dist/utils/project-detection.d.ts +12 -17
- package/dist/utils/project-detection.js +43 -3
- package/dist/utils/version-check.d.ts +9 -0
- package/dist/utils/version-check.js +60 -0
- package/dist/utils/webpack-runner.d.ts +1 -9
- package/dist/utils/webpack-runner.js +1 -28
- package/package.json +6 -5
- package/readme.md +20 -4
package/dist/app.js
CHANGED
|
@@ -8,6 +8,7 @@ import { BuildScreen } from './commands/build.js';
|
|
|
8
8
|
import { DeployScreen } from './commands/deploy.js';
|
|
9
9
|
import { SignScreen } from './commands/sign.js';
|
|
10
10
|
import { SigningPropertiesForm } from './components/SigningPropertiesForm.js';
|
|
11
|
+
import { setDeployPassword as saveDeployPassword, setSigningPassword as saveSigningPassword } from './utils/keychain.js';
|
|
11
12
|
export default function App({ project }) {
|
|
12
13
|
const [state, setState] = useState('setup');
|
|
13
14
|
const [currentCommand, setCurrentCommand] = useState('');
|
|
@@ -23,8 +24,11 @@ export default function App({ project }) {
|
|
|
23
24
|
return project.devProperties;
|
|
24
25
|
return { ...project.devProperties, password: devPassword };
|
|
25
26
|
};
|
|
26
|
-
const handleDevPasswordSubmit = (password) => {
|
|
27
|
+
const handleDevPasswordSubmit = (password, remember) => {
|
|
27
28
|
setDevPassword(password);
|
|
29
|
+
if (remember && project.devProperties?.domain && project.devProperties.username && password) {
|
|
30
|
+
saveDeployPassword(project.devProperties.domain, project.devProperties.username, password);
|
|
31
|
+
}
|
|
28
32
|
// Continue to the intended command
|
|
29
33
|
if (currentCommand === 'dev' || currentCommand === 'dev-signed') {
|
|
30
34
|
if (currentCommand === 'dev-signed' && !signingPassword) {
|
|
@@ -38,8 +42,11 @@ export default function App({ project }) {
|
|
|
38
42
|
setState('deploy');
|
|
39
43
|
}
|
|
40
44
|
};
|
|
41
|
-
const handleSigningPasswordSubmit = (password) => {
|
|
45
|
+
const handleSigningPasswordSubmit = (password, remember) => {
|
|
42
46
|
setSigningPassword(password);
|
|
47
|
+
if (remember && project.devProperties?.signingUsername && password) {
|
|
48
|
+
saveSigningPassword(project.devProperties.signingUsername, password);
|
|
49
|
+
}
|
|
43
50
|
if (currentCommand === 'dev-signed') {
|
|
44
51
|
setState('dev');
|
|
45
52
|
}
|
|
@@ -133,14 +140,16 @@ export default function App({ project }) {
|
|
|
133
140
|
return React.createElement(InfoScreen, { project: project, onBack: () => setState('menu') });
|
|
134
141
|
}
|
|
135
142
|
if (state === 'dev-password-input') {
|
|
136
|
-
return (React.createElement(PasswordInput, { key: "dev-password", label: "Enter Development Password (usually Sitevision Cloud Password)", onSubmit: handleDevPasswordSubmit, onCancel: () => setState('menu') }));
|
|
143
|
+
return (React.createElement(PasswordInput, { key: "dev-password", label: "Enter Development Password (usually Sitevision Cloud Password)", showRememberOption: Boolean(project.devProperties?.domain && project.devProperties?.username), onSubmit: handleDevPasswordSubmit, onCancel: () => setState('menu') }));
|
|
137
144
|
}
|
|
138
145
|
if (state === 'signing-password-input') {
|
|
139
|
-
return (React.createElement(PasswordInput, { key: "signing-password", label: "Enter Signing Password (developer.sitevision.se)", onSubmit: handleSigningPasswordSubmit, onCancel: () => setState('menu') }));
|
|
146
|
+
return (React.createElement(PasswordInput, { key: "signing-password", label: "Enter Signing Password (developer.sitevision.se)", showRememberOption: Boolean(project.devProperties?.signingUsername), onSubmit: handleSigningPasswordSubmit, onCancel: () => setState('menu') }));
|
|
140
147
|
}
|
|
141
148
|
if (state === 'dev') {
|
|
142
149
|
return (React.createElement(DevScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: getEffectiveDevProperties(), signed: currentCommand === 'dev-signed', onBack: () => setState('menu'), onRetryCredentials: () => {
|
|
143
150
|
setDevPassword('');
|
|
151
|
+
if (project.devProperties)
|
|
152
|
+
project.devProperties.password = undefined;
|
|
144
153
|
if (currentCommand === 'dev-signed') {
|
|
145
154
|
setSigningPassword('');
|
|
146
155
|
}
|
|
@@ -165,6 +174,8 @@ export default function App({ project }) {
|
|
|
165
174
|
if (state === 'deploy') {
|
|
166
175
|
return (React.createElement(DeployScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: getEffectiveDevProperties(), force: currentCommand === 'deploy-force', production: currentCommand === 'deploy-production', activate: currentCommand === 'deploy-production', onBack: () => setState('menu'), onRetryCredentials: () => {
|
|
167
176
|
setDevPassword('');
|
|
177
|
+
if (project.devProperties)
|
|
178
|
+
project.devProperties.password = undefined;
|
|
168
179
|
setState('dev-password-input');
|
|
169
180
|
} }));
|
|
170
181
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,12 @@ 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 updateNotifier from 'update-notifier';
|
|
8
7
|
import App from './app.js';
|
|
9
8
|
import { getCommand } from './commands/index.js';
|
|
10
|
-
import { requireProject } from './utils/project-detection.js';
|
|
11
|
-
|
|
9
|
+
import { requireProject, migrateLegacyPassword } from './utils/project-detection.js';
|
|
10
|
+
import { promptYesNo } from './utils/password-prompt.js';
|
|
11
|
+
import { checkForUpdate } from './utils/version-check.js';
|
|
12
12
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
13
|
-
updateNotifier({ pkg }).notify();
|
|
14
13
|
const cli = meow(`
|
|
15
14
|
Usage
|
|
16
15
|
$ svc Start interactive menu
|
|
@@ -55,6 +54,12 @@ const cli = meow(`
|
|
|
55
54
|
});
|
|
56
55
|
const [commandName, ...args] = cli.input;
|
|
57
56
|
async function main() {
|
|
57
|
+
// Show the CLI version on startup, and check npm for a newer release.
|
|
58
|
+
console.log(`\x1b[36msvc v${pkg.version}\x1b[0m`);
|
|
59
|
+
const latestVersion = await checkForUpdate(pkg.name, pkg.version);
|
|
60
|
+
if (latestVersion) {
|
|
61
|
+
console.log(`\x1b[33m ↑ update available: ${pkg.version} → ${latestVersion} (run: npm i -g ${pkg.name})\x1b[0m`);
|
|
62
|
+
}
|
|
58
63
|
// Check if we're in a Sitevision project
|
|
59
64
|
const project = (() => {
|
|
60
65
|
try {
|
|
@@ -87,6 +92,22 @@ async function main() {
|
|
|
87
92
|
" to see available commands")));
|
|
88
93
|
process.exit(1);
|
|
89
94
|
}
|
|
95
|
+
// Offer to migrate a legacy plaintext password into the OS keychain.
|
|
96
|
+
// Interactive `svc` handles this in SetupFlow; this covers direct commands
|
|
97
|
+
// (svc deploy/dev/sign/…). Skip on non-TTY stdin (e.g. CI) where prompting
|
|
98
|
+
// would fail — the plaintext password is still used for this run.
|
|
99
|
+
if (project.hasLegacyPassword && process.stdin.isTTY) {
|
|
100
|
+
console.log('\n\x1b[33m⚠ Plaintext password found in .dev_properties.json\x1b[0m');
|
|
101
|
+
const move = await promptYesNo('Move it to the OS keychain and remove it from the file? (y/N): ');
|
|
102
|
+
if (move) {
|
|
103
|
+
if (migrateLegacyPassword(project)) {
|
|
104
|
+
console.log('\x1b[32m✓ Password moved to keychain.\x1b[0m\n');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log('\x1b[31mCould not access keychain; leaving the file unchanged.\x1b[0m\n');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
90
111
|
// Execute the command
|
|
91
112
|
await command.execute({
|
|
92
113
|
project,
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,6 +4,8 @@ import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
|
4
4
|
import { deployApp, deployProduction } from '../utils/sitevision-api.js';
|
|
5
5
|
import { getZipPath, getSignedZipPath, getAppType, } from '../utils/project-detection.js';
|
|
6
6
|
import { zipExists } from '../utils/zip.js';
|
|
7
|
+
import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
|
|
8
|
+
import { setDeployPassword } from '../utils/keychain.js';
|
|
7
9
|
export function DeployScreen({ projectRoot, manifest, devProperties, force, production, activate, signingPassword, onBack, onRetryCredentials, }) {
|
|
8
10
|
const [state, setState] = React.useState({
|
|
9
11
|
status: 'deploying',
|
|
@@ -144,9 +146,24 @@ export const deployCommand = {
|
|
|
144
146
|
// Check if dev properties are configured
|
|
145
147
|
if (!project.hasDevProperties || !project.devProperties) {
|
|
146
148
|
console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
|
|
147
|
-
console.log('Create a .dev_properties.json file with domain, siteName, addonName, username,
|
|
149
|
+
console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
|
|
148
150
|
return;
|
|
149
151
|
}
|
|
152
|
+
// Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
|
|
153
|
+
if (!project.devProperties.password) {
|
|
154
|
+
const { domain, username } = project.devProperties;
|
|
155
|
+
console.log('');
|
|
156
|
+
const password = await promptPassword(`Deploy password for ${username}@${domain}: `);
|
|
157
|
+
if (!password) {
|
|
158
|
+
console.log('\x1b[31mError: Password is required\x1b[0m');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
|
|
162
|
+
if (remember && domain && username) {
|
|
163
|
+
setDeployPassword(domain, username, password);
|
|
164
|
+
}
|
|
165
|
+
project.devProperties.password = password;
|
|
166
|
+
}
|
|
150
167
|
const production = Boolean(flags['production']);
|
|
151
168
|
const force = Boolean(flags['force']);
|
|
152
169
|
const activate = Boolean(flags['activate']);
|
package/dist/commands/dev.js
CHANGED
|
@@ -2,8 +2,9 @@ import React from 'react';
|
|
|
2
2
|
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
3
3
|
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
4
4
|
import { WebpackRunner } from '../utils/webpack-runner.js';
|
|
5
|
-
import { promptPassword } from '../utils/password-prompt.js';
|
|
5
|
+
import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
|
|
6
6
|
import { signApp, deployApp } from '../utils/sitevision-api.js';
|
|
7
|
+
import { getSigningPassword, setSigningPassword, setDeployPassword } from '../utils/keychain.js';
|
|
7
8
|
import { copyStaticToBuild, createBuildZip, cleanBuild, } from '../utils/zip.js';
|
|
8
9
|
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, } from '../utils/project-detection.js';
|
|
9
10
|
export function DevScreen({ projectRoot, manifest, devProperties, signed, signingCredentials, onBack, onRetryCredentials, }) {
|
|
@@ -261,26 +262,53 @@ export const devCommand = {
|
|
|
261
262
|
// Check if dev properties are configured
|
|
262
263
|
if (!project.hasDevProperties || !project.devProperties) {
|
|
263
264
|
console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
|
|
264
|
-
console.log('Create a .dev_properties.json file with domain, siteName, addonName, username,
|
|
265
|
+
console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
|
|
265
266
|
return;
|
|
266
267
|
}
|
|
268
|
+
// Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
|
|
269
|
+
if (!project.devProperties.password) {
|
|
270
|
+
const { domain, username } = project.devProperties;
|
|
271
|
+
console.log('');
|
|
272
|
+
const pw = await promptPassword(`Deploy password for ${username}@${domain}: `);
|
|
273
|
+
if (!pw) {
|
|
274
|
+
console.log('\x1b[31mError: Password is required\x1b[0m');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
|
|
278
|
+
if (remember && domain && username) {
|
|
279
|
+
setDeployPassword(domain, username, pw);
|
|
280
|
+
}
|
|
281
|
+
project.devProperties.password = pw;
|
|
282
|
+
}
|
|
267
283
|
const signed = Boolean(flags['signed']);
|
|
268
284
|
let signingCredentials;
|
|
269
|
-
// If signed mode,
|
|
285
|
+
// If signed mode, resolve signing password (keychain → env → prompt)
|
|
270
286
|
if (signed) {
|
|
271
287
|
if (!project.hasSigningProperties || !project.devProperties.signingUsername) {
|
|
272
288
|
console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
|
|
273
289
|
console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
|
|
274
290
|
return;
|
|
275
291
|
}
|
|
276
|
-
|
|
277
|
-
|
|
292
|
+
const signingUsername = project.devProperties.signingUsername;
|
|
293
|
+
let password = getSigningPassword(signingUsername) || process.env['SITEVISION_SIGNING_PASSWORD'] || '';
|
|
294
|
+
let promptedManually = false;
|
|
295
|
+
if (!password) {
|
|
296
|
+
console.log('');
|
|
297
|
+
password = await promptPassword('Signing password (developer.sitevision.se): ');
|
|
298
|
+
promptedManually = true;
|
|
299
|
+
}
|
|
278
300
|
if (!password) {
|
|
279
301
|
console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
|
|
280
302
|
return;
|
|
281
303
|
}
|
|
304
|
+
if (promptedManually) {
|
|
305
|
+
const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
|
|
306
|
+
if (remember) {
|
|
307
|
+
setSigningPassword(signingUsername, password);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
282
310
|
signingCredentials = {
|
|
283
|
-
username:
|
|
311
|
+
username: signingUsername,
|
|
284
312
|
password,
|
|
285
313
|
certificateName: project.devProperties.certificateName,
|
|
286
314
|
};
|
|
@@ -64,8 +64,8 @@ export const setupSigningCommand = {
|
|
|
64
64
|
certificateName = defaultCertName;
|
|
65
65
|
}
|
|
66
66
|
rl.close();
|
|
67
|
-
// Update dev properties (
|
|
68
|
-
const { signingPassword:
|
|
67
|
+
// Update dev properties (strip any plaintext passwords — they live in the keychain)
|
|
68
|
+
const { signingPassword: _signingRemoved, password: _passwordRemoved, ...cleanedProperties } = existingProperties;
|
|
69
69
|
const updatedProperties = {
|
|
70
70
|
...cleanedProperties,
|
|
71
71
|
signingUsername,
|
package/dist/commands/sign.js
CHANGED
|
@@ -2,7 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import { render, Box, Text, useInput } from 'ink';
|
|
3
3
|
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
4
4
|
import { signApp } from '../utils/sitevision-api.js';
|
|
5
|
-
import { promptPassword } from '../utils/password-prompt.js';
|
|
5
|
+
import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
|
|
6
|
+
import { getSigningPassword, setSigningPassword } from '../utils/keychain.js';
|
|
6
7
|
import { getZipPath, getSignedZipPath, } from '../utils/project-detection.js';
|
|
7
8
|
import { formatFileSize, getZipSize, zipExists } from '../utils/zip.js';
|
|
8
9
|
export function SignScreen({ projectRoot, manifest, devProperties, password, onBack, onRetryCredentials }) {
|
|
@@ -90,13 +91,25 @@ export const signCommand = {
|
|
|
90
91
|
console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const signingUsername = project.devProperties.signingUsername;
|
|
95
|
+
// Try keychain first, then env var, then prompt
|
|
96
|
+
let password = getSigningPassword(signingUsername) || process.env['SITEVISION_SIGNING_PASSWORD'] || '';
|
|
97
|
+
let promptedManually = false;
|
|
98
|
+
if (!password) {
|
|
99
|
+
console.log('');
|
|
100
|
+
password = await promptPassword('Signing password (developer.sitevision.se): ');
|
|
101
|
+
promptedManually = true;
|
|
102
|
+
}
|
|
96
103
|
if (!password) {
|
|
97
104
|
console.log('\x1b[31mError: Password is required\x1b[0m');
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
107
|
+
if (promptedManually) {
|
|
108
|
+
const remember = await promptYesNo('Save password to OS keychain? (y/N): ');
|
|
109
|
+
if (remember) {
|
|
110
|
+
setSigningPassword(signingUsername, password);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
100
113
|
const { waitUntilExit } = render(React.createElement(SignScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, password: password }));
|
|
101
114
|
await waitUntilExit();
|
|
102
115
|
},
|
|
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { TextInput } from './TextInput.js';
|
|
4
4
|
import { writeDevProperties } from '../utils/project-detection.js';
|
|
5
|
+
import { setDeployPassword, deleteDeployPassword } from '../utils/keychain.js';
|
|
5
6
|
const STEPS = [
|
|
6
7
|
{ id: 'domain', label: 'Domain' },
|
|
7
8
|
{ id: 'siteName', label: 'Site Name' },
|
|
@@ -28,8 +29,16 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
|
|
|
28
29
|
setStepIndex(stepIndex + 1);
|
|
29
30
|
}
|
|
30
31
|
else {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const finalProperties = newProperties;
|
|
33
|
+
const { password, domain, username } = finalProperties;
|
|
34
|
+
if (password && domain && username) {
|
|
35
|
+
setDeployPassword(domain, username, password);
|
|
36
|
+
}
|
|
37
|
+
else if (domain && username) {
|
|
38
|
+
// Empty password — clear any stale keychain entry so deploy falls through to prompt
|
|
39
|
+
deleteDeployPassword(domain, username);
|
|
40
|
+
}
|
|
41
|
+
writeDevProperties(projectRoot, finalProperties);
|
|
33
42
|
onComplete();
|
|
34
43
|
}
|
|
35
44
|
};
|
|
@@ -44,7 +53,7 @@ export function DevPropertiesForm({ projectRoot, initialProperties, packageJson,
|
|
|
44
53
|
case 'username':
|
|
45
54
|
return (React.createElement(TextInput, { key: "username", label: "Username (usually your Sitevision Cloud email)", defaultValue: properties.username, onSubmit: (value) => handleNext('username', value), onCancel: onCancel }));
|
|
46
55
|
case 'password':
|
|
47
|
-
return (React.createElement(TextInput, { key: "password", label: "Password (
|
|
56
|
+
return (React.createElement(TextInput, { key: "password", 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 }));
|
|
48
57
|
case 'useHTTP':
|
|
49
58
|
return (React.createElement(BooleanInput, { key: "useHTTP", label: "Use HTTP for deployment? (y/n)", defaultValue: properties.useHTTPForDevDeploy, onSubmit: (value) => handleNext('useHTTPForDevDeploy', value) }));
|
|
50
59
|
default:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface Props {
|
|
3
3
|
label?: string;
|
|
4
|
-
|
|
4
|
+
showRememberOption?: boolean;
|
|
5
|
+
onSubmit: (password: string, remember: boolean) => void;
|
|
5
6
|
onCancel: () => void;
|
|
6
7
|
}
|
|
7
|
-
export declare function PasswordInput({ label, onSubmit, onCancel }: Props): React.JSX.Element;
|
|
8
|
+
export declare function PasswordInput({ label, showRememberOption, onSubmit, onCancel }: Props): React.JSX.Element;
|
|
8
9
|
export {};
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
export function PasswordInput({ label = 'Enter Signing Password', onSubmit, onCancel }) {
|
|
3
|
+
export function PasswordInput({ label = 'Enter Signing Password', showRememberOption = false, onSubmit, onCancel }) {
|
|
4
4
|
const [password, setPassword] = useState('');
|
|
5
|
+
const [remember, setRemember] = useState(false);
|
|
5
6
|
useInput((input, key) => {
|
|
6
7
|
if (key.return) {
|
|
7
|
-
onSubmit(password);
|
|
8
|
+
onSubmit(password, remember);
|
|
8
9
|
return;
|
|
9
10
|
}
|
|
10
11
|
if (key.escape) {
|
|
11
12
|
onCancel();
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
15
|
+
if (key.tab && showRememberOption) {
|
|
16
|
+
setRemember((prev) => !prev);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
14
19
|
if (key.delete || key.backspace) {
|
|
15
20
|
setPassword((prev) => prev.slice(0, -1));
|
|
16
21
|
return;
|
|
@@ -25,6 +30,13 @@ export function PasswordInput({ label = 'Enter Signing Password', onSubmit, onCa
|
|
|
25
30
|
React.createElement(Text, { bold: true, color: "cyan" }, label)),
|
|
26
31
|
React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1 },
|
|
27
32
|
React.createElement(Text, null, '*'.repeat(password.length))),
|
|
33
|
+
showRememberOption && (React.createElement(Box, { marginTop: 1 },
|
|
34
|
+
React.createElement(Text, { dimColor: true }, "Save to OS keychain: "),
|
|
35
|
+
React.createElement(Text, { color: remember ? 'green' : 'gray' },
|
|
36
|
+
"[",
|
|
37
|
+
remember ? 'x' : ' ',
|
|
38
|
+
"]"),
|
|
39
|
+
React.createElement(Text, { dimColor: true }, " (Tab to toggle)"))),
|
|
28
40
|
React.createElement(Box, { marginTop: 1 },
|
|
29
41
|
React.createElement(Text, { dimColor: true }, "Press Enter to submit, Esc to cancel"))));
|
|
30
42
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import { getAppType } from '../utils/project-detection.js';
|
|
3
|
+
import { getAppType, migrateLegacyPassword } from '../utils/project-detection.js';
|
|
4
4
|
import { ProcessRunner } from '../utils/process-runner.js';
|
|
5
5
|
import { ProcessOutputComponent } from './ProcessOutput.js';
|
|
6
6
|
import { StatusIndicator } from './StatusIndicator.js';
|
|
@@ -23,7 +23,12 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
23
23
|
}
|
|
24
24
|
else if (step === 'check-dev-properties') {
|
|
25
25
|
if (project.hasDevProperties) {
|
|
26
|
-
|
|
26
|
+
if (project.hasLegacyPassword) {
|
|
27
|
+
setStep('confirm-password-migration');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
setStep('check-signing-properties');
|
|
31
|
+
}
|
|
27
32
|
}
|
|
28
33
|
else {
|
|
29
34
|
setStep('confirm-dev-setup');
|
|
@@ -75,6 +80,15 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
75
80
|
setStep('check-signing-properties');
|
|
76
81
|
}
|
|
77
82
|
}
|
|
83
|
+
else if (step === 'confirm-password-migration') {
|
|
84
|
+
if (input === 'y' || input === 'Y') {
|
|
85
|
+
migrateLegacyPassword(project);
|
|
86
|
+
setStep('check-signing-properties');
|
|
87
|
+
}
|
|
88
|
+
else if (input === 'n' || input === 'N') {
|
|
89
|
+
setStep('check-signing-properties');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
78
92
|
else if (step === 'confirm-signing-setup') {
|
|
79
93
|
if (input === 'y' || input === 'Y') {
|
|
80
94
|
setStep('setup-signing-properties');
|
|
@@ -128,6 +142,17 @@ export function SetupFlow({ project, onComplete }) {
|
|
|
128
142
|
React.createElement(Box, { marginBottom: 1 },
|
|
129
143
|
React.createElement(Text, null, "Would you like to set up dev properties? (y/n)"))));
|
|
130
144
|
}
|
|
145
|
+
// Confirm legacy password migration
|
|
146
|
+
if (step === 'confirm-password-migration') {
|
|
147
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
148
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
149
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Sitevision CLI")),
|
|
150
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
151
|
+
React.createElement(Text, { color: "yellow" }, "\u26A0 Plaintext password found in .dev_properties.json")),
|
|
152
|
+
React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
|
|
153
|
+
React.createElement(Text, null, "Move it to the OS keychain and remove it from the file? (y/n)"),
|
|
154
|
+
React.createElement(Text, { dimColor: true }, "Recommended \u2014 storing passwords in project files is insecure."))));
|
|
155
|
+
}
|
|
131
156
|
// Confirm signing setup
|
|
132
157
|
if (step === 'confirm-signing-setup') {
|
|
133
158
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
package/dist/types/index.d.ts
CHANGED
|
@@ -27,14 +27,18 @@ export interface SitevisionManifest {
|
|
|
27
27
|
categories?: string[];
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Development properties
|
|
30
|
+
* Development properties.
|
|
31
|
+
*
|
|
32
|
+
* Persisted fields live in .dev_properties.json. `password` is runtime-only:
|
|
33
|
+
* resolved from the OS keychain (or a legacy plaintext file during migration)
|
|
34
|
+
* and never written back to disk.
|
|
31
35
|
*/
|
|
32
36
|
export interface DevProperties {
|
|
33
37
|
domain: string;
|
|
34
38
|
siteName: string;
|
|
35
39
|
addonName: string;
|
|
36
40
|
username: string;
|
|
37
|
-
password
|
|
41
|
+
password?: string;
|
|
38
42
|
useHTTPForDevDeploy?: boolean;
|
|
39
43
|
signingUsername?: string;
|
|
40
44
|
certificateName?: string;
|
|
@@ -80,6 +84,7 @@ export interface ProjectInfo {
|
|
|
80
84
|
manifest: SitevisionManifest;
|
|
81
85
|
hasDevProperties: boolean;
|
|
82
86
|
hasSigningProperties: boolean;
|
|
87
|
+
hasLegacyPassword: boolean;
|
|
83
88
|
devProperties?: DevProperties;
|
|
84
89
|
packageJson: PackageJson;
|
|
85
90
|
hasSitevisionScripts: boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function getDeployPassword(domain: string, username: string): string | null;
|
|
2
|
+
export declare function setDeployPassword(domain: string, username: string, password: string): boolean;
|
|
3
|
+
export declare function deleteDeployPassword(domain: string, username: string): void;
|
|
4
|
+
export declare function getSigningPassword(username: string): string | null;
|
|
5
|
+
export declare function setSigningPassword(username: string, password: string): boolean;
|
|
6
|
+
export declare function deleteSigningPassword(username: string): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Entry } from '@napi-rs/keyring';
|
|
2
|
+
const SERVICE = 'sitevision-cli';
|
|
3
|
+
function deployAccount(domain, username) {
|
|
4
|
+
return `deploy:${username}@${domain}`;
|
|
5
|
+
}
|
|
6
|
+
function signingAccount(username) {
|
|
7
|
+
return `signing:${username}`;
|
|
8
|
+
}
|
|
9
|
+
function safeGet(account) {
|
|
10
|
+
try {
|
|
11
|
+
return new Entry(SERVICE, account).getPassword();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function safeSet(account, password) {
|
|
18
|
+
try {
|
|
19
|
+
new Entry(SERVICE, account).setPassword(password);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function safeDelete(account) {
|
|
27
|
+
try {
|
|
28
|
+
new Entry(SERVICE, account).deletePassword();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function getDeployPassword(domain, username) {
|
|
35
|
+
if (!domain || !username)
|
|
36
|
+
return null;
|
|
37
|
+
return safeGet(deployAccount(domain, username));
|
|
38
|
+
}
|
|
39
|
+
export function setDeployPassword(domain, username, password) {
|
|
40
|
+
if (!domain || !username || !password)
|
|
41
|
+
return false;
|
|
42
|
+
return safeSet(deployAccount(domain, username), password);
|
|
43
|
+
}
|
|
44
|
+
export function deleteDeployPassword(domain, username) {
|
|
45
|
+
if (!domain || !username)
|
|
46
|
+
return;
|
|
47
|
+
safeDelete(deployAccount(domain, username));
|
|
48
|
+
}
|
|
49
|
+
export function getSigningPassword(username) {
|
|
50
|
+
if (!username)
|
|
51
|
+
return null;
|
|
52
|
+
return safeGet(signingAccount(username));
|
|
53
|
+
}
|
|
54
|
+
export function setSigningPassword(username, password) {
|
|
55
|
+
if (!username || !password)
|
|
56
|
+
return false;
|
|
57
|
+
return safeSet(signingAccount(username), password);
|
|
58
|
+
}
|
|
59
|
+
export function deleteSigningPassword(username) {
|
|
60
|
+
if (!username)
|
|
61
|
+
return;
|
|
62
|
+
safeDelete(signingAccount(username));
|
|
63
|
+
}
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt for a yes/no answer. Returns true for y/Y, false otherwise (incl. empty / Enter).
|
|
3
|
+
*/
|
|
4
|
+
export function promptYesNo(prompt) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
process.stdout.write(prompt);
|
|
7
|
+
const stdin = process.stdin;
|
|
8
|
+
stdin.setRawMode(true);
|
|
9
|
+
stdin.resume();
|
|
10
|
+
stdin.setEncoding('utf8');
|
|
11
|
+
const onData = (data) => {
|
|
12
|
+
const char = data[0] || '';
|
|
13
|
+
stdin.setRawMode(false);
|
|
14
|
+
stdin.removeListener('data', onData);
|
|
15
|
+
stdin.pause();
|
|
16
|
+
process.stdout.write(`${char}\n`);
|
|
17
|
+
if (char.charCodeAt(0) === 3) {
|
|
18
|
+
process.exit();
|
|
19
|
+
}
|
|
20
|
+
resolve(char === 'y' || char === 'Y');
|
|
21
|
+
};
|
|
22
|
+
stdin.on('data', onData);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
1
25
|
/**
|
|
2
26
|
* Prompt for password input with masked display
|
|
3
27
|
*/
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import type { SitevisionManifest, DevProperties, ProjectPaths, SimpleAppType,
|
|
2
|
-
export type { SitevisionManifest, DevProperties } from '../types/index.js';
|
|
3
|
-
/**
|
|
4
|
-
* Project information with paths
|
|
5
|
-
*/
|
|
6
|
-
export interface ProjectInfo {
|
|
7
|
-
root: string;
|
|
8
|
-
manifest: SitevisionManifest;
|
|
9
|
-
hasDevProperties: boolean;
|
|
10
|
-
hasSigningProperties: boolean;
|
|
11
|
-
devProperties?: DevProperties;
|
|
12
|
-
packageJson: PackageJson;
|
|
13
|
-
hasSitevisionScripts: boolean;
|
|
14
|
-
hasNodeModules: boolean;
|
|
15
|
-
paths: ProjectPaths;
|
|
16
|
-
}
|
|
1
|
+
import type { SitevisionManifest, DevProperties, ProjectInfo, ProjectPaths, SimpleAppType, ApiEndpoints } from '../types/index.js';
|
|
2
|
+
export type { SitevisionManifest, DevProperties, ProjectInfo } from '../types/index.js';
|
|
17
3
|
/**
|
|
18
4
|
* Get standard project paths for a given root directory
|
|
19
5
|
*/
|
|
@@ -86,9 +72,18 @@ export declare function isBundledApp(manifest: SitevisionManifest): boolean;
|
|
|
86
72
|
*/
|
|
87
73
|
export declare function readDevProperties(projectRoot: string): DevProperties | null;
|
|
88
74
|
/**
|
|
89
|
-
* Write dev properties to file
|
|
75
|
+
* Write dev properties to file. The `password` field is never persisted —
|
|
76
|
+
* it is held in the OS keychain instead.
|
|
90
77
|
*/
|
|
91
78
|
export declare function writeDevProperties(projectRoot: string, properties: DevProperties): void;
|
|
79
|
+
/**
|
|
80
|
+
* Move a plaintext password from .dev_properties.json into the OS keychain and
|
|
81
|
+
* strip it from the file. Returns true if the password was migrated.
|
|
82
|
+
*
|
|
83
|
+
* The in-memory `project.devProperties.password` is intentionally left intact
|
|
84
|
+
* so the current invocation can keep using it; only the on-disk copy is removed.
|
|
85
|
+
*/
|
|
86
|
+
export declare function migrateLegacyPassword(project: ProjectInfo): boolean;
|
|
92
87
|
/**
|
|
93
88
|
* Ensure the dist directory exists
|
|
94
89
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { getDeployPassword, setDeployPassword } from './keychain.js';
|
|
3
4
|
// =============================================================================
|
|
4
5
|
// PATH UTILITIES
|
|
5
6
|
// =============================================================================
|
|
@@ -173,10 +174,26 @@ export function detectProject(cwd = process.cwd()) {
|
|
|
173
174
|
const devPropertiesPath = findDevPropertiesPath(cwd);
|
|
174
175
|
let devProperties;
|
|
175
176
|
let hasDevProperties = false;
|
|
177
|
+
let hasLegacyPassword = false;
|
|
176
178
|
if (devPropertiesPath) {
|
|
177
179
|
hasDevProperties = true;
|
|
178
180
|
try {
|
|
179
|
-
|
|
181
|
+
const parsed = JSON.parse(fs.readFileSync(devPropertiesPath, 'utf-8'));
|
|
182
|
+
hasLegacyPassword = typeof parsed.password === 'string' && parsed.password.length > 0;
|
|
183
|
+
devProperties = parsed;
|
|
184
|
+
// Resolve deploy password: env var > keychain (file is legacy-only)
|
|
185
|
+
if (!hasLegacyPassword && devProperties.domain && devProperties.username) {
|
|
186
|
+
const envPassword = process.env['SITEVISION_DEPLOY_PASSWORD'];
|
|
187
|
+
if (envPassword) {
|
|
188
|
+
devProperties.password = envPassword;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const stored = getDeployPassword(devProperties.domain, devProperties.username);
|
|
192
|
+
if (stored) {
|
|
193
|
+
devProperties.password = stored;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
180
197
|
}
|
|
181
198
|
catch {
|
|
182
199
|
// Invalid dev properties file
|
|
@@ -191,6 +208,7 @@ export function detectProject(cwd = process.cwd()) {
|
|
|
191
208
|
manifest,
|
|
192
209
|
hasDevProperties,
|
|
193
210
|
hasSigningProperties,
|
|
211
|
+
hasLegacyPassword,
|
|
194
212
|
devProperties,
|
|
195
213
|
packageJson,
|
|
196
214
|
hasSitevisionScripts,
|
|
@@ -250,11 +268,33 @@ export function readDevProperties(projectRoot) {
|
|
|
250
268
|
}
|
|
251
269
|
}
|
|
252
270
|
/**
|
|
253
|
-
* Write dev properties to file
|
|
271
|
+
* Write dev properties to file. The `password` field is never persisted —
|
|
272
|
+
* it is held in the OS keychain instead.
|
|
254
273
|
*/
|
|
255
274
|
export function writeDevProperties(projectRoot, properties) {
|
|
256
275
|
const devPropertiesPath = findDevPropertiesPath(projectRoot) || getDefaultDevPropertiesPath(projectRoot);
|
|
257
|
-
|
|
276
|
+
const { password: _password, ...persisted } = properties;
|
|
277
|
+
fs.writeFileSync(devPropertiesPath, JSON.stringify(persisted, null, 2));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Move a plaintext password from .dev_properties.json into the OS keychain and
|
|
281
|
+
* strip it from the file. Returns true if the password was migrated.
|
|
282
|
+
*
|
|
283
|
+
* The in-memory `project.devProperties.password` is intentionally left intact
|
|
284
|
+
* so the current invocation can keep using it; only the on-disk copy is removed.
|
|
285
|
+
*/
|
|
286
|
+
export function migrateLegacyPassword(project) {
|
|
287
|
+
if (!project.hasLegacyPassword || !project.devProperties)
|
|
288
|
+
return false;
|
|
289
|
+
const { domain, username, password } = project.devProperties;
|
|
290
|
+
if (!domain || !username || !password)
|
|
291
|
+
return false;
|
|
292
|
+
if (!setDeployPassword(domain, username, password))
|
|
293
|
+
return false;
|
|
294
|
+
// writeDevProperties strips `password` defensively; keep the in-memory value.
|
|
295
|
+
writeDevProperties(project.root, project.devProperties);
|
|
296
|
+
project.hasLegacyPassword = false;
|
|
297
|
+
return true;
|
|
258
298
|
}
|
|
259
299
|
/**
|
|
260
300
|
* Ensure the dist directory exists
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check the npm registry for a newer published version of `packageName`.
|
|
3
|
+
*
|
|
4
|
+
* Resolves to the latest version string when it is newer than
|
|
5
|
+
* `currentVersion`, otherwise `null`. Never rejects — network errors,
|
|
6
|
+
* timeouts, non-200 responses and parse failures all resolve to `null`, so a
|
|
7
|
+
* failed check never blocks startup or surfaces an error to the user.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkForUpdate(packageName: string, currentVersion: string, timeoutMs?: number): Promise<string | null>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
/**
|
|
3
|
+
* True if version `a` is strictly greater than `b`, comparing the numeric
|
|
4
|
+
* MAJOR.MINOR.PATCH core. Pre-release suffixes (e.g. `-beta.1`) are ignored.
|
|
5
|
+
*/
|
|
6
|
+
function isNewer(a, b) {
|
|
7
|
+
const core = (v) => (v.split('-')[0] ?? '').split('.');
|
|
8
|
+
const pa = core(a);
|
|
9
|
+
const pb = core(b);
|
|
10
|
+
for (let i = 0; i < 3; i++) {
|
|
11
|
+
const da = Number.parseInt(pa[i] ?? '0', 10) || 0;
|
|
12
|
+
const db = Number.parseInt(pb[i] ?? '0', 10) || 0;
|
|
13
|
+
if (da > db)
|
|
14
|
+
return true;
|
|
15
|
+
if (da < db)
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check the npm registry for a newer published version of `packageName`.
|
|
22
|
+
*
|
|
23
|
+
* Resolves to the latest version string when it is newer than
|
|
24
|
+
* `currentVersion`, otherwise `null`. Never rejects — network errors,
|
|
25
|
+
* timeouts, non-200 responses and parse failures all resolve to `null`, so a
|
|
26
|
+
* failed check never blocks startup or surfaces an error to the user.
|
|
27
|
+
*/
|
|
28
|
+
export function checkForUpdate(packageName, currentVersion, timeoutMs = 1500) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
31
|
+
const request = https.get(url, { timeout: timeoutMs, headers: { accept: 'application/json' } }, (response) => {
|
|
32
|
+
if (response.statusCode !== 200) {
|
|
33
|
+
response.resume();
|
|
34
|
+
resolve(null);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let body = '';
|
|
38
|
+
response.setEncoding('utf8');
|
|
39
|
+
response.on('data', (chunk) => {
|
|
40
|
+
body += chunk;
|
|
41
|
+
});
|
|
42
|
+
response.on('end', () => {
|
|
43
|
+
try {
|
|
44
|
+
const latest = JSON.parse(body).version;
|
|
45
|
+
resolve(latest && isNewer(latest, currentVersion) ? latest : null);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
resolve(null);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
request.on('timeout', () => {
|
|
53
|
+
request.destroy();
|
|
54
|
+
resolve(null);
|
|
55
|
+
});
|
|
56
|
+
request.on('error', () => {
|
|
57
|
+
resolve(null);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -4,16 +4,8 @@
|
|
|
4
4
|
* Handles webpack compilation for Sitevision apps.
|
|
5
5
|
* Dynamically loads webpack from the target project's node_modules.
|
|
6
6
|
*/
|
|
7
|
-
import { EventEmitter } from 'events';
|
|
8
7
|
import type { BuildOptions, BuildResult } from '../types/index.js';
|
|
9
|
-
|
|
10
|
-
* Events emitted by WebpackRunner:
|
|
11
|
-
* - 'compile': Emitted when compilation starts
|
|
12
|
-
* - 'done': Emitted when compilation completes successfully
|
|
13
|
-
* - 'error': Emitted when compilation fails
|
|
14
|
-
* - 'warning': Emitted when compilation has warnings
|
|
15
|
-
*/
|
|
16
|
-
export declare class WebpackRunner extends EventEmitter {
|
|
8
|
+
export declare class WebpackRunner {
|
|
17
9
|
private projectRoot;
|
|
18
10
|
private options;
|
|
19
11
|
private webpack;
|
|
@@ -7,21 +7,12 @@
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
|
-
import { EventEmitter } from 'events';
|
|
11
10
|
import { copyChunksToResources } from './zip.js';
|
|
12
11
|
// =============================================================================
|
|
13
12
|
// WEBPACK RUNNER CLASS
|
|
14
13
|
// =============================================================================
|
|
15
|
-
|
|
16
|
-
* Events emitted by WebpackRunner:
|
|
17
|
-
* - 'compile': Emitted when compilation starts
|
|
18
|
-
* - 'done': Emitted when compilation completes successfully
|
|
19
|
-
* - 'error': Emitted when compilation fails
|
|
20
|
-
* - 'warning': Emitted when compilation has warnings
|
|
21
|
-
*/
|
|
22
|
-
export class WebpackRunner extends EventEmitter {
|
|
14
|
+
export class WebpackRunner {
|
|
23
15
|
constructor(projectRoot, options) {
|
|
24
|
-
super();
|
|
25
16
|
Object.defineProperty(this, "projectRoot", {
|
|
26
17
|
enumerable: true,
|
|
27
18
|
configurable: true,
|
|
@@ -151,17 +142,14 @@ export class WebpackRunner extends EventEmitter {
|
|
|
151
142
|
throw new Error('Webpack not initialized');
|
|
152
143
|
}
|
|
153
144
|
return new Promise((resolve, reject) => {
|
|
154
|
-
this.emit('compile');
|
|
155
145
|
this.compiler = this.webpack(this.config);
|
|
156
146
|
this.compiler.run((err, stats) => {
|
|
157
147
|
if (err) {
|
|
158
|
-
this.emit('error', err);
|
|
159
148
|
reject(err);
|
|
160
149
|
return;
|
|
161
150
|
}
|
|
162
151
|
if (!stats) {
|
|
163
152
|
const error = new Error('No stats returned from webpack');
|
|
164
|
-
this.emit('error', error);
|
|
165
153
|
reject(error);
|
|
166
154
|
return;
|
|
167
155
|
}
|
|
@@ -176,13 +164,6 @@ export class WebpackRunner extends EventEmitter {
|
|
|
176
164
|
console.warn('Warning: Failed to copy chunks:', chunkError);
|
|
177
165
|
}
|
|
178
166
|
}
|
|
179
|
-
if (stats.hasErrors()) {
|
|
180
|
-
this.emit('error', new Error(stats.toString({ colors: false })));
|
|
181
|
-
}
|
|
182
|
-
else if (stats.hasWarnings()) {
|
|
183
|
-
this.emit('warning', stats.toString({ colors: false }));
|
|
184
|
-
}
|
|
185
|
-
this.emit('done', result);
|
|
186
167
|
resolve(result);
|
|
187
168
|
});
|
|
188
169
|
});
|
|
@@ -204,7 +185,6 @@ export class WebpackRunner extends EventEmitter {
|
|
|
204
185
|
ignored: ['**/dist/**', '**/build/**', '**/node_modules/**'],
|
|
205
186
|
}, (err, stats) => {
|
|
206
187
|
if (err) {
|
|
207
|
-
this.emit('error', err);
|
|
208
188
|
callback?.({
|
|
209
189
|
success: false,
|
|
210
190
|
errors: [err.message],
|
|
@@ -224,13 +204,6 @@ export class WebpackRunner extends EventEmitter {
|
|
|
224
204
|
console.warn('Warning: Failed to copy chunks:', chunkError);
|
|
225
205
|
}
|
|
226
206
|
}
|
|
227
|
-
if (stats.hasErrors()) {
|
|
228
|
-
this.emit('error', new Error(stats.toString({ colors: false })));
|
|
229
|
-
}
|
|
230
|
-
else if (stats.hasWarnings()) {
|
|
231
|
-
this.emit('warning', stats.toString({ colors: false }));
|
|
232
|
-
}
|
|
233
|
-
this.emit('done', result);
|
|
234
207
|
callback?.(result);
|
|
235
208
|
});
|
|
236
209
|
// Resolve immediately - watch continues in background
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sitevision-cli",
|
|
3
|
-
"version": "0.1.1",
|
|
3
|
+
"version": "0.3.1-beta.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"svc": "dist/cli.js"
|
|
@@ -12,23 +12,24 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
|
-
"test": "prettier --check . && xo && ava"
|
|
15
|
+
"test": "prettier --check . && xo && ava",
|
|
16
|
+
"release": "./scripts/publish.sh",
|
|
17
|
+
"release:beta": "./scripts/publish-beta.sh"
|
|
16
18
|
},
|
|
17
19
|
"files": [
|
|
18
20
|
"dist"
|
|
19
21
|
],
|
|
20
22
|
"dependencies": {
|
|
23
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
21
24
|
"ink": "^6.6.0",
|
|
22
25
|
"ink-spinner": "^5.0.0",
|
|
23
26
|
"meow": "^11.0.0",
|
|
24
|
-
"react": "^19.2.3"
|
|
25
|
-
"update-notifier": "^7.0.0"
|
|
27
|
+
"react": "^19.2.3"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@sindresorhus/tsconfig": "^3.0.1",
|
|
29
31
|
"@types/node": "^25.0.3",
|
|
30
32
|
"@types/react": "^19.2.7",
|
|
31
|
-
"@types/update-notifier": "^6.0.8",
|
|
32
33
|
"@vdemedes/prettier-config": "^2.0.1",
|
|
33
34
|
"ava": "^5.2.0",
|
|
34
35
|
"chalk": "^5.2.0",
|
package/readme.md
CHANGED
|
@@ -12,7 +12,7 @@ However, these scripts have some limitations:
|
|
|
12
12
|
- **Project Detection** - Automatically detects Sitevision projects
|
|
13
13
|
- **Two Modes** - Interactive menu OR direct command execution
|
|
14
14
|
- **Automatic Setup** - Guided setup for dev properties and signing credentials
|
|
15
|
-
- **Secure Credentials** - Passwords
|
|
15
|
+
- **Secure Credentials** - Passwords live in the OS keychain (macOS Keychain / Windows Credential Manager / Linux libsecret), never on disk
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -117,14 +117,29 @@ Create this file in your project root for deployment configuration:
|
|
|
117
117
|
"siteName": "YourSite",
|
|
118
118
|
"addonName": "your-addon",
|
|
119
119
|
"username": "your-email@example.com",
|
|
120
|
-
"password": "",
|
|
121
120
|
"useHTTPForDevDeploy": false,
|
|
122
121
|
"signingUsername": "your-developer-account@example.com",
|
|
123
122
|
"certificateName": "optional-certificate-name"
|
|
124
123
|
}
|
|
125
124
|
```
|
|
126
125
|
|
|
127
|
-
|
|
126
|
+
### Password storage
|
|
127
|
+
|
|
128
|
+
Passwords are stored in the OS-native secret store (macOS Keychain, Windows
|
|
129
|
+
Credential Manager, Linux libsecret) under the `sitevision-cli` service —
|
|
130
|
+
never in `.dev_properties.json`. Run `svc` and complete the setup form (or
|
|
131
|
+
enter the password when prompted at deploy/sign time and toggle "save to
|
|
132
|
+
keychain") to populate it.
|
|
133
|
+
|
|
134
|
+
If an existing `.dev_properties.json` contains a plaintext `password` field,
|
|
135
|
+
the CLI offers to migrate it to the keychain on next launch and strip the
|
|
136
|
+
field from the file. The migration prompt only appears in interactive mode
|
|
137
|
+
(plain `svc`) — if you only ever invoke commands directly (`svc deploy`,
|
|
138
|
+
`svc dev`), run `svc` once to migrate.
|
|
139
|
+
|
|
140
|
+
For CI / headless use, set `SITEVISION_DEPLOY_PASSWORD` and/or
|
|
141
|
+
`SITEVISION_SIGNING_PASSWORD` — these take precedence over the keychain and
|
|
142
|
+
are never written anywhere.
|
|
128
143
|
|
|
129
144
|
### Signing Credentials
|
|
130
145
|
|
|
@@ -132,7 +147,8 @@ Signing credentials are used to sign apps via developer.sitevision.se:
|
|
|
132
147
|
- `signingUsername` - Your developer.sitevision.se account
|
|
133
148
|
- `certificateName` - Optional, if you have multiple certificates
|
|
134
149
|
|
|
135
|
-
The signing password is
|
|
150
|
+
The signing password is prompted on first use, with an option to save it to
|
|
151
|
+
the OS keychain for future runs.
|
|
136
152
|
|
|
137
153
|
## License
|
|
138
154
|
|