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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
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 { printBranding } from './utils/branding.js';
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
  }
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { render, Box, Text, useInput } from 'ink';
4
4
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { WebpackRunner } from '../utils/webpack-runner.js';
6
- import { copyStaticToBuild, copySrcToBuild, cleanBuild, formatFileSize, createBuildZip, getZipSize, } from '../utils/zip.js';
5
+ import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
6
+ import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
7
+ import { copyStaticToBuild, copySrcToBuild, cleanBuild, formatFileSize, createBuildZip, getZipSize, zipExists, } from '../utils/zip.js';
7
8
  import { isBundledApp, getAppType, getFullAppId, } from '../utils/project-detection.js';
8
9
  export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }) {
9
10
  const [state, setState] = React.useState({
@@ -25,8 +26,55 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
25
26
  setState({ status: 'cleaning', message: 'Cleaning build directory...' });
26
27
  cleanBuild(projectRoot);
27
28
  // Step 2: Build or copy files
29
+ if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
30
+ // No project-local webpack config: delegate the whole build to
31
+ // @sitevision/sitevision-scripts, which compiles + zips to
32
+ // dist/<appId>.zip (the path the CLI's sign/deploy already use).
33
+ if (!hasSitevisionScripts(projectRoot)) {
34
+ setState({
35
+ status: 'error',
36
+ error: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
37
+ });
38
+ return;
39
+ }
40
+ // Warn if the installed sitevision-scripts is outside the range
41
+ // the delegated build was validated against.
42
+ const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
43
+ setState({
44
+ status: 'building',
45
+ message: 'Building via sitevision-scripts...',
46
+ warning,
47
+ });
48
+ const sitevisionResult = await runSitevisionScriptsBuild(projectRoot);
49
+ if (!sitevisionResult.success) {
50
+ setState({
51
+ status: 'error',
52
+ error: `${sitevisionResult.error}\n${sitevisionResult.output.slice(-1000)}`,
53
+ warning,
54
+ });
55
+ return;
56
+ }
57
+ // sitevision-scripts already produced the zip; report it directly.
58
+ const zipPath = getDelegatedZipPath(projectRoot, manifest.id);
59
+ if (!zipExists(zipPath)) {
60
+ setState({
61
+ status: 'error',
62
+ error: `Build reported success but no zip was found at ${zipPath}.`,
63
+ warning,
64
+ });
65
+ return;
66
+ }
67
+ setState({
68
+ status: 'success',
69
+ message: 'Build complete',
70
+ zipPath,
71
+ zipSize: getZipSize(zipPath),
72
+ warning,
73
+ });
74
+ return;
75
+ }
28
76
  if (isBundled) {
29
- // Check if webpack is available
77
+ // Project ships its own webpack config: build it in-process.
30
78
  if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
31
79
  setState({
32
80
  status: 'error',
@@ -122,7 +170,7 @@ export function BuildScreen({ projectRoot, manifest, createZip = true, onBack, }
122
170
  return 'Build failed';
123
171
  }
124
172
  };
125
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
173
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusIndicator(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.status === 'success' && state.result?.stats && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compiled in ", state.result.stats.time, "ms"] }), state.result.stats.assets &&
126
174
  state.result.stats.assets.length > 0 && (_jsxs(Text, { dimColor: true, children: ["Assets: ", state.result.stats.assets.join(', ')] }))] })), state.status === 'success' && state.zipPath && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 Created: ", state.zipPath] }), state.zipSize !== undefined && (_jsxs(Text, { dimColor: true, children: [" Size: ", formatFileSize(state.zipSize)] }))] })), state.result?.warnings && state.result.warnings.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "Warnings:" }), state.result.warnings.slice(0, 5).map((warning, i) => (_jsx(Text, { color: "yellow", dimColor: true, children: warning.substring(0, 200) }, i))), state.result.warnings.length > 5 && (_jsxs(Text, { color: "yellow", dimColor: true, children: ["...and ", state.result.warnings.length - 5, " more"] }))] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), onBack && (state.status === 'success' || state.status === 'error') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" }) }))] }));
127
175
  }
128
176
  export const buildCommand = {
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import fs from 'fs';
3
+ import path from 'path';
2
4
  import React from 'react';
3
5
  import { render, Box, Text, useApp, useInput } from 'ink';
4
6
  import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { WebpackRunner } from '../utils/webpack-runner.js';
7
+ import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
8
+ import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
6
9
  import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
7
10
  import { signApp, deployApp } from '../utils/sitevision-api.js';
8
11
  import { setDeployPassword } from '../utils/keychain.js';
@@ -26,23 +29,13 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
26
29
  }
27
30
  });
28
31
  const webpackRunnerRef = React.useRef(null);
29
- const handleBuildComplete = React.useCallback(async (result) => {
30
- if (!result.success) {
31
- setState(prev => ({
32
- ...prev,
33
- status: 'error',
34
- message: result.errors?.join('\n') || 'Build failed',
35
- error: result.errors?.join('\n'),
36
- }));
37
- return;
38
- }
32
+ const watchersRef = React.useRef([]);
33
+ const debounceTimerRef = React.useRef(null);
34
+ const isBuildingRef = React.useRef(false);
35
+ const pendingRebuildRef = React.useRef(false);
36
+ // Sign (if needed) and deploy an already-built zip, updating UI state.
37
+ const signAndDeploy = React.useCallback(async (zipPath, buildTime) => {
39
38
  try {
40
- // Copy static files
41
- copyStaticToBuild(projectRoot);
42
- // Create zip
43
- const appId = getFullAppId(manifest.id);
44
- await createBuildZip(projectRoot, appId);
45
- const zipPath = getZipPath(projectRoot, manifest);
46
39
  let deployZipPath = zipPath;
47
40
  // Sign if needed
48
41
  if (signed && signingCredentials) {
@@ -94,7 +87,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
94
87
  status: 'ready',
95
88
  message: 'Deployed. Watching for changes...',
96
89
  buildCount: prev.buildCount + 1,
97
- lastBuildTime: result.stats?.time,
90
+ lastBuildTime: buildTime,
98
91
  error: undefined,
99
92
  }));
100
93
  }
@@ -107,14 +100,122 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
107
100
  }));
108
101
  }
109
102
  }, [projectRoot, manifest, devProperties, signed, signingCredentials]);
103
+ // In-house webpack path: copy static, zip, then sign + deploy.
104
+ const handleBuildComplete = React.useCallback(async (result) => {
105
+ if (!result.success) {
106
+ setState(prev => ({
107
+ ...prev,
108
+ status: 'error',
109
+ message: result.errors?.join('\n') || 'Build failed',
110
+ error: result.errors?.join('\n'),
111
+ }));
112
+ return;
113
+ }
114
+ try {
115
+ copyStaticToBuild(projectRoot);
116
+ const appId = getFullAppId(manifest.id);
117
+ await createBuildZip(projectRoot, appId);
118
+ await signAndDeploy(getZipPath(projectRoot, manifest), result.stats?.time);
119
+ }
120
+ catch (error) {
121
+ setState(prev => ({
122
+ ...prev,
123
+ status: 'error',
124
+ message: error instanceof Error ? error.message : String(error),
125
+ error: error instanceof Error ? error.message : String(error),
126
+ }));
127
+ }
128
+ }, [projectRoot, manifest, signAndDeploy]);
129
+ // Delegated path: full `sitevision-scripts build` then sign + deploy.
130
+ // Coalesces overlapping triggers so a save mid-build queues one rebuild.
131
+ const runDelegatedBuild = React.useCallback(async () => {
132
+ if (isBuildingRef.current) {
133
+ pendingRebuildRef.current = true;
134
+ return;
135
+ }
136
+ isBuildingRef.current = true;
137
+ const buildOnce = async () => {
138
+ pendingRebuildRef.current = false;
139
+ setState(prev => ({
140
+ ...prev,
141
+ status: 'building',
142
+ message: 'Building via sitevision-scripts...',
143
+ }));
144
+ const result = await runSitevisionScriptsBuild(projectRoot);
145
+ if (result.success) {
146
+ await signAndDeploy(getDelegatedZipPath(projectRoot, manifest.id));
147
+ }
148
+ else {
149
+ setState(prev => ({
150
+ ...prev,
151
+ status: 'error',
152
+ message: result.error ?? 'Build failed',
153
+ error: `${result.error}\n${result.output.slice(-1000)}`,
154
+ }));
155
+ }
156
+ if (pendingRebuildRef.current) {
157
+ await buildOnce();
158
+ }
159
+ };
160
+ try {
161
+ await buildOnce();
162
+ }
163
+ finally {
164
+ isBuildingRef.current = false;
165
+ }
166
+ }, [projectRoot, manifest, signAndDeploy]);
167
+ // Watch source files and trigger a delegated rebuild (debounced).
168
+ const startFileWatcher = React.useCallback(() => {
169
+ const targets = [
170
+ 'src',
171
+ 'static',
172
+ 'i18n',
173
+ 'resource',
174
+ 'config',
175
+ 'manifest.json',
176
+ ]
177
+ .map(name => path.join(projectRoot, name))
178
+ .filter(target => fs.existsSync(target));
179
+ for (const target of targets) {
180
+ const isDir = fs.statSync(target).isDirectory();
181
+ const watcher = fs.watch(target, { recursive: isDir }, () => {
182
+ if (debounceTimerRef.current) {
183
+ clearTimeout(debounceTimerRef.current);
184
+ }
185
+ debounceTimerRef.current = setTimeout(() => {
186
+ void runDelegatedBuild();
187
+ }, 300);
188
+ });
189
+ watchersRef.current.push(watcher);
190
+ }
191
+ }, [projectRoot, runDelegatedBuild]);
110
192
  React.useEffect(() => {
111
193
  const isBundled = isBundledApp(manifest);
112
194
  async function startWatch() {
113
195
  try {
114
196
  // Clean build directory
115
197
  cleanBuild(projectRoot);
116
- if (isBundled) {
117
- // Check if webpack is available
198
+ if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
199
+ // No local webpack config: delegate each build to
200
+ // sitevision-scripts (full rebuild) and watch source files
201
+ // ourselves, keeping the CLI's own sign + deploy flow.
202
+ if (!hasSitevisionScripts(projectRoot)) {
203
+ setState({
204
+ status: 'error',
205
+ message: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
206
+ buildCount: 0,
207
+ webpackReady: false,
208
+ error: 'No build pipeline available',
209
+ });
210
+ return;
211
+ }
212
+ const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
213
+ setState(prev => ({ ...prev, webpackReady: true, warning }));
214
+ startFileWatcher();
215
+ await runDelegatedBuild();
216
+ }
217
+ else if (isBundled) {
218
+ // Project ships its own webpack config: incremental in-process watch.
118
219
  if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
119
220
  setState({
120
221
  status: 'error',
@@ -175,14 +276,30 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
175
276
  if (webpackRunnerRef.current) {
176
277
  webpackRunnerRef.current.close().catch(() => { });
177
278
  }
279
+ if (debounceTimerRef.current) {
280
+ clearTimeout(debounceTimerRef.current);
281
+ }
282
+ for (const watcher of watchersRef.current) {
283
+ watcher.close();
284
+ }
285
+ watchersRef.current = [];
178
286
  };
179
- }, [projectRoot, manifest, handleBuildComplete]);
287
+ }, [
288
+ projectRoot,
289
+ manifest,
290
+ handleBuildComplete,
291
+ runDelegatedBuild,
292
+ startFileWatcher,
293
+ ]);
180
294
  // Handle Ctrl+C
181
295
  React.useEffect(() => {
182
296
  const handleExit = () => {
183
297
  if (webpackRunnerRef.current) {
184
298
  webpackRunnerRef.current.close().catch(() => { });
185
299
  }
300
+ for (const watcher of watchersRef.current) {
301
+ watcher.close();
302
+ }
186
303
  exit();
187
304
  };
188
305
  process.on('SIGINT', handleExit);
@@ -220,7 +337,7 @@ export function DevScreen({ projectRoot, manifest, devProperties, signed, signin
220
337
  return 'Error';
221
338
  }
222
339
  };
223
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
340
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
224
341
  ? ` | Last build: ${state.lastBuildTime}ms`
225
342
  : '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [manifest.name, " v", manifest.version] }) }), devProperties && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Target: ", devProperties.domain, "/", devProperties.siteName, "/", devProperties.addonName] }) })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack ? (_jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu (Ctrl+C to stop process)" })) : (_jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop" }))] })] }));
226
343
  }
@@ -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
  }
@@ -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.
@@ -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
  *