sitevision-cli 0.4.0-beta.2 → 0.6.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +24 -4
- package/dist/commands/build.js +52 -4
- package/dist/commands/dev.js +139 -22
- package/dist/components/AnimatedLogo.d.ts +9 -0
- package/dist/components/AnimatedLogo.js +79 -0
- package/dist/components/InfoScreen.js +3 -1
- package/dist/utils/branding.d.ts +9 -0
- package/dist/utils/branding.js +18 -0
- package/dist/utils/process-runner.d.ts +0 -8
- package/dist/utils/process-runner.js +0 -15
- package/dist/utils/sitevision-api.d.ts +32 -0
- package/dist/utils/sitevision-api.js +111 -42
- package/dist/utils/sitevision-scripts-runner.d.ts +83 -0
- package/dist/utils/sitevision-scripts-runner.js +187 -0
- package/dist/utils/webpack-runner.d.ts +9 -0
- package/dist/utils/webpack-runner.js +30 -15
- package/dist/utils/zip.d.ts +10 -3
- package/dist/utils/zip.js +171 -59
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { render } from 'ink';
|
|
4
4
|
import { Text, Box } from 'ink';
|
|
5
5
|
import meow from 'meow';
|
|
@@ -11,7 +11,8 @@ import { promptYesNo } from './utils/password-prompt.js';
|
|
|
11
11
|
import { checkForUpdate } from './utils/version-check.js';
|
|
12
12
|
import { isFirstRun, markFirstRunComplete, getLastSeenVersion, setLastSeenVersion, } from './utils/config.js';
|
|
13
13
|
import { WelcomeScreen } from './components/WelcomeScreen.js';
|
|
14
|
-
import {
|
|
14
|
+
import { AnimatedLogo } from './components/AnimatedLogo.js';
|
|
15
|
+
import { printBranding, BIG_LOGO_WIDTH } from './utils/branding.js';
|
|
15
16
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
16
17
|
const cli = meow(`
|
|
17
18
|
Usage
|
|
@@ -84,6 +85,13 @@ function printMasthead(version) {
|
|
|
84
85
|
`${spaces(gap)}${DIM}${right}${RESET}${spaces(padding)}${CYAN}│${RESET}`);
|
|
85
86
|
console.log(`${CYAN}╰${border}╯${RESET}`);
|
|
86
87
|
}
|
|
88
|
+
// Play the one-shot animated wordmark and resolve once it finishes.
|
|
89
|
+
async function playIntro() {
|
|
90
|
+
await new Promise(resolve => {
|
|
91
|
+
const app = render(_jsx(AnimatedLogo, { onDone: () => app.unmount() }));
|
|
92
|
+
app.waitUntilExit().then(() => resolve(), () => resolve());
|
|
93
|
+
});
|
|
94
|
+
}
|
|
87
95
|
async function main() {
|
|
88
96
|
// On the very first run we show a dedicated welcome screen instead of the
|
|
89
97
|
// masthead, so the branding is the moment. Only when stdin is a TTY — the
|
|
@@ -95,12 +103,20 @@ async function main() {
|
|
|
95
103
|
// silently rather than claiming an update happened.
|
|
96
104
|
const lastSeen = getLastSeenVersion();
|
|
97
105
|
const isUpdate = !firstRun && lastSeen !== undefined && lastSeen !== pkg.version;
|
|
106
|
+
// On the plain interactive `svc` (no command), play the animated wordmark
|
|
107
|
+
// instead of the static masthead — but only when stdout is wide enough for
|
|
108
|
+
// the art and stdin is a TTY (so it doesn't run in CI / piped input).
|
|
109
|
+
const wantsIntro = !firstRun &&
|
|
110
|
+
!isUpdate &&
|
|
111
|
+
!commandName &&
|
|
112
|
+
Boolean(process.stdin.isTTY) &&
|
|
113
|
+
(process.stdout.columns ?? 0) >= BIG_LOGO_WIDTH;
|
|
98
114
|
if (!firstRun) {
|
|
99
115
|
if (isUpdate) {
|
|
100
116
|
printBranding();
|
|
101
117
|
console.log(`\x1b[32m\n ✨ Updated to v${pkg.version}\x1b[0m \x1b[2m(from v${lastSeen})\x1b[0m\n`);
|
|
102
118
|
}
|
|
103
|
-
else {
|
|
119
|
+
else if (!wantsIntro) {
|
|
104
120
|
printMasthead(pkg.version);
|
|
105
121
|
}
|
|
106
122
|
// Record the current version so the banner shows once per upgrade.
|
|
@@ -133,8 +149,12 @@ async function main() {
|
|
|
133
149
|
app.waitUntilExit().then(() => resolve(), () => resolve());
|
|
134
150
|
});
|
|
135
151
|
}
|
|
136
|
-
// If no command, show interactive menu
|
|
152
|
+
// If no command, show interactive menu (with the animated intro first when
|
|
153
|
+
// the terminal can fit it).
|
|
137
154
|
if (!commandName) {
|
|
155
|
+
if (wantsIntro) {
|
|
156
|
+
await playIntro();
|
|
157
|
+
}
|
|
138
158
|
render(_jsx(App, { project: project }));
|
|
139
159
|
return;
|
|
140
160
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render, Box, Text, useInput } from 'ink';
|
|
4
4
|
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
5
|
-
import { WebpackRunner } from '../utils/webpack-runner.js';
|
|
6
|
-
import {
|
|
5
|
+
import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
|
|
6
|
+
import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
|
|
7
|
+
import { copyStaticToBuild, copySrcToBuild, cleanBuild, formatFileSize, createBuildZip, getZipSize, zipExists, } from '../utils/zip.js';
|
|
7
8
|
import { isBundledApp, getAppType, getFullAppId, } from '../utils/project-detection.js';
|
|
8
9
|
export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }) {
|
|
9
10
|
const [state, setState] = React.useState({
|
|
@@ -25,8 +26,55 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
|
|
|
25
26
|
setState({ status: 'cleaning', message: 'Cleaning build directory...' });
|
|
26
27
|
cleanBuild(projectRoot);
|
|
27
28
|
// Step 2: Build or copy files
|
|
29
|
+
if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
|
|
30
|
+
// No project-local webpack config: delegate the whole build to
|
|
31
|
+
// @sitevision/sitevision-scripts, which compiles + zips to
|
|
32
|
+
// dist/<appId>.zip (the path the CLI's sign/deploy already use).
|
|
33
|
+
if (!hasSitevisionScripts(projectRoot)) {
|
|
34
|
+
setState({
|
|
35
|
+
status: 'error',
|
|
36
|
+
error: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Warn if the installed sitevision-scripts is outside the range
|
|
41
|
+
// the delegated build was validated against.
|
|
42
|
+
const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
|
|
43
|
+
setState({
|
|
44
|
+
status: 'building',
|
|
45
|
+
message: 'Building via sitevision-scripts...',
|
|
46
|
+
warning,
|
|
47
|
+
});
|
|
48
|
+
const sitevisionResult = await runSitevisionScriptsBuild(projectRoot);
|
|
49
|
+
if (!sitevisionResult.success) {
|
|
50
|
+
setState({
|
|
51
|
+
status: 'error',
|
|
52
|
+
error: `${sitevisionResult.error}\n${sitevisionResult.output.slice(-1000)}`,
|
|
53
|
+
warning,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// sitevision-scripts already produced the zip; report it directly.
|
|
58
|
+
const zipPath = getDelegatedZipPath(projectRoot, manifest.id);
|
|
59
|
+
if (!zipExists(zipPath)) {
|
|
60
|
+
setState({
|
|
61
|
+
status: 'error',
|
|
62
|
+
error: `Build reported success but no zip was found at ${zipPath}.`,
|
|
63
|
+
warning,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setState({
|
|
68
|
+
status: 'success',
|
|
69
|
+
message: 'Build complete',
|
|
70
|
+
zipPath,
|
|
71
|
+
zipSize: getZipSize(zipPath),
|
|
72
|
+
warning,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
28
76
|
if (isBundled) {
|
|
29
|
-
//
|
|
77
|
+
// Project ships its own webpack config: build it in-process.
|
|
30
78
|
if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
|
|
31
79
|
setState({
|
|
32
80
|
status: 'error',
|
|
@@ -122,7 +170,7 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
|
|
|
122
170
|
return 'Build failed';
|
|
123
171
|
}
|
|
124
172
|
};
|
|
125
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
|
|
173
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
|
|
126
174
|
state.result.stats.assets.length > 0 && (_jsxs(Text, { dimColor: true, children: ["Assets: ", state.result.stats.assets.join(', ')] }))] })), state.status === 'success' && state.zipPath && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 Created: ", state.zipPath] }), state.zipSize !== undefined && (_jsxs(Text, { dimColor: true, children: [" Size: ", formatFileSize(state.zipSize)] }))] })), state.result?.warnings && state.result.warnings.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "Warnings:" }), state.result.warnings.slice(0, 5).map((warning, i) => (_jsx(Text, { color: "yellow", dimColor: true, children: warning.substring(0, 200) }, i))), state.result.warnings.length > 5 && (_jsxs(Text, { color: "yellow", dimColor: true, children: ["...and ", state.result.warnings.length - 5, " more"] }))] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), onBack && (state.status === 'success' || state.status === 'error') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" }) }))] }));
|
|
127
175
|
}
|
|
128
176
|
export const buildCommand = {
|
package/dist/commands/dev.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import React from 'react';
|
|
3
5
|
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
4
6
|
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
5
|
-
import { WebpackRunner } from '../utils/webpack-runner.js';
|
|
7
|
+
import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
|
|
8
|
+
import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
|
|
6
9
|
import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
|
|
7
10
|
import { signApp, deployApp } from '../utils/sitevision-api.js';
|
|
8
11
|
import { setDeployPassword } from '../utils/keychain.js';
|
|
@@ -26,23 +29,13 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
|
|
|
26
29
|
}
|
|
27
30
|
});
|
|
28
31
|
const webpackRunnerRef = React.useRef(null);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
error: result.errors?.join('\n'),
|
|
36
|
-
}));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
32
|
+
const watchersRef = React.useRef([]);
|
|
33
|
+
const debounceTimerRef = React.useRef(null);
|
|
34
|
+
const isBuildingRef = React.useRef(false);
|
|
35
|
+
const pendingRebuildRef = React.useRef(false);
|
|
36
|
+
// Sign (if needed) and deploy an already-built zip, updating UI state.
|
|
37
|
+
const signAndDeploy = React.useCallback(async (zipPath, buildTime) => {
|
|
39
38
|
try {
|
|
40
|
-
// Copy static files
|
|
41
|
-
copyStaticToBuild(projectRoot);
|
|
42
|
-
// Create zip
|
|
43
|
-
const appId = getFullAppId(manifest.id);
|
|
44
|
-
await createBuildZip(projectRoot, appId);
|
|
45
|
-
const zipPath = getZipPath(projectRoot, manifest);
|
|
46
39
|
let deployZipPath = zipPath;
|
|
47
40
|
// Sign if needed
|
|
48
41
|
if (signed && signingCredentials) {
|
|
@@ -94,7 +87,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
|
|
|
94
87
|
status: 'ready',
|
|
95
88
|
message: 'Deployed. Watching for changes...',
|
|
96
89
|
buildCount: prev.buildCount + 1,
|
|
97
|
-
lastBuildTime:
|
|
90
|
+
lastBuildTime: buildTime,
|
|
98
91
|
error: undefined,
|
|
99
92
|
}));
|
|
100
93
|
}
|
|
@@ -107,14 +100,122 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
|
|
|
107
100
|
}));
|
|
108
101
|
}
|
|
109
102
|
}, [projectRoot, manifest, devProperties, signed, signingCredentials]);
|
|
103
|
+
// In-house webpack path: copy static, zip, then sign + deploy.
|
|
104
|
+
const handleBuildComplete = React.useCallback(async (result) => {
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
setState(prev => ({
|
|
107
|
+
...prev,
|
|
108
|
+
status: 'error',
|
|
109
|
+
message: result.errors?.join('\n') || 'Build failed',
|
|
110
|
+
error: result.errors?.join('\n'),
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
copyStaticToBuild(projectRoot);
|
|
116
|
+
const appId = getFullAppId(manifest.id);
|
|
117
|
+
await createBuildZip(projectRoot, appId);
|
|
118
|
+
await signAndDeploy(getZipPath(projectRoot, manifest), result.stats?.time);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
setState(prev => ({
|
|
122
|
+
...prev,
|
|
123
|
+
status: 'error',
|
|
124
|
+
message: error instanceof Error ? error.message : String(error),
|
|
125
|
+
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
}, [projectRoot, manifest, signAndDeploy]);
|
|
129
|
+
// Delegated path: full `sitevision-scripts build` then sign + deploy.
|
|
130
|
+
// Coalesces overlapping triggers so a save mid-build queues one rebuild.
|
|
131
|
+
const runDelegatedBuild = React.useCallback(async () => {
|
|
132
|
+
if (isBuildingRef.current) {
|
|
133
|
+
pendingRebuildRef.current = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
isBuildingRef.current = true;
|
|
137
|
+
const buildOnce = async () => {
|
|
138
|
+
pendingRebuildRef.current = false;
|
|
139
|
+
setState(prev => ({
|
|
140
|
+
...prev,
|
|
141
|
+
status: 'building',
|
|
142
|
+
message: 'Building via sitevision-scripts...',
|
|
143
|
+
}));
|
|
144
|
+
const result = await runSitevisionScriptsBuild(projectRoot);
|
|
145
|
+
if (result.success) {
|
|
146
|
+
await signAndDeploy(getDelegatedZipPath(projectRoot, manifest.id));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
setState(prev => ({
|
|
150
|
+
...prev,
|
|
151
|
+
status: 'error',
|
|
152
|
+
message: result.error ?? 'Build failed',
|
|
153
|
+
error: `${result.error}\n${result.output.slice(-1000)}`,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
if (pendingRebuildRef.current) {
|
|
157
|
+
await buildOnce();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
await buildOnce();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
isBuildingRef.current = false;
|
|
165
|
+
}
|
|
166
|
+
}, [projectRoot, manifest, signAndDeploy]);
|
|
167
|
+
// Watch source files and trigger a delegated rebuild (debounced).
|
|
168
|
+
const startFileWatcher = React.useCallback(() => {
|
|
169
|
+
const targets = [
|
|
170
|
+
'src',
|
|
171
|
+
'static',
|
|
172
|
+
'i18n',
|
|
173
|
+
'resource',
|
|
174
|
+
'config',
|
|
175
|
+
'manifest.json',
|
|
176
|
+
]
|
|
177
|
+
.map(name => path.join(projectRoot, name))
|
|
178
|
+
.filter(target => fs.existsSync(target));
|
|
179
|
+
for (const target of targets) {
|
|
180
|
+
const isDir = fs.statSync(target).isDirectory();
|
|
181
|
+
const watcher = fs.watch(target, { recursive: isDir }, () => {
|
|
182
|
+
if (debounceTimerRef.current) {
|
|
183
|
+
clearTimeout(debounceTimerRef.current);
|
|
184
|
+
}
|
|
185
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
186
|
+
void runDelegatedBuild();
|
|
187
|
+
}, 300);
|
|
188
|
+
});
|
|
189
|
+
watchersRef.current.push(watcher);
|
|
190
|
+
}
|
|
191
|
+
}, [projectRoot, runDelegatedBuild]);
|
|
110
192
|
React.useEffect(() => {
|
|
111
193
|
const isBundled = isBundledApp(manifest);
|
|
112
194
|
async function startWatch() {
|
|
113
195
|
try {
|
|
114
196
|
// Clean build directory
|
|
115
197
|
cleanBuild(projectRoot);
|
|
116
|
-
if (isBundled) {
|
|
117
|
-
//
|
|
198
|
+
if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
|
|
199
|
+
// No local webpack config: delegate each build to
|
|
200
|
+
// sitevision-scripts (full rebuild) and watch source files
|
|
201
|
+
// ourselves, keeping the CLI's own sign + deploy flow.
|
|
202
|
+
if (!hasSitevisionScripts(projectRoot)) {
|
|
203
|
+
setState({
|
|
204
|
+
status: 'error',
|
|
205
|
+
message: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
|
|
206
|
+
buildCount: 0,
|
|
207
|
+
webpackReady: false,
|
|
208
|
+
error: 'No build pipeline available',
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
|
|
213
|
+
setState(prev => ({ ...prev, webpackReady: true, warning }));
|
|
214
|
+
startFileWatcher();
|
|
215
|
+
await runDelegatedBuild();
|
|
216
|
+
}
|
|
217
|
+
else if (isBundled) {
|
|
218
|
+
// Project ships its own webpack config: incremental in-process watch.
|
|
118
219
|
if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
|
|
119
220
|
setState({
|
|
120
221
|
status: 'error',
|
|
@@ -175,14 +276,30 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
|
|
|
175
276
|
if (webpackRunnerRef.current) {
|
|
176
277
|
webpackRunnerRef.current.close().catch(() => { });
|
|
177
278
|
}
|
|
279
|
+
if (debounceTimerRef.current) {
|
|
280
|
+
clearTimeout(debounceTimerRef.current);
|
|
281
|
+
}
|
|
282
|
+
for (const watcher of watchersRef.current) {
|
|
283
|
+
watcher.close();
|
|
284
|
+
}
|
|
285
|
+
watchersRef.current = [];
|
|
178
286
|
};
|
|
179
|
-
}, [
|
|
287
|
+
}, [
|
|
288
|
+
projectRoot,
|
|
289
|
+
manifest,
|
|
290
|
+
handleBuildComplete,
|
|
291
|
+
runDelegatedBuild,
|
|
292
|
+
startFileWatcher,
|
|
293
|
+
]);
|
|
180
294
|
// Handle Ctrl+C
|
|
181
295
|
React.useEffect(() => {
|
|
182
296
|
const handleExit = () => {
|
|
183
297
|
if (webpackRunnerRef.current) {
|
|
184
298
|
webpackRunnerRef.current.close().catch(() => { });
|
|
185
299
|
}
|
|
300
|
+
for (const watcher of watchersRef.current) {
|
|
301
|
+
watcher.close();
|
|
302
|
+
}
|
|
186
303
|
exit();
|
|
187
304
|
};
|
|
188
305
|
process.on('SIGINT', handleExit);
|
|
@@ -220,7 +337,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
|
|
|
220
337
|
return 'Error';
|
|
221
338
|
}
|
|
222
339
|
};
|
|
223
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
|
|
340
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
|
|
224
341
|
? ` | Last build: ${state.lastBuildTime}ms`
|
|
225
342
|
: '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [manifest.name, " v", manifest.version] }) }), devProperties && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Target: ", devProperties.domain, "/", devProperties.siteName, "/", devProperties.addonName] }) })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack ? (_jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu (Ctrl+C to stop process)" })) : (_jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop" }))] })] }));
|
|
226
343
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
onDone: () => void;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* One-shot startup flair: wipes the big wordmark in left-to-right while a
|
|
6
|
+
* rainbow gradient drifts across it, then calls `onDone`. Purely decorative.
|
|
7
|
+
*/
|
|
8
|
+
export declare function AnimatedLogo({ onDone }: Props): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { BIG_LOGO, BIG_LOGO_WIDTH } from '../utils/branding.js';
|
|
5
|
+
const FRAME_MS = 45;
|
|
6
|
+
const REVEAL_COLS_PER_FRAME = 9; // how fast the wipe sweeps left → right
|
|
7
|
+
const HOLD_FRAMES = 20; // frames to keep cycling colours once fully revealed
|
|
8
|
+
const REVEAL_FRAMES = Math.ceil(BIG_LOGO_WIDTH / REVEAL_COLS_PER_FRAME);
|
|
9
|
+
const TOTAL_FRAMES = REVEAL_FRAMES + HOLD_FRAMES;
|
|
10
|
+
// Convert HSL (h in degrees, s/l in 0..1) to a #rrggbb string for ink/chalk.
|
|
11
|
+
function hslToHex(h, s, l) {
|
|
12
|
+
const hue = h / 360;
|
|
13
|
+
const a = s * Math.min(l, 1 - l);
|
|
14
|
+
const channel = (n) => {
|
|
15
|
+
const scaled = hue * 12;
|
|
16
|
+
const k = (n + scaled) % 12;
|
|
17
|
+
const offset = a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
18
|
+
const value = l - offset;
|
|
19
|
+
return Math.round(255 * value)
|
|
20
|
+
.toString(16)
|
|
21
|
+
.padStart(2, '0');
|
|
22
|
+
};
|
|
23
|
+
return `#${channel(0)}${channel(8)}${channel(4)}`;
|
|
24
|
+
}
|
|
25
|
+
function buildSpans(line, y, frame, reveal) {
|
|
26
|
+
const spans = [];
|
|
27
|
+
for (const [x, char] of [...line].entries()) {
|
|
28
|
+
const hidden = x >= reveal;
|
|
29
|
+
const blank = char === ' ' || hidden;
|
|
30
|
+
// Moving diagonal rainbow: hue depends on column + row + time, quantised
|
|
31
|
+
// so neighbouring characters share a colour and runs stay long.
|
|
32
|
+
const col = x * 1.6;
|
|
33
|
+
const row = y * 6;
|
|
34
|
+
const time = frame * 7;
|
|
35
|
+
const stepped = Math.round((col + row + time) / 8) * 8;
|
|
36
|
+
const hue = blank ? undefined : stepped % 360;
|
|
37
|
+
// Shadow characters sit darker than the solid blocks for a bit of depth.
|
|
38
|
+
const color = hue === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: hslToHex(hue, 0.95, char === '░' ? 0.32 : 0.58);
|
|
41
|
+
const text = hidden ? ' ' : char;
|
|
42
|
+
const last = spans.at(-1);
|
|
43
|
+
if (last && last.color === color) {
|
|
44
|
+
last.text += text;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
spans.push({ text, color });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return spans;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One-shot startup flair: wipes the big wordmark in left-to-right while a
|
|
54
|
+
* rainbow gradient drifts across it, then calls `onDone`. Purely decorative.
|
|
55
|
+
*/
|
|
56
|
+
export function AnimatedLogo({ onDone }) {
|
|
57
|
+
const [frame, setFrame] = useState(0);
|
|
58
|
+
const intervalRef = useRef(undefined);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
intervalRef.current = setInterval(() => {
|
|
61
|
+
setFrame(current => current + 1);
|
|
62
|
+
}, FRAME_MS);
|
|
63
|
+
return () => {
|
|
64
|
+
clearInterval(intervalRef.current);
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
// Stop the loop and notify the parent exactly once, when the last frame is
|
|
68
|
+
// reached. Kept out of the setFrame updater so that updater stays pure.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (frame >= TOTAL_FRAMES) {
|
|
71
|
+
clearInterval(intervalRef.current);
|
|
72
|
+
onDone();
|
|
73
|
+
}
|
|
74
|
+
}, [frame, onDone]);
|
|
75
|
+
const reveal = frame >= REVEAL_FRAMES
|
|
76
|
+
? BIG_LOGO_WIDTH
|
|
77
|
+
: (frame + 1) * REVEAL_COLS_PER_FRAME;
|
|
78
|
+
return (_jsx(Box, { flexDirection: "column", padding: 1, children: BIG_LOGO.map((line, y) => (_jsx(Text, { children: buildSpans(line, y, frame, reveal).map((span, index) => (_jsx(Fragment, { children: span.color ? (_jsx(Text, { color: span.color, children: span.text })) : (_jsx(Text, { children: span.text })) }, index))) }, y))) }));
|
|
79
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { getAppType } from '../utils/project-detection.js';
|
|
4
|
+
import { checkSitevisionScriptsCompatibility } from '../utils/sitevision-scripts-runner.js';
|
|
4
5
|
export function InfoScreen({ project, onBack }) {
|
|
5
6
|
const appType = getAppType(project.manifest);
|
|
7
|
+
const scriptsCompat = checkSitevisionScriptsCompatibility(project.root);
|
|
6
8
|
useInput((input, key) => {
|
|
7
9
|
if (key.escape || input === 'q' || key.return) {
|
|
8
10
|
onBack();
|
|
9
11
|
}
|
|
10
12
|
});
|
|
11
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision Project Information" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: project.manifest.name })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ID: " }), _jsx(Text, { children: project.manifest.id })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Version: " }), _jsx(Text, { children: project.manifest.version })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Type: " }), _jsx(Text, { color: "green", children: project.manifest.type }), _jsxs(Text, { dimColor: true, children: [" (", appType, ")"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Bundled: " }), _jsx(Text, { children: project.manifest.bundled ? 'Yes' : 'No' })] })] }), project.hasDevProperties && project.devProperties && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Development Configuration" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Domain: " }), _jsx(Text, { children: project.devProperties.domain })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Site: " }), _jsx(Text, { children: project.devProperties.siteName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Addon: " }), _jsx(Text, { children: project.devProperties.addonName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Username: " }), _jsx(Text, { children: project.devProperties.username })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Use HTTP: " }), _jsx(Text, { children: project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No' })] })] })] })), !project.hasDevProperties && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 No dev properties found. Run setup-dev-properties to configure." }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Project Root: " }), _jsx(Text, { dimColor: true, children: project.root })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: "Press Enter or ESC to return to menu" }) })] }));
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Sitevision Project Information" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: project.manifest.name })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ID: " }), _jsx(Text, { children: project.manifest.id })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Version: " }), _jsx(Text, { children: project.manifest.version })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Type: " }), _jsx(Text, { color: "green", children: project.manifest.type }), _jsxs(Text, { dimColor: true, children: [" (", appType, ")"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Bundled: " }), _jsx(Text, { children: project.manifest.bundled ? 'Yes' : 'No' })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Build Tooling" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "sitevision-scripts: " }), _jsx(Text, { children: scriptsCompat.installed ?? 'not installed' }), _jsxs(Text, { dimColor: true, children: [" (supported ", scriptsCompat.supportedRange, ")"] })] }), scriptsCompat.warning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", scriptsCompat.warning] }) }))] }), project.hasDevProperties && project.devProperties && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Development Configuration" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Domain: " }), _jsx(Text, { children: project.devProperties.domain })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Site: " }), _jsx(Text, { children: project.devProperties.siteName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Addon: " }), _jsx(Text, { children: project.devProperties.addonName })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Username: " }), _jsx(Text, { children: project.devProperties.username })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Use HTTP: " }), _jsx(Text, { children: project.devProperties.useHTTPForDevDeploy ? 'Yes' : 'No' })] })] })] })), !project.hasDevProperties && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "\u26A0 No dev properties found. Run setup-dev-properties to configure." }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Project Root: " }), _jsx(Text, { dimColor: true, children: project.root })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: "Press Enter or ESC to return to menu" }) })] }));
|
|
12
14
|
}
|
package/dist/utils/branding.d.ts
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const LOGO: string[];
|
|
7
7
|
export declare const AUTHOR = "Rasmus S\u00F6derstr\u00F6m";
|
|
8
|
+
/**
|
|
9
|
+
* Big block-shadow "Sitevision CLI" wordmark, used by the animated startup
|
|
10
|
+
* intro (see components/AnimatedLogo). It's ~120 columns wide, so callers
|
|
11
|
+
* should only render it when the terminal is at least that wide — otherwise it
|
|
12
|
+
* wraps and looks broken.
|
|
13
|
+
*/
|
|
14
|
+
export declare const BIG_LOGO: string[];
|
|
15
|
+
/** Display width of the widest BIG_LOGO line. */
|
|
16
|
+
export declare const BIG_LOGO_WIDTH: number;
|
|
8
17
|
/**
|
|
9
18
|
* Print the logo + author line straight to stdout (non-interactive), mirroring
|
|
10
19
|
* how the masthead is printed. Used for the update banner.
|
package/dist/utils/branding.js
CHANGED
|
@@ -17,6 +17,24 @@ export const LOGO = [
|
|
|
17
17
|
' └──────────┘ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄',
|
|
18
18
|
];
|
|
19
19
|
export const AUTHOR = 'Rasmus Söderström';
|
|
20
|
+
/**
|
|
21
|
+
* Big block-shadow "Sitevision CLI" wordmark, used by the animated startup
|
|
22
|
+
* intro (see components/AnimatedLogo). It's ~120 columns wide, so callers
|
|
23
|
+
* should only render it when the terminal is at least that wide — otherwise it
|
|
24
|
+
* wraps and looks broken.
|
|
25
|
+
*/
|
|
26
|
+
export const BIG_LOGO = [
|
|
27
|
+
' █████████ ███ █████ ███ ███ █████████ █████ █████',
|
|
28
|
+
' ███░░░░░███ ░░░ ░░███ ░░░ ░░░ ███░░░░░███░░███ ░░███ ',
|
|
29
|
+
'░███ ░░░ ████ ███████ ██████ █████ █████ ████ █████ ████ ██████ ████████ ███ ░░░ ░███ ░███ ',
|
|
30
|
+
'░░█████████ ░░███ ░░░███░ ███░░███░░███ ░░███ ░░███ ███░░ ░░███ ███░░███░░███░░███ ░███ ░███ ░███ ',
|
|
31
|
+
' ░░░░░░░░███ ░███ ░███ ░███████ ░███ ░███ ░███ ░░█████ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ',
|
|
32
|
+
' ███ ░███ ░███ ░███ ███░███░░░ ░░███ ███ ░███ ░░░░███ ░███ ░███ ░███ ░███ ░███ ░░███ ███ ░███ █ ░███ ',
|
|
33
|
+
'░░█████████ █████ ░░█████ ░░██████ ░░█████ █████ ██████ █████░░██████ ████ █████ ░░█████████ ███████████ █████',
|
|
34
|
+
' ░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░░ ░░░░░ ',
|
|
35
|
+
];
|
|
36
|
+
/** Display width of the widest BIG_LOGO line. */
|
|
37
|
+
export const BIG_LOGO_WIDTH = Math.max(...BIG_LOGO.map(line => line.length));
|
|
20
38
|
/**
|
|
21
39
|
* Print the logo + author line straight to stdout (non-interactive), mirroring
|
|
22
40
|
* how the masthead is printed. Used for the update banner.
|
|
@@ -20,11 +20,3 @@ export declare class ProcessRunner extends EventEmitter {
|
|
|
20
20
|
kill(): void;
|
|
21
21
|
getOutput(): ProcessOutput[];
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Run a sitevision-scripts command in the project directory
|
|
25
|
-
*/
|
|
26
|
-
export declare function runSitevisionScript(scriptName: string, args?: string[], projectRoot?: string): ProcessRunner;
|
|
27
|
-
/**
|
|
28
|
-
* Run an NPM script
|
|
29
|
-
*/
|
|
30
|
-
export declare function runNpmScript(scriptName: string, args?: string[], projectRoot?: string, customEnv?: Record<string, string>): ProcessRunner;
|
|
@@ -72,18 +72,3 @@ export class ProcessRunner extends EventEmitter {
|
|
|
72
72
|
return this.output;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Run a sitevision-scripts command in the project directory
|
|
77
|
-
*/
|
|
78
|
-
export function runSitevisionScript(scriptName, args = [], projectRoot) {
|
|
79
|
-
// Check if sitevision-scripts is available locally
|
|
80
|
-
const runner = new ProcessRunner('npm', ['run', scriptName, '--', ...args], projectRoot);
|
|
81
|
-
return runner;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Run an NPM script
|
|
85
|
-
*/
|
|
86
|
-
export function runNpmScript(scriptName, args = [], projectRoot, customEnv) {
|
|
87
|
-
const runner = new ProcessRunner('npm', ['run', scriptName, ...args], projectRoot, false, customEnv);
|
|
88
|
-
return runner;
|
|
89
|
-
}
|
|
@@ -13,6 +13,38 @@ import type { SigningCredentials, DeployConfig, ProductionDeployConfig, SigningR
|
|
|
13
13
|
* Create Basic Auth header value
|
|
14
14
|
*/
|
|
15
15
|
declare function createBasicAuth(username: string, password: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Make an HTTP/HTTPS request
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeRequest(url: string, options: {
|
|
20
|
+
method: string;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
body?: Buffer;
|
|
23
|
+
auth?: {
|
|
24
|
+
username: string;
|
|
25
|
+
password: string;
|
|
26
|
+
};
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
statusCode: number;
|
|
30
|
+
body: Buffer;
|
|
31
|
+
headers: Record<string, string>;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Whether an HTTP status is worth retrying (transient server-side failures).
|
|
35
|
+
*/
|
|
36
|
+
export declare function isRetryableStatus(statusCode: number): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Summarize a non-success response body for error messages.
|
|
39
|
+
* Avoids dumping raw bytes (e.g. an HTML error page or a binary blob) by
|
|
40
|
+
* trimming text bodies and labelling binary ones by their content type.
|
|
41
|
+
*/
|
|
42
|
+
export declare function summarizeErrorBody(body: Buffer, headers: Record<string, string>): string;
|
|
43
|
+
/**
|
|
44
|
+
* Check that a buffer begins with the ZIP magic bytes. Used to fail fast when
|
|
45
|
+
* the signing endpoint returns an error page with HTTP 200.
|
|
46
|
+
*/
|
|
47
|
+
export declare function looksLikeZip(body: Buffer): boolean;
|
|
16
48
|
/**
|
|
17
49
|
* Sign an app via developer.sitevision.se
|
|
18
50
|
*
|