sitevision-cli 0.3.1-beta.2 → 0.4.0-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.d.ts +1 -2
- package/dist/app.js +19 -14
- package/dist/cli.js +48 -24
- package/dist/commands/build.d.ts +1 -1
- package/dist/commands/build.js +8 -32
- package/dist/commands/deploy.js +19 -20
- package/dist/commands/dev.js +11 -44
- package/dist/commands/index.js +1 -1
- package/dist/commands/info.js +3 -53
- package/dist/commands/setup-signing.js +2 -2
- package/dist/commands/sign.d.ts +1 -1
- package/dist/commands/sign.js +13 -35
- package/dist/components/DevPropertiesForm.d.ts +1 -2
- package/dist/components/DevPropertiesForm.js +15 -30
- package/dist/components/InfoScreen.d.ts +1 -2
- package/dist/components/InfoScreen.js +2 -50
- package/dist/components/MainMenu.d.ts +1 -2
- package/dist/components/MainMenu.js +5 -70
- package/dist/components/PasswordInput.d.ts +1 -2
- package/dist/components/PasswordInput.js +7 -19
- package/dist/components/ProcessOutput.d.ts +1 -2
- package/dist/components/ProcessOutput.js +3 -5
- package/dist/components/SetupFlow.d.ts +1 -2
- package/dist/components/SetupFlow.js +12 -87
- package/dist/components/SigningPropertiesForm.d.ts +1 -2
- package/dist/components/SigningPropertiesForm.js +7 -18
- package/dist/components/StatusIndicator.d.ts +1 -2
- package/dist/components/StatusIndicator.js +6 -12
- package/dist/components/TextInput.d.ts +1 -2
- package/dist/components/TextInput.js +5 -13
- package/dist/components/WelcomeScreen.d.ts +14 -0
- package/dist/components/WelcomeScreen.js +53 -0
- package/dist/utils/branding.d.ts +12 -0
- package/dist/utils/branding.js +29 -0
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +57 -0
- package/dist/utils/password-prompt.d.ts +3 -2
- package/dist/utils/password-prompt.js +12 -5
- package/dist/utils/process-runner.d.ts +6 -6
- package/dist/utils/process-runner.js +12 -42
- package/dist/utils/project-detection.d.ts +1 -1
- package/dist/utils/project-detection.js +7 -3
- package/dist/utils/signing-password.d.ts +12 -0
- package/dist/utils/signing-password.js +36 -0
- package/dist/utils/sitevision-api.js +1 -1
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.d.ts +2 -2
- package/dist/utils/webpack-runner.js +15 -42
- package/dist/utils/zip.js +5 -7
- package/package.json +23 -30
- package/readme.md +11 -7
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
interface Props {
|
|
3
2
|
projectRoot: string;
|
|
4
3
|
onComplete: () => void;
|
|
5
4
|
onCancel: () => void;
|
|
6
5
|
}
|
|
7
|
-
export declare function SigningPropertiesForm({ projectRoot, onComplete, onCancel }: Props):
|
|
6
|
+
export declare function SigningPropertiesForm({ projectRoot, onComplete, onCancel, }: Props): import("react").JSX.Element;
|
|
8
7
|
export {};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
4
|
import { TextInput } from './TextInput.js';
|
|
4
|
-
import { writeDevProperties, readDevProperties } from '../utils/project-detection.js';
|
|
5
|
+
import { writeDevProperties, readDevProperties, } from '../utils/project-detection.js';
|
|
5
6
|
const STEPS = [
|
|
6
7
|
{ id: 'username', label: 'Signing Username' },
|
|
7
8
|
{ id: 'certificate', label: 'Certificate Name' },
|
|
8
9
|
];
|
|
9
|
-
export function SigningPropertiesForm({ projectRoot, onComplete, onCancel }) {
|
|
10
|
+
export function SigningPropertiesForm({ projectRoot, onComplete, onCancel, }) {
|
|
10
11
|
const [stepIndex, setStepIndex] = useState(0);
|
|
11
12
|
// Read existing properties to preserve other fields
|
|
12
13
|
const [properties, setProperties] = useState(() => readDevProperties(projectRoot) || {});
|
|
@@ -26,24 +27,12 @@ export function SigningPropertiesForm({ projectRoot, onComplete, onCancel }) {
|
|
|
26
27
|
const renderInput = () => {
|
|
27
28
|
switch (currentStep?.id) {
|
|
28
29
|
case 'username':
|
|
29
|
-
return (
|
|
30
|
+
return (_jsx(TextInput, { label: "Signing Username (developer.sitevision.se)", defaultValue: properties.signingUsername, onSubmit: (value) => handleNext('signingUsername', value), onCancel: onCancel }, "username"));
|
|
30
31
|
case 'certificate':
|
|
31
|
-
return (
|
|
32
|
+
return (_jsx(TextInput, { label: "Certificate Name (Optional)", defaultValue: properties.certificateName, placeholder: "Leave empty for default", onSubmit: (value) => handleNext('certificateName', value), onCancel: onCancel }, "certificate"));
|
|
32
33
|
default:
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
|
-
return (
|
|
37
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
38
|
-
React.createElement(Text, { bold: true, color: "cyan" }, "Setup Signing Properties"),
|
|
39
|
-
React.createElement(Text, null,
|
|
40
|
-
" Step ",
|
|
41
|
-
stepIndex + 1,
|
|
42
|
-
" of ",
|
|
43
|
-
STEPS.length,
|
|
44
|
-
": ",
|
|
45
|
-
currentStep?.label)),
|
|
46
|
-
React.createElement(Box, { marginBottom: 1 }, STEPS.map((s, i) => (React.createElement(Box, { key: s.id, marginRight: 1 },
|
|
47
|
-
React.createElement(Text, { color: i === stepIndex ? 'green' : i < stepIndex ? 'green' : 'gray' }, i < stepIndex ? '✓' : i === stepIndex ? '●' : '○'))))),
|
|
48
|
-
React.createElement(Box, { borderStyle: "single", borderColor: "gray", padding: 1 }, renderInput())));
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Setup Signing Properties" }), _jsxs(Text, { children: [' ', "Step ", stepIndex + 1, " of ", STEPS.length, ": ", currentStep?.label] })] }), _jsx(Box, { marginBottom: 1, children: STEPS.map((s, i) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: i === stepIndex ? 'green' : i < stepIndex ? 'green' : 'gray', children: i < stepIndex ? '✓' : i === stepIndex ? '●' : '○' }) }, s.id))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: renderInput() })] }));
|
|
49
38
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
export type Status = 'pending' | 'running' | 'success' | 'error';
|
|
3
2
|
interface Props {
|
|
4
3
|
status: Status;
|
|
5
4
|
label: string;
|
|
6
5
|
message?: string;
|
|
7
6
|
}
|
|
8
|
-
export declare function StatusIndicator({ status, label, message }: Props):
|
|
7
|
+
export declare function StatusIndicator({ status, label, message }: Props): import("react").JSX.Element;
|
|
9
8
|
export {};
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
export function StatusIndicator({ status, label, message }) {
|
|
5
5
|
const getIcon = () => {
|
|
6
6
|
switch (status) {
|
|
7
7
|
case 'pending':
|
|
8
|
-
return
|
|
8
|
+
return _jsx(Text, { dimColor: true, children: "\u25CB" });
|
|
9
9
|
case 'running':
|
|
10
|
-
return (
|
|
11
|
-
React.createElement(Spinner, { type: "dots" })));
|
|
10
|
+
return (_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }));
|
|
12
11
|
case 'success':
|
|
13
|
-
return
|
|
12
|
+
return _jsx(Text, { color: "green", children: "\u2713" });
|
|
14
13
|
case 'error':
|
|
15
|
-
return
|
|
14
|
+
return _jsx(Text, { color: "red", children: "\u2717" });
|
|
16
15
|
}
|
|
17
16
|
};
|
|
18
17
|
const getColor = () => {
|
|
@@ -27,10 +26,5 @@ export function StatusIndicator({ status, label, message }) {
|
|
|
27
26
|
return 'red';
|
|
28
27
|
}
|
|
29
28
|
};
|
|
30
|
-
return (
|
|
31
|
-
getIcon(),
|
|
32
|
-
React.createElement(Box, { marginLeft: 1 },
|
|
33
|
-
React.createElement(Text, { color: getColor(), bold: true }, label)),
|
|
34
|
-
message && (React.createElement(Box, { marginLeft: 1 },
|
|
35
|
-
React.createElement(Text, { dimColor: true }, message)))));
|
|
29
|
+
return (_jsxs(Box, { children: [getIcon(), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getColor(), bold: true, children: label }) }), message && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: message }) }))] }));
|
|
36
30
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
interface Props {
|
|
3
2
|
label: string;
|
|
4
3
|
defaultValue?: string;
|
|
@@ -7,5 +6,5 @@ interface Props {
|
|
|
7
6
|
onCancel?: () => void;
|
|
8
7
|
type?: 'text' | 'password';
|
|
9
8
|
}
|
|
10
|
-
export declare function TextInput({ label, defaultValue, placeholder, onSubmit, onCancel, type, }: Props):
|
|
9
|
+
export declare function TextInput({ label, defaultValue, placeholder, onSubmit, onCancel, type, }: Props): import("react").JSX.Element;
|
|
11
10
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { Box, Text, useInput } from 'ink';
|
|
3
4
|
export function TextInput({ label, defaultValue = '', placeholder, onSubmit, onCancel, type = 'text', }) {
|
|
4
5
|
const [value, setValue] = useState(defaultValue);
|
|
@@ -17,21 +18,12 @@ export function TextInput({ label, defaultValue = '', placeholder, onSubmit, onC
|
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
if (key.delete || key.backspace) {
|
|
20
|
-
setValue(
|
|
21
|
+
setValue(prev => prev.slice(0, -1));
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
24
|
if (!key.ctrl && !key.meta) {
|
|
24
|
-
setValue(
|
|
25
|
+
setValue(prev => prev + input);
|
|
25
26
|
}
|
|
26
27
|
});
|
|
27
|
-
return (
|
|
28
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
29
|
-
React.createElement(Text, { bold: true, color: "cyan" }, label)),
|
|
30
|
-
React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1 },
|
|
31
|
-
React.createElement(Text, null, type === 'password' ? '*'.repeat(value.length) : value),
|
|
32
|
-
value === '' && placeholder && (React.createElement(Text, { dimColor: true }, placeholder))),
|
|
33
|
-
React.createElement(Box, { marginTop: 1 },
|
|
34
|
-
React.createElement(Text, { dimColor: true },
|
|
35
|
-
"Press Enter to submit",
|
|
36
|
-
onCancel ? ', Esc to cancel' : ''))));
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: label }) }), _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: type === 'password' ? '*'.repeat(value.length) : value }), value === '' && placeholder && _jsx(Text, { dimColor: true, children: placeholder })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press Enter to submit", onCancel ? ', Esc to cancel' : ''] }) })] }));
|
|
37
29
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ProjectInfo } from '../utils/project-detection.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
project: ProjectInfo;
|
|
4
|
+
onComplete: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* First-run welcome screen. Shows the branding once and, when signing
|
|
8
|
+
* credentials are configured but no password is stored yet, offers to save the
|
|
9
|
+
* signing password to the OS keychain. If signing isn't set up, it just shows
|
|
10
|
+
* the branding — the user can still save a signing password later (the regular
|
|
11
|
+
* signing flow offers a "remember" option every time).
|
|
12
|
+
*/
|
|
13
|
+
export declare function WelcomeScreen({ project, onComplete }: Props): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { PasswordInput } from './PasswordInput.js';
|
|
5
|
+
import { getSigningPassword, setSigningPassword as saveSigningPassword, } from '../utils/keychain.js';
|
|
6
|
+
import { LOGO, AUTHOR } from '../utils/branding.js';
|
|
7
|
+
function Banner() {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [LOGO.map((line, index) => (_jsx(Text, { color: "cyan", children: line }, index))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ' a tool by ' }), _jsx(Text, { bold: true, children: AUTHOR })] })] }));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* First-run welcome screen. Shows the branding once and, when signing
|
|
12
|
+
* credentials are configured but no password is stored yet, offers to save the
|
|
13
|
+
* signing password to the OS keychain. If signing isn't set up, it just shows
|
|
14
|
+
* the branding — the user can still save a signing password later (the regular
|
|
15
|
+
* signing flow offers a "remember" option every time).
|
|
16
|
+
*/
|
|
17
|
+
export function WelcomeScreen({ project, onComplete }) {
|
|
18
|
+
const signingUsername = project.devProperties?.signingUsername;
|
|
19
|
+
const canSaveSigning = Boolean(signingUsername) && !getSigningPassword(signingUsername);
|
|
20
|
+
const [step, setStep] = useState('prompt');
|
|
21
|
+
const [resultMessage, setResultMessage] = useState('');
|
|
22
|
+
useInput((input, key) => {
|
|
23
|
+
if (step === 'prompt') {
|
|
24
|
+
if (canSaveSigning && (input === 'y' || input === 'Y')) {
|
|
25
|
+
setStep('password');
|
|
26
|
+
}
|
|
27
|
+
else if (input === 'n' || input === 'N' || key.return) {
|
|
28
|
+
onComplete();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (step === 'done') {
|
|
32
|
+
onComplete();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (step === 'password') {
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Banner, {}), _jsx(PasswordInput, { label: "Enter Signing Password (developer.sitevision.se)", onSubmit: password => {
|
|
37
|
+
if (password && signingUsername) {
|
|
38
|
+
const saved = saveSigningPassword(signingUsername, password);
|
|
39
|
+
setResultMessage(saved
|
|
40
|
+
? '✓ Signing password saved to the OS keychain.'
|
|
41
|
+
: 'Could not access the keychain; password not saved.');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
setResultMessage('Skipped — no password entered.');
|
|
45
|
+
}
|
|
46
|
+
setStep('done');
|
|
47
|
+
}, onCancel: onComplete })] }));
|
|
48
|
+
}
|
|
49
|
+
if (step === 'done') {
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Banner, {}), _jsx(Text, { color: resultMessage.startsWith('✓') ? 'green' : 'yellow', children: resultMessage }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press any key to continue\u2026" }) })] }));
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Banner, {}), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Welcome to Sitevision CLI! \uD83D\uDC4B" }) }), canSaveSigning ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Save your signing password to the OS keychain now? (y/n)" }), _jsx(Text, { dimColor: true, children: "So you won't be asked for it every time you sign." })] })) : (_jsxs(Box, { flexDirection: "column", children: [signingUsername ? (_jsx(Text, { dimColor: true, children: "Your signing password is already saved in the keychain." })) : (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Signing credentials aren't configured yet." }), _jsx(Text, { dimColor: true, children: "Run \"svc setup-signing\" later to enable app signing." })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Press Enter to continue\u2026" }) })] }))] }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI branding — the logo art and author line. Used both by the Ink
|
|
3
|
+
* first-run welcome screen and the non-interactive "what's new" update banner,
|
|
4
|
+
* so the two stay identical.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LOGO: string[];
|
|
7
|
+
export declare const AUTHOR = "Rasmus S\u00F6derstr\u00F6m";
|
|
8
|
+
/**
|
|
9
|
+
* Print the logo + author line straight to stdout (non-interactive), mirroring
|
|
10
|
+
* how the masthead is printed. Used for the update banner.
|
|
11
|
+
*/
|
|
12
|
+
export declare function printBranding(): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI branding — the logo art and author line. Used both by the Ink
|
|
3
|
+
* first-run welcome screen and the non-interactive "what's new" update banner,
|
|
4
|
+
* so the two stay identical.
|
|
5
|
+
*/
|
|
6
|
+
const CYAN = '\x1b[36m';
|
|
7
|
+
const BOLD = '\x1b[1m';
|
|
8
|
+
const DIM = '\x1b[2m';
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
export const LOGO = [
|
|
11
|
+
' ┌──────────┐',
|
|
12
|
+
' │ │ ▐ ▗ ▗',
|
|
13
|
+
' │ ▄▖ ▞ │ ▄▖ ▄▖ ▄▟ ▄▖ ▖▄ ▄▖ ▗▟▄ ▖▄ ▄▖ ▗▄▄ ▄▖ ▗▄ ▄▖ ▗▟▄',
|
|
14
|
+
' │ ▐ ▝ ▞ │ ▐ ▝ ▐▘▜ ▐▘▜ ▐▘▐ ▛ ▘▐ ▝ ▐ ▛ ▘▐▘▜ ▐▐▐ ▐ ▝ ▐▐ ▐▘▐ ▐',
|
|
15
|
+
' │ ▀▚ ▞ │ ▀▚ ▐ ▐ ▐ ▐ ▐▀▀ ▌ ▀▚ ▐ ▌ ▐ ▐ ▐▐▐ ▀▚ ▐▐ ▐▀▀ ▐',
|
|
16
|
+
' │ ▝▄▞▞ │ ▝▄▞ ▝▙▛ ▝▙█ ▝▙▞ ▌ ▝▄▞ ▝▄ ▌ ▝▙▛ ▐▐▐ ▝▄▞ ▖ ▐▐ ▝▙▞ ▝▄',
|
|
17
|
+
' └──────────┘ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄',
|
|
18
|
+
];
|
|
19
|
+
export const AUTHOR = 'Rasmus Söderström';
|
|
20
|
+
/**
|
|
21
|
+
* Print the logo + author line straight to stdout (non-interactive), mirroring
|
|
22
|
+
* how the masthead is printed. Used for the update banner.
|
|
23
|
+
*/
|
|
24
|
+
export function printBranding() {
|
|
25
|
+
for (const line of LOGO) {
|
|
26
|
+
console.log(`${CYAN}${line}${RESET}`);
|
|
27
|
+
}
|
|
28
|
+
console.log(`${DIM} a tool by ${RESET}${BOLD}${AUTHOR}${RESET}`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True until the user has completed the first-run welcome at least once.
|
|
3
|
+
*/
|
|
4
|
+
export declare function isFirstRun(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Persist that the first-run welcome has been seen.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markFirstRunComplete(): void;
|
|
9
|
+
/**
|
|
10
|
+
* The CLI version that was last run, or undefined if never recorded (a fresh
|
|
11
|
+
* install, or a user who completed first-run before version tracking existed).
|
|
12
|
+
*/
|
|
13
|
+
export declare function getLastSeenVersion(): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Persist the CLI version that just ran, so the next run can detect an upgrade.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setLastSeenVersion(version: string): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
function configDir() {
|
|
5
|
+
const base = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
|
6
|
+
return path.join(base, 'sitevision-cli');
|
|
7
|
+
}
|
|
8
|
+
function configFile() {
|
|
9
|
+
return path.join(configDir(), 'config.json');
|
|
10
|
+
}
|
|
11
|
+
function readConfig() {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(configFile(), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeConfig(config) {
|
|
20
|
+
try {
|
|
21
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
22
|
+
fs.writeFileSync(configFile(), JSON.stringify(config, null, 2));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Best-effort: if we can't persist the flag the welcome screen simply
|
|
26
|
+
// shows again next time, which is harmless.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True until the user has completed the first-run welcome at least once.
|
|
31
|
+
*/
|
|
32
|
+
export function isFirstRun() {
|
|
33
|
+
return !readConfig().firstRunCompleted;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Persist that the first-run welcome has been seen.
|
|
37
|
+
*/
|
|
38
|
+
export function markFirstRunComplete() {
|
|
39
|
+
const config = readConfig();
|
|
40
|
+
config.firstRunCompleted = true;
|
|
41
|
+
writeConfig(config);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The CLI version that was last run, or undefined if never recorded (a fresh
|
|
45
|
+
* install, or a user who completed first-run before version tracking existed).
|
|
46
|
+
*/
|
|
47
|
+
export function getLastSeenVersion() {
|
|
48
|
+
return readConfig().lastSeenVersion;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Persist the CLI version that just ran, so the next run can detect an upgrade.
|
|
52
|
+
*/
|
|
53
|
+
export function setLastSeenVersion(version) {
|
|
54
|
+
const config = readConfig();
|
|
55
|
+
config.lastSeenVersion = version;
|
|
56
|
+
writeConfig(config);
|
|
57
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt for a yes/no answer. Returns true for y/Y
|
|
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,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt for a yes/no answer. Returns true for y/Y
|
|
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
|
-
return new Promise(
|
|
5
|
+
export function promptYesNo(prompt, defaultYes = false) {
|
|
6
|
+
return new Promise(resolve => {
|
|
6
7
|
process.stdout.write(prompt);
|
|
7
8
|
const stdin = process.stdin;
|
|
8
9
|
stdin.setRawMode(true);
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -26,7 +33,7 @@ export function promptYesNo(prompt) {
|
|
|
26
33
|
* Prompt for password input with masked display
|
|
27
34
|
*/
|
|
28
35
|
export function promptPassword(prompt) {
|
|
29
|
-
return new Promise(
|
|
36
|
+
return new Promise(resolve => {
|
|
30
37
|
process.stdout.write(prompt);
|
|
31
38
|
const stdin = process.stdin;
|
|
32
39
|
stdin.setRawMode(true);
|
|
@@ -8,14 +8,14 @@ export interface ProcessResult {
|
|
|
8
8
|
output: ProcessOutput[];
|
|
9
9
|
}
|
|
10
10
|
export declare class ProcessRunner extends EventEmitter {
|
|
11
|
-
private command;
|
|
12
|
-
private args;
|
|
13
|
-
private cwd?;
|
|
14
|
-
private interactive;
|
|
15
|
-
private customEnv?;
|
|
16
11
|
private process;
|
|
17
12
|
private output;
|
|
18
|
-
|
|
13
|
+
private readonly command;
|
|
14
|
+
private readonly args;
|
|
15
|
+
private readonly cwd?;
|
|
16
|
+
private readonly interactive;
|
|
17
|
+
private readonly customEnv?;
|
|
18
|
+
constructor(command: string, args?: string[], cwd?: string, interactive?: boolean, customEnv?: Record<string, string>);
|
|
19
19
|
run(): Promise<ProcessResult>;
|
|
20
20
|
kill(): void;
|
|
21
21
|
getOutput(): ProcessOutput[];
|
|
@@ -1,50 +1,20 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
export class ProcessRunner extends EventEmitter {
|
|
4
|
+
process = null;
|
|
5
|
+
output = [];
|
|
6
|
+
command;
|
|
7
|
+
args;
|
|
8
|
+
cwd;
|
|
9
|
+
interactive;
|
|
10
|
+
customEnv;
|
|
4
11
|
constructor(command, args = [], cwd, interactive = false, customEnv) {
|
|
5
12
|
super();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
});
|
|
12
|
-
Object.defineProperty(this, "args", {
|
|
13
|
-
enumerable: true,
|
|
14
|
-
configurable: true,
|
|
15
|
-
writable: true,
|
|
16
|
-
value: args
|
|
17
|
-
});
|
|
18
|
-
Object.defineProperty(this, "cwd", {
|
|
19
|
-
enumerable: true,
|
|
20
|
-
configurable: true,
|
|
21
|
-
writable: true,
|
|
22
|
-
value: cwd
|
|
23
|
-
});
|
|
24
|
-
Object.defineProperty(this, "interactive", {
|
|
25
|
-
enumerable: true,
|
|
26
|
-
configurable: true,
|
|
27
|
-
writable: true,
|
|
28
|
-
value: interactive
|
|
29
|
-
});
|
|
30
|
-
Object.defineProperty(this, "customEnv", {
|
|
31
|
-
enumerable: true,
|
|
32
|
-
configurable: true,
|
|
33
|
-
writable: true,
|
|
34
|
-
value: customEnv
|
|
35
|
-
});
|
|
36
|
-
Object.defineProperty(this, "process", {
|
|
37
|
-
enumerable: true,
|
|
38
|
-
configurable: true,
|
|
39
|
-
writable: true,
|
|
40
|
-
value: null
|
|
41
|
-
});
|
|
42
|
-
Object.defineProperty(this, "output", {
|
|
43
|
-
enumerable: true,
|
|
44
|
-
configurable: true,
|
|
45
|
-
writable: true,
|
|
46
|
-
value: []
|
|
47
|
-
});
|
|
13
|
+
this.command = command;
|
|
14
|
+
this.args = args;
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
this.interactive = interactive;
|
|
17
|
+
this.customEnv = customEnv;
|
|
48
18
|
}
|
|
49
19
|
run() {
|
|
50
20
|
return new Promise((resolve, reject) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SitevisionManifest, DevProperties, ProjectInfo, ProjectPaths, SimpleAppType, ApiEndpoints } from '../types/index.js';
|
|
2
|
-
export type { SitevisionManifest, DevProperties, ProjectInfo } from '../types/index.js';
|
|
2
|
+
export type { SitevisionManifest, DevProperties, ProjectInfo, } from '../types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Get standard project paths for a given root directory
|
|
5
5
|
*/
|
|
@@ -179,10 +179,13 @@ export function detectProject(cwd = process.cwd()) {
|
|
|
179
179
|
hasDevProperties = true;
|
|
180
180
|
try {
|
|
181
181
|
const parsed = JSON.parse(fs.readFileSync(devPropertiesPath, 'utf-8'));
|
|
182
|
-
hasLegacyPassword =
|
|
182
|
+
hasLegacyPassword =
|
|
183
|
+
typeof parsed.password === 'string' && parsed.password.length > 0;
|
|
183
184
|
devProperties = parsed;
|
|
184
185
|
// Resolve deploy password: env var > keychain (file is legacy-only)
|
|
185
|
-
if (!hasLegacyPassword &&
|
|
186
|
+
if (!hasLegacyPassword &&
|
|
187
|
+
devProperties.domain &&
|
|
188
|
+
devProperties.username) {
|
|
186
189
|
const envPassword = process.env['SITEVISION_DEPLOY_PASSWORD'];
|
|
187
190
|
if (envPassword) {
|
|
188
191
|
devProperties.password = envPassword;
|
|
@@ -272,7 +275,8 @@ export function readDevProperties(projectRoot) {
|
|
|
272
275
|
* it is held in the OS keychain instead.
|
|
273
276
|
*/
|
|
274
277
|
export function writeDevProperties(projectRoot, properties) {
|
|
275
|
-
const devPropertiesPath = findDevPropertiesPath(projectRoot) ||
|
|
278
|
+
const devPropertiesPath = findDevPropertiesPath(projectRoot) ||
|
|
279
|
+
getDefaultDevPropertiesPath(projectRoot);
|
|
276
280
|
const { password: _password, ...persisted } = properties;
|
|
277
281
|
fs.writeFileSync(devPropertiesPath, JSON.stringify(persisted, null, 2));
|
|
278
282
|
}
|
|
@@ -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
|
+
}
|
|
@@ -74,7 +74,7 @@ function makeRequest(url, options) {
|
|
|
74
74
|
method: options.method,
|
|
75
75
|
headers,
|
|
76
76
|
};
|
|
77
|
-
const req = transport.request(requestOptions,
|
|
77
|
+
const req = transport.request(requestOptions, res => {
|
|
78
78
|
const chunks = [];
|
|
79
79
|
res.on('data', (chunk) => {
|
|
80
80
|
chunks.push(chunk);
|
|
@@ -26,9 +26,9 @@ function isNewer(a, b) {
|
|
|
26
26
|
* failed check never blocks startup or surfaces an error to the user.
|
|
27
27
|
*/
|
|
28
28
|
export function checkForUpdate(packageName, currentVersion, timeoutMs = 1500) {
|
|
29
|
-
return new Promise(
|
|
29
|
+
return new Promise(resolve => {
|
|
30
30
|
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
31
|
-
const request = https.get(url, { timeout: timeoutMs, headers: { accept: 'application/json' } },
|
|
31
|
+
const request = https.get(url, { timeout: timeoutMs, headers: { accept: 'application/json' } }, response => {
|
|
32
32
|
if (response.statusCode !== 200) {
|
|
33
33
|
response.resume();
|
|
34
34
|
resolve(null);
|
|
@@ -36,7 +36,7 @@ export function checkForUpdate(packageName, currentVersion, timeoutMs = 1500) {
|
|
|
36
36
|
}
|
|
37
37
|
let body = '';
|
|
38
38
|
response.setEncoding('utf8');
|
|
39
|
-
response.on('data',
|
|
39
|
+
response.on('data', chunk => {
|
|
40
40
|
body += chunk;
|
|
41
41
|
});
|
|
42
42
|
response.on('end', () => {
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { BuildOptions, BuildResult } from '../types/index.js';
|
|
8
8
|
export declare class WebpackRunner {
|
|
9
|
-
private projectRoot;
|
|
10
|
-
private options;
|
|
11
9
|
private webpack;
|
|
12
10
|
private config;
|
|
13
11
|
private compiler;
|
|
14
12
|
private watcher;
|
|
13
|
+
private readonly projectRoot;
|
|
14
|
+
private readonly options;
|
|
15
15
|
constructor(projectRoot: string, options: BuildOptions);
|
|
16
16
|
/**
|
|
17
17
|
* Initialize webpack by loading it from the project's node_modules
|