sitevision-cli 0.5.0-beta.0 → 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 {
|
|
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
|
}
|
|
@@ -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
|
+
}
|
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.
|