sitevision-cli 0.0.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.d.ts +7 -0
- package/dist/app.js +180 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +95 -0
- package/dist/commands/build.d.ts +12 -0
- package/dist/commands/build.js +168 -0
- package/dist/commands/deploy.d.ts +17 -0
- package/dist/commands/deploy.js +162 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +291 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +20 -0
- package/dist/commands/info.d.ts +2 -0
- package/dist/commands/info.js +66 -0
- package/dist/commands/setup-signing.d.ts +2 -0
- package/dist/commands/setup-signing.js +82 -0
- package/dist/commands/sign.d.ts +14 -0
- package/dist/commands/sign.js +103 -0
- package/dist/commands/types.d.ts +18 -0
- package/dist/commands/types.js +1 -0
- package/dist/components/DevPropertiesForm.d.ts +11 -0
- package/dist/components/DevPropertiesForm.js +87 -0
- package/dist/components/InfoScreen.d.ts +8 -0
- package/dist/components/InfoScreen.js +60 -0
- package/dist/components/MainMenu.d.ts +8 -0
- package/dist/components/MainMenu.js +138 -0
- package/dist/components/PasswordInput.d.ts +8 -0
- package/dist/components/PasswordInput.js +30 -0
- package/dist/components/ProcessOutput.d.ts +7 -0
- package/dist/components/ProcessOutput.js +32 -0
- package/dist/components/SetupFlow.d.ts +8 -0
- package/dist/components/SetupFlow.js +194 -0
- package/dist/components/SigningPropertiesForm.d.ts +8 -0
- package/dist/components/SigningPropertiesForm.js +49 -0
- package/dist/components/StatusIndicator.d.ts +9 -0
- package/dist/components/StatusIndicator.js +36 -0
- package/dist/components/TextInput.d.ts +11 -0
- package/dist/components/TextInput.js +37 -0
- package/dist/types/index.d.ts +250 -0
- package/dist/types/index.js +6 -0
- package/dist/utils/password-prompt.d.ts +4 -0
- package/dist/utils/password-prompt.js +45 -0
- package/dist/utils/process-runner.d.ts +30 -0
- package/dist/utils/process-runner.js +119 -0
- package/dist/utils/project-detection.d.ts +103 -0
- package/dist/utils/project-detection.js +287 -0
- package/dist/utils/sitevision-api.d.ts +56 -0
- package/dist/utils/sitevision-api.js +393 -0
- package/dist/utils/webpack-runner.d.ts +75 -0
- package/dist/utils/webpack-runner.js +313 -0
- package/dist/utils/zip.d.ts +64 -0
- package/dist/utils/zip.js +246 -0
- package/package.json +59 -0
- package/readme.md +196 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
4
|
+
import { WebpackRunner } from '../utils/webpack-runner.js';
|
|
5
|
+
import { promptPassword } from '../utils/password-prompt.js';
|
|
6
|
+
import { signApp, deployApp } from '../utils/sitevision-api.js';
|
|
7
|
+
import { copyStaticToBuild, createBuildZip, cleanBuild, } from '../utils/zip.js';
|
|
8
|
+
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, } from '../utils/project-detection.js';
|
|
9
|
+
export function DevScreen({ projectRoot, manifest, devProperties, signed, signingCredentials, onBack, onRetryCredentials, }) {
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
const [state, setState] = React.useState({
|
|
12
|
+
status: 'initializing',
|
|
13
|
+
message: 'Starting webpack watch...',
|
|
14
|
+
buildCount: 0,
|
|
15
|
+
webpackReady: false,
|
|
16
|
+
});
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (onBack && (key.escape || input === 'q')) {
|
|
19
|
+
onBack();
|
|
20
|
+
}
|
|
21
|
+
if (onRetryCredentials && state.status === 'error' && input === 'r') {
|
|
22
|
+
onRetryCredentials();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const webpackRunnerRef = React.useRef(null);
|
|
26
|
+
const handleBuildComplete = React.useCallback(async (result) => {
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
setState(prev => ({
|
|
29
|
+
...prev,
|
|
30
|
+
status: 'error',
|
|
31
|
+
message: result.errors?.join('\n') || 'Build failed',
|
|
32
|
+
error: result.errors?.join('\n'),
|
|
33
|
+
}));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Copy static files
|
|
38
|
+
copyStaticToBuild(projectRoot);
|
|
39
|
+
// Create zip
|
|
40
|
+
const appId = getFullAppId(manifest.id);
|
|
41
|
+
await createBuildZip(projectRoot, appId);
|
|
42
|
+
const zipPath = getZipPath(projectRoot, manifest);
|
|
43
|
+
let deployZipPath = zipPath;
|
|
44
|
+
// Sign if needed
|
|
45
|
+
if (signed && signingCredentials) {
|
|
46
|
+
setState(prev => ({
|
|
47
|
+
...prev,
|
|
48
|
+
status: 'signing',
|
|
49
|
+
message: 'Signing app...',
|
|
50
|
+
}));
|
|
51
|
+
const signedZipPath = getSignedZipPath(projectRoot, manifest);
|
|
52
|
+
const signResult = await signApp(zipPath, signingCredentials, signedZipPath);
|
|
53
|
+
if (!signResult.success) {
|
|
54
|
+
setState(prev => ({
|
|
55
|
+
...prev,
|
|
56
|
+
status: 'error',
|
|
57
|
+
message: `Signing failed: ${signResult.error}`,
|
|
58
|
+
error: signResult.error,
|
|
59
|
+
}));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
deployZipPath = signedZipPath;
|
|
63
|
+
}
|
|
64
|
+
// Deploy
|
|
65
|
+
setState(prev => ({
|
|
66
|
+
...prev,
|
|
67
|
+
status: 'deploying',
|
|
68
|
+
message: 'Deploying to dev...',
|
|
69
|
+
}));
|
|
70
|
+
const appType = getAppType(manifest);
|
|
71
|
+
const deployResult = await deployApp(deployZipPath, {
|
|
72
|
+
domain: devProperties.domain,
|
|
73
|
+
siteName: devProperties.siteName,
|
|
74
|
+
addonName: devProperties.addonName,
|
|
75
|
+
username: devProperties.username,
|
|
76
|
+
password: devProperties.password,
|
|
77
|
+
useHTTP: devProperties.useHTTPForDevDeploy,
|
|
78
|
+
}, appType, true);
|
|
79
|
+
if (!deployResult.success) {
|
|
80
|
+
setState(prev => ({
|
|
81
|
+
...prev,
|
|
82
|
+
status: 'error',
|
|
83
|
+
message: `Deploy failed: ${deployResult.error}`,
|
|
84
|
+
error: deployResult.error,
|
|
85
|
+
}));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Success - back to watching
|
|
89
|
+
setState(prev => ({
|
|
90
|
+
...prev,
|
|
91
|
+
status: 'ready',
|
|
92
|
+
message: 'Deployed. Watching for changes...',
|
|
93
|
+
buildCount: prev.buildCount + 1,
|
|
94
|
+
lastBuildTime: result.stats?.time,
|
|
95
|
+
error: undefined,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
setState(prev => ({
|
|
100
|
+
...prev,
|
|
101
|
+
status: 'error',
|
|
102
|
+
message: error instanceof Error ? error.message : String(error),
|
|
103
|
+
error: error instanceof Error ? error.message : String(error),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
}, [projectRoot, manifest, devProperties, signed, signingCredentials]);
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
const isBundled = isBundledApp(manifest);
|
|
109
|
+
async function startWatch() {
|
|
110
|
+
try {
|
|
111
|
+
// Clean build directory
|
|
112
|
+
cleanBuild(projectRoot);
|
|
113
|
+
if (isBundled) {
|
|
114
|
+
// Check if webpack is available
|
|
115
|
+
if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
|
|
116
|
+
setState({
|
|
117
|
+
status: 'error',
|
|
118
|
+
message: 'webpack not found. Run npm install.',
|
|
119
|
+
buildCount: 0,
|
|
120
|
+
webpackReady: false,
|
|
121
|
+
error: 'webpack not found',
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setState(prev => ({
|
|
126
|
+
...prev,
|
|
127
|
+
status: 'building',
|
|
128
|
+
message: 'Starting initial build...',
|
|
129
|
+
}));
|
|
130
|
+
const appType = getAppType(manifest);
|
|
131
|
+
const runner = new WebpackRunner(projectRoot, {
|
|
132
|
+
mode: 'development',
|
|
133
|
+
watch: true,
|
|
134
|
+
cssPrefix: manifest.id,
|
|
135
|
+
restApp: appType === 'rest',
|
|
136
|
+
});
|
|
137
|
+
webpackRunnerRef.current = runner;
|
|
138
|
+
await runner.watch(handleBuildComplete);
|
|
139
|
+
setState(prev => ({
|
|
140
|
+
...prev,
|
|
141
|
+
status: 'watching',
|
|
142
|
+
message: 'Building...',
|
|
143
|
+
webpackReady: true,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Non-bundled app: just copy and deploy
|
|
148
|
+
setState(prev => ({
|
|
149
|
+
...prev,
|
|
150
|
+
status: 'building',
|
|
151
|
+
message: 'Copying files...',
|
|
152
|
+
}));
|
|
153
|
+
await handleBuildComplete({
|
|
154
|
+
success: true,
|
|
155
|
+
stats: { time: 0, hash: '', assets: [] },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
setState({
|
|
161
|
+
status: 'error',
|
|
162
|
+
message: error instanceof Error ? error.message : String(error),
|
|
163
|
+
buildCount: 0,
|
|
164
|
+
webpackReady: false,
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
startWatch();
|
|
170
|
+
// Cleanup
|
|
171
|
+
return () => {
|
|
172
|
+
if (webpackRunnerRef.current) {
|
|
173
|
+
webpackRunnerRef.current.close().catch(() => { });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}, [projectRoot, manifest, handleBuildComplete]);
|
|
177
|
+
// Handle Ctrl+C
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
const handleExit = () => {
|
|
180
|
+
if (webpackRunnerRef.current) {
|
|
181
|
+
webpackRunnerRef.current.close().catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
exit();
|
|
184
|
+
};
|
|
185
|
+
process.on('SIGINT', handleExit);
|
|
186
|
+
process.on('SIGTERM', handleExit);
|
|
187
|
+
return () => {
|
|
188
|
+
process.off('SIGINT', handleExit);
|
|
189
|
+
process.off('SIGTERM', handleExit);
|
|
190
|
+
};
|
|
191
|
+
}, [exit]);
|
|
192
|
+
const getStatusType = () => {
|
|
193
|
+
switch (state.status) {
|
|
194
|
+
case 'error':
|
|
195
|
+
return 'error';
|
|
196
|
+
case 'ready':
|
|
197
|
+
return 'success';
|
|
198
|
+
default:
|
|
199
|
+
return 'running';
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const getStatusLabel = () => {
|
|
203
|
+
switch (state.status) {
|
|
204
|
+
case 'initializing':
|
|
205
|
+
return 'Initializing';
|
|
206
|
+
case 'watching':
|
|
207
|
+
return 'Watching';
|
|
208
|
+
case 'building':
|
|
209
|
+
return 'Building';
|
|
210
|
+
case 'signing':
|
|
211
|
+
return 'Signing';
|
|
212
|
+
case 'deploying':
|
|
213
|
+
return 'Deploying';
|
|
214
|
+
case 'ready':
|
|
215
|
+
return 'Ready';
|
|
216
|
+
case 'error':
|
|
217
|
+
return 'Error';
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
221
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
222
|
+
React.createElement(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message })),
|
|
223
|
+
state.buildCount > 0 && (React.createElement(Box, { marginLeft: 2, marginBottom: 1 },
|
|
224
|
+
React.createElement(Text, { dimColor: true },
|
|
225
|
+
"Builds: ",
|
|
226
|
+
state.buildCount,
|
|
227
|
+
state.lastBuildTime ? ` | Last build: ${state.lastBuildTime}ms` : '',
|
|
228
|
+
signed ? ' | Signed mode' : ''))),
|
|
229
|
+
React.createElement(Box, { marginLeft: 2, marginBottom: 1 },
|
|
230
|
+
React.createElement(Text, { dimColor: true },
|
|
231
|
+
manifest.name,
|
|
232
|
+
" v",
|
|
233
|
+
manifest.version)),
|
|
234
|
+
devProperties && (React.createElement(Box, { marginLeft: 2, marginBottom: 1 },
|
|
235
|
+
React.createElement(Text, { dimColor: true },
|
|
236
|
+
"Target: ",
|
|
237
|
+
devProperties.domain,
|
|
238
|
+
"/",
|
|
239
|
+
devProperties.siteName,
|
|
240
|
+
"/",
|
|
241
|
+
devProperties.addonName))),
|
|
242
|
+
state.status === 'error' && state.error && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
243
|
+
React.createElement(Text, { color: "red" }, state.error))),
|
|
244
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
245
|
+
state.status === 'error' && onRetryCredentials && (React.createElement(Text, { dimColor: true }, "Press r to retry with new credentials")),
|
|
246
|
+
onBack ? (React.createElement(Text, { dimColor: true }, "Press q or Esc to return to menu (Ctrl+C to stop process)")) : (React.createElement(Text, { dimColor: true }, "Press Ctrl+C to stop")))));
|
|
247
|
+
}
|
|
248
|
+
export const devCommand = {
|
|
249
|
+
name: 'dev',
|
|
250
|
+
description: 'Start development server with watch mode',
|
|
251
|
+
requiresProject: true,
|
|
252
|
+
flags: {
|
|
253
|
+
signed: {
|
|
254
|
+
type: 'boolean',
|
|
255
|
+
description: 'Use signed mode (sign before each deploy)',
|
|
256
|
+
alias: 's',
|
|
257
|
+
default: false,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
async execute({ project, flags }) {
|
|
261
|
+
// Check if dev properties are configured
|
|
262
|
+
if (!project.hasDevProperties || !project.devProperties) {
|
|
263
|
+
console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
|
|
264
|
+
console.log('Create a .dev_properties.json file with domain, siteName, addonName, username, and password.\n');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const signed = Boolean(flags['signed']);
|
|
268
|
+
let signingCredentials;
|
|
269
|
+
// If signed mode, prompt for signing password
|
|
270
|
+
if (signed) {
|
|
271
|
+
if (!project.hasSigningProperties || !project.devProperties.signingUsername) {
|
|
272
|
+
console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
|
|
273
|
+
console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
console.log('');
|
|
277
|
+
const password = await promptPassword('Signing password (developer.sitevision.se): ');
|
|
278
|
+
if (!password) {
|
|
279
|
+
console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
signingCredentials = {
|
|
283
|
+
username: project.devProperties.signingUsername,
|
|
284
|
+
password,
|
|
285
|
+
certificateName: project.devProperties.certificateName,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const { waitUntilExit } = render(React.createElement(DevScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, signed: signed, signingCredentials: signingCredentials }));
|
|
289
|
+
await waitUntilExit();
|
|
290
|
+
},
|
|
291
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { devCommand } from './dev.js';
|
|
2
|
+
import { buildCommand } from './build.js';
|
|
3
|
+
import { deployCommand } from './deploy.js';
|
|
4
|
+
import { infoCommand } from './info.js';
|
|
5
|
+
import { setupSigningCommand } from './setup-signing.js';
|
|
6
|
+
import { signCommand } from './sign.js';
|
|
7
|
+
export const commands = [
|
|
8
|
+
devCommand,
|
|
9
|
+
buildCommand,
|
|
10
|
+
signCommand,
|
|
11
|
+
deployCommand,
|
|
12
|
+
infoCommand,
|
|
13
|
+
setupSigningCommand,
|
|
14
|
+
];
|
|
15
|
+
export function getCommand(name) {
|
|
16
|
+
return commands.find((cmd) => cmd.name === name);
|
|
17
|
+
}
|
|
18
|
+
export function getAllCommands() {
|
|
19
|
+
return commands;
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { getAppType } from '../utils/project-detection.js';
|
|
5
|
+
function InfoScreen({ project }) {
|
|
6
|
+
const appType = getAppType(project.manifest);
|
|
7
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
8
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
9
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Sitevision Project Information")),
|
|
10
|
+
React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
|
|
11
|
+
React.createElement(Box, null,
|
|
12
|
+
React.createElement(Text, { bold: true }, "Name: "),
|
|
13
|
+
React.createElement(Text, null, project.manifest.name)),
|
|
14
|
+
React.createElement(Box, null,
|
|
15
|
+
React.createElement(Text, { bold: true }, "ID: "),
|
|
16
|
+
React.createElement(Text, null, project.manifest.id)),
|
|
17
|
+
React.createElement(Box, null,
|
|
18
|
+
React.createElement(Text, { bold: true }, "Version: "),
|
|
19
|
+
React.createElement(Text, null, project.manifest.version)),
|
|
20
|
+
React.createElement(Box, null,
|
|
21
|
+
React.createElement(Text, { bold: true }, "Type: "),
|
|
22
|
+
React.createElement(Text, { color: "green" }, project.manifest.type),
|
|
23
|
+
React.createElement(Text, { dimColor: true },
|
|
24
|
+
" (",
|
|
25
|
+
appType,
|
|
26
|
+
")")),
|
|
27
|
+
React.createElement(Box, null,
|
|
28
|
+
React.createElement(Text, { bold: true }, "Bundled: "),
|
|
29
|
+
React.createElement(Text, null, project.manifest.bundled ? 'Yes' : 'No'))),
|
|
30
|
+
project.hasDevProperties && project.devProperties && (React.createElement(React.Fragment, null,
|
|
31
|
+
React.createElement(Box, { marginTop: 1, marginBottom: 1 },
|
|
32
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Development Configuration")),
|
|
33
|
+
React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
|
|
34
|
+
React.createElement(Box, null,
|
|
35
|
+
React.createElement(Text, { bold: true }, "Domain: "),
|
|
36
|
+
React.createElement(Text, null, project.devProperties.domain)),
|
|
37
|
+
React.createElement(Box, null,
|
|
38
|
+
React.createElement(Text, { bold: true }, "Site: "),
|
|
39
|
+
React.createElement(Text, null, project.devProperties.siteName)),
|
|
40
|
+
React.createElement(Box, null,
|
|
41
|
+
React.createElement(Text, { bold: true }, "Addon: "),
|
|
42
|
+
React.createElement(Text, null, project.devProperties.addonName)),
|
|
43
|
+
React.createElement(Box, null,
|
|
44
|
+
React.createElement(Text, { bold: true }, "Username: "),
|
|
45
|
+
React.createElement(Text, null, project.devProperties.username)),
|
|
46
|
+
React.createElement(Box, null,
|
|
47
|
+
React.createElement(Text, { bold: true }, "Use HTTP: "),
|
|
48
|
+
React.createElement(Text, null, project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No'))))),
|
|
49
|
+
!project.hasDevProperties && (React.createElement(Box, { marginTop: 1 },
|
|
50
|
+
React.createElement(Text, { color: "yellow" },
|
|
51
|
+
"\u26A0 No dev properties found. Run",
|
|
52
|
+
' ',
|
|
53
|
+
React.createElement(Text, { bold: true }, "setup-dev-properties"),
|
|
54
|
+
" to configure."))),
|
|
55
|
+
React.createElement(Box, { marginTop: 1 },
|
|
56
|
+
React.createElement(Text, { bold: true }, "Project Root: "),
|
|
57
|
+
React.createElement(Text, { dimColor: true }, project.root))));
|
|
58
|
+
}
|
|
59
|
+
export const infoCommand = {
|
|
60
|
+
name: 'info',
|
|
61
|
+
description: 'Show project information',
|
|
62
|
+
requiresProject: true,
|
|
63
|
+
async execute({ project }) {
|
|
64
|
+
render(React.createElement(InfoScreen, { project: project }));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
function question(rl, prompt) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
rl.question(prompt, (answer) => {
|
|
7
|
+
resolve(answer);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export const setupSigningCommand = {
|
|
12
|
+
name: 'setup-signing',
|
|
13
|
+
description: 'Configure signing credentials for developer.sitevision.se',
|
|
14
|
+
requiresProject: true,
|
|
15
|
+
async execute({ project }) {
|
|
16
|
+
const rl = readline.createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
});
|
|
20
|
+
console.log('\n\x1b[36m\x1b[1mSetup Signing Credentials\x1b[0m\n');
|
|
21
|
+
console.log('Configure credentials for signing apps on developer.sitevision.se');
|
|
22
|
+
console.log('(Password will be prompted when running signing commands)\n');
|
|
23
|
+
// Find existing dev properties file
|
|
24
|
+
const devPropertiesPaths = [
|
|
25
|
+
path.join(project.root, '.dev_properties.json'),
|
|
26
|
+
path.join(project.root, '.dev-properties.json'),
|
|
27
|
+
];
|
|
28
|
+
let devPropertiesPath = devPropertiesPaths[0];
|
|
29
|
+
let existingProperties = {};
|
|
30
|
+
for (const p of devPropertiesPaths) {
|
|
31
|
+
if (fs.existsSync(p)) {
|
|
32
|
+
devPropertiesPath = p;
|
|
33
|
+
try {
|
|
34
|
+
existingProperties = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Invalid file, start fresh
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Get signing username
|
|
44
|
+
const defaultUsername = existingProperties['signingUsername'] || '';
|
|
45
|
+
const usernamePrompt = defaultUsername
|
|
46
|
+
? `Signing username [${defaultUsername}]: `
|
|
47
|
+
: 'Signing username: ';
|
|
48
|
+
let signingUsername = await question(rl, usernamePrompt);
|
|
49
|
+
if (!signingUsername && defaultUsername) {
|
|
50
|
+
signingUsername = defaultUsername;
|
|
51
|
+
}
|
|
52
|
+
if (!signingUsername) {
|
|
53
|
+
console.log('\x1b[31mError: Signing username is required\x1b[0m');
|
|
54
|
+
rl.close();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Get certificate name (optional)
|
|
58
|
+
const defaultCertName = existingProperties['certificateName'] || '';
|
|
59
|
+
const certPrompt = defaultCertName
|
|
60
|
+
? `Certificate name (blank for default) [${defaultCertName}]: `
|
|
61
|
+
: 'Certificate name (blank for default): ';
|
|
62
|
+
let certificateName = await question(rl, certPrompt);
|
|
63
|
+
if (!certificateName && defaultCertName) {
|
|
64
|
+
certificateName = defaultCertName;
|
|
65
|
+
}
|
|
66
|
+
rl.close();
|
|
67
|
+
// Update dev properties (remove any stored password from old config)
|
|
68
|
+
const { signingPassword: _removed, ...cleanedProperties } = existingProperties;
|
|
69
|
+
const updatedProperties = {
|
|
70
|
+
...cleanedProperties,
|
|
71
|
+
signingUsername,
|
|
72
|
+
...(certificateName && { certificateName }),
|
|
73
|
+
};
|
|
74
|
+
fs.writeFileSync(devPropertiesPath, JSON.stringify(updatedProperties, null, 2));
|
|
75
|
+
console.log(`\n\x1b[32mSigning credentials saved to ${path.basename(devPropertiesPath)}\x1b[0m\n`);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
rl.close();
|
|
79
|
+
console.error('\x1b[31mError setting up signing credentials:\x1b[0m', error);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type Command } from './types.js';
|
|
3
|
+
import type { SitevisionManifest, DevProperties } from '../types/index.js';
|
|
4
|
+
interface SignScreenProps {
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
manifest: SitevisionManifest;
|
|
7
|
+
devProperties: DevProperties;
|
|
8
|
+
password: string;
|
|
9
|
+
onBack?: () => void;
|
|
10
|
+
onRetryCredentials?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function SignScreen({ projectRoot, manifest, devProperties, password, onBack, onRetryCredentials }: SignScreenProps): React.JSX.Element;
|
|
13
|
+
export declare const signCommand: Command;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, Box, Text, useInput } from 'ink';
|
|
3
|
+
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
4
|
+
import { signApp } from '../utils/sitevision-api.js';
|
|
5
|
+
import { promptPassword } from '../utils/password-prompt.js';
|
|
6
|
+
import { getZipPath, getSignedZipPath, } from '../utils/project-detection.js';
|
|
7
|
+
import { formatFileSize, getZipSize, zipExists } from '../utils/zip.js';
|
|
8
|
+
export function SignScreen({ projectRoot, manifest, devProperties, password, onBack, onRetryCredentials }) {
|
|
9
|
+
const [state, setState] = React.useState({
|
|
10
|
+
status: 'signing',
|
|
11
|
+
message: 'Signing app via developer.sitevision.se...',
|
|
12
|
+
});
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if (state.status !== 'signing') {
|
|
15
|
+
if (onBack && (key.escape || input === 'q')) {
|
|
16
|
+
onBack();
|
|
17
|
+
}
|
|
18
|
+
if (onRetryCredentials && state.status === 'error' && input === 'r') {
|
|
19
|
+
onRetryCredentials();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
async function runSign() {
|
|
25
|
+
try {
|
|
26
|
+
const zipPath = getZipPath(projectRoot, manifest);
|
|
27
|
+
const signedZipPath = getSignedZipPath(projectRoot, manifest);
|
|
28
|
+
// Validate zip exists
|
|
29
|
+
if (!zipExists(zipPath)) {
|
|
30
|
+
setState({
|
|
31
|
+
status: 'error',
|
|
32
|
+
error: `Zip file not found: ${zipPath}\nRun 'build' first to create the zip file.`,
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Sign the app
|
|
37
|
+
const result = await signApp(zipPath, {
|
|
38
|
+
username: devProperties.signingUsername,
|
|
39
|
+
password,
|
|
40
|
+
certificateName: devProperties.certificateName,
|
|
41
|
+
}, signedZipPath);
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
setState({
|
|
44
|
+
status: 'error',
|
|
45
|
+
error: result.error || 'Signing failed',
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const signedSize = getZipSize(signedZipPath);
|
|
50
|
+
setState({
|
|
51
|
+
status: 'success',
|
|
52
|
+
message: 'App signed successfully',
|
|
53
|
+
signedPath: signedZipPath,
|
|
54
|
+
signedSize,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
setState({
|
|
59
|
+
status: 'error',
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
runSign();
|
|
65
|
+
}, [projectRoot, manifest, devProperties, password]);
|
|
66
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
67
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
68
|
+
React.createElement(StatusIndicator, { status: state.status === 'signing' ? 'running' : state.status, label: state.status === 'signing' ? 'Signing' : state.status === 'success' ? 'Signed' : 'Failed', message: state.message })),
|
|
69
|
+
state.status === 'success' && state.signedPath && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
|
|
70
|
+
React.createElement(Text, { color: "green" },
|
|
71
|
+
"Created: ",
|
|
72
|
+
state.signedPath),
|
|
73
|
+
state.signedSize !== undefined && (React.createElement(Text, { dimColor: true },
|
|
74
|
+
"Size: ",
|
|
75
|
+
formatFileSize(state.signedSize))))),
|
|
76
|
+
state.status === 'error' && state.error && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
77
|
+
React.createElement(Text, { color: "red" }, state.error))),
|
|
78
|
+
state.status !== 'signing' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
79
|
+
state.status === 'error' && onRetryCredentials && (React.createElement(Text, { dimColor: true }, "Press r to retry with new credentials")),
|
|
80
|
+
onBack && (React.createElement(Text, { dimColor: true }, "Press q or Esc to return to menu"))))));
|
|
81
|
+
}
|
|
82
|
+
export const signCommand = {
|
|
83
|
+
name: 'sign',
|
|
84
|
+
description: 'Sign the app for production deployment',
|
|
85
|
+
requiresProject: true,
|
|
86
|
+
async execute({ project }) {
|
|
87
|
+
// Check if signing credentials are configured
|
|
88
|
+
if (!project.hasSigningProperties || !project.devProperties?.signingUsername) {
|
|
89
|
+
console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
|
|
90
|
+
console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Prompt for password
|
|
94
|
+
console.log('');
|
|
95
|
+
const password = await promptPassword('Signing password (developer.sitevision.se: ');
|
|
96
|
+
if (!password) {
|
|
97
|
+
console.log('\x1b[31mError: Password is required\x1b[0m');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { waitUntilExit } = render(React.createElement(SignScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, password: password }));
|
|
101
|
+
await waitUntilExit();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ProjectInfo } from '../utils/project-detection.js';
|
|
2
|
+
export interface CommandContext {
|
|
3
|
+
project: ProjectInfo;
|
|
4
|
+
flags: Record<string, any>;
|
|
5
|
+
args: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface Command {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
requiresProject: boolean;
|
|
11
|
+
flags?: Record<string, {
|
|
12
|
+
type: 'string' | 'boolean';
|
|
13
|
+
description: string;
|
|
14
|
+
alias?: string;
|
|
15
|
+
default?: any;
|
|
16
|
+
}>;
|
|
17
|
+
execute: (context: CommandContext) => Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { DevProperties, PackageJson } from '../types/index.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
initialProperties?: DevProperties;
|
|
6
|
+
packageJson: PackageJson;
|
|
7
|
+
onComplete: () => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function DevPropertiesForm({ projectRoot, initialProperties, packageJson, onComplete, onCancel }: Props): React.JSX.Element;
|
|
11
|
+
export {};
|