ansimax 1.0.0 → 1.1.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/LICENSE +201 -21
- package/README.es.md +697 -629
- package/README.md +444 -376
- package/dist/index.d.mts +1256 -336
- package/dist/index.d.ts +1256 -336
- package/dist/index.js +4391 -683
- package/dist/index.mjs +4293 -682
- package/examples/01-cli-installer.ts +96 -0
- package/examples/02-live-dashboard.ts +152 -0
- package/examples/03-pixel-art-game.ts +170 -0
- package/examples/04-interactive-deploy.ts +163 -0
- package/examples/05-tree-visualizations.ts +183 -0
- package/examples/06-everything-together.ts +466 -0
- package/examples/animations.ts +172 -0
- package/examples/demo.js +213 -0
- package/examples/demo.ts +212 -0
- package/examples/loaders.ts +254 -0
- package/examples/showcase.ts +174 -0
- package/examples/trees-basic.ts +99 -0
- package/examples/tsconfig.json +18 -0
- package/examples/tsconfig.vscode.json +25 -0
- package/package.json +20 -8
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// EXAMPLE 1 — CLI installer with hierarchical tasks
|
|
3
|
+
//
|
|
4
|
+
// Demonstrates a realistic project setup flow:
|
|
5
|
+
// - Banner intro with theme gradient
|
|
6
|
+
// - Hierarchical tasks (parent + subtasks with rollup)
|
|
7
|
+
// - Status indicators with custom icons
|
|
8
|
+
// - Box for final summary
|
|
9
|
+
//
|
|
10
|
+
// Run:
|
|
11
|
+
// npx ts-node examples/01-cli-installer.ts
|
|
12
|
+
// ─────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ascii,
|
|
16
|
+
themes,
|
|
17
|
+
loader,
|
|
18
|
+
components,
|
|
19
|
+
configure,
|
|
20
|
+
sleep,
|
|
21
|
+
} from '../dist/index.js';
|
|
22
|
+
|
|
23
|
+
// Configure global defaults
|
|
24
|
+
configure({
|
|
25
|
+
theme: 'dracula',
|
|
26
|
+
animationSpeed: 'fast',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Simulated work
|
|
30
|
+
const work = (ms: number, fail = false) => async (): Promise<string> => {
|
|
31
|
+
await sleep(ms);
|
|
32
|
+
if (fail) throw new Error('Simulated failure');
|
|
33
|
+
return 'ok';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const main = async (): Promise<void> => {
|
|
37
|
+
// ── Intro banner ──────────────────────────────
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(themes.banner('CREATE-APP', { font: 'small' }));
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(components.section(themes.primary('Initializing project'), { width: 60 }));
|
|
42
|
+
console.log();
|
|
43
|
+
|
|
44
|
+
// ── Hierarchical task list ────────────────────
|
|
45
|
+
const results = await loader.tasks([
|
|
46
|
+
{
|
|
47
|
+
text: 'Setup environment',
|
|
48
|
+
fn: work(300),
|
|
49
|
+
subtasks: [
|
|
50
|
+
{ text: 'Detect Node version', fn: work(150) },
|
|
51
|
+
{ text: 'Verify npm registry', fn: work(200) },
|
|
52
|
+
{ text: 'Create project dir', fn: work(100) },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
text: 'Install dependencies',
|
|
57
|
+
fn: work(400),
|
|
58
|
+
subtasks: [
|
|
59
|
+
{ text: 'Resolve package tree', fn: work(250) },
|
|
60
|
+
{ text: 'Download tarballs', fn: work(500) },
|
|
61
|
+
{ text: 'Build native modules', fn: work(300) },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
text: 'Run post-install hooks',
|
|
66
|
+
fn: work(200),
|
|
67
|
+
subtasks: [
|
|
68
|
+
{ text: 'Generate types', fn: work(150) },
|
|
69
|
+
{ text: 'Lint workspace', fn: work(180) },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// ── Final summary box ─────────────────────────
|
|
75
|
+
console.log();
|
|
76
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
77
|
+
const failed = results.length - succeeded;
|
|
78
|
+
|
|
79
|
+
const summary = [
|
|
80
|
+
components.status('success', `${succeeded} steps completed`),
|
|
81
|
+
failed > 0
|
|
82
|
+
? components.status('error', `${failed} steps failed`)
|
|
83
|
+
: components.status('info', 'No errors'),
|
|
84
|
+
'',
|
|
85
|
+
themes.muted('Next steps:'),
|
|
86
|
+
' ' + themes.accent('cd my-app && npm run dev'),
|
|
87
|
+
].join('\n');
|
|
88
|
+
|
|
89
|
+
console.log(components.box(summary, { borderStyle: 'rounded', padding: 1 }));
|
|
90
|
+
console.log();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
main().catch((err) => {
|
|
94
|
+
console.error(themes.error('✗ ' + err.message));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// EXAMPLE 2 — Real-time dashboard with live updates
|
|
3
|
+
//
|
|
4
|
+
// Demonstrates:
|
|
5
|
+
// - frames.live() for sticky bottom-of-screen UI
|
|
6
|
+
// - components.progressBar with gradient
|
|
7
|
+
// - components.table for data display
|
|
8
|
+
// - onResize listener for responsive layout
|
|
9
|
+
// - throttle for rate-limited updates
|
|
10
|
+
// - AbortSignal cleanup
|
|
11
|
+
//
|
|
12
|
+
// Run:
|
|
13
|
+
// npx ts-node examples/02-live-dashboard.ts
|
|
14
|
+
// ─────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
themes,
|
|
18
|
+
components,
|
|
19
|
+
frames,
|
|
20
|
+
onResize,
|
|
21
|
+
throttle,
|
|
22
|
+
termSize,
|
|
23
|
+
sleep,
|
|
24
|
+
cursor,
|
|
25
|
+
screen,
|
|
26
|
+
write,
|
|
27
|
+
} from '../dist/index.js';
|
|
28
|
+
|
|
29
|
+
interface Stat {
|
|
30
|
+
service: string;
|
|
31
|
+
status: 'up' | 'down' | 'pending';
|
|
32
|
+
latency: number;
|
|
33
|
+
load: number; // 0..100
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Simulated metrics that drift over time
|
|
37
|
+
const stats: Stat[] = [
|
|
38
|
+
{ service: 'api-gateway', status: 'up', latency: 14, load: 32 },
|
|
39
|
+
{ service: 'auth-service', status: 'up', latency: 22, load: 58 },
|
|
40
|
+
{ service: 'database', status: 'pending', latency: 0, load: 0 },
|
|
41
|
+
{ service: 'cdn-edge', status: 'up', latency: 8, load: 18 },
|
|
42
|
+
{ service: 'cache-layer', status: 'up', latency: 3, load: 71 },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const drift = (): void => {
|
|
46
|
+
for (const s of stats) {
|
|
47
|
+
if (s.status === 'pending') {
|
|
48
|
+
// Pending services come online randomly
|
|
49
|
+
if (Math.random() < 0.1) {
|
|
50
|
+
s.status = 'up';
|
|
51
|
+
s.latency = 10 + Math.floor(Math.random() * 30);
|
|
52
|
+
}
|
|
53
|
+
} else if (s.status === 'up') {
|
|
54
|
+
// Slight load + latency wander
|
|
55
|
+
s.load = Math.max(0, Math.min(100, s.load + (Math.random() - 0.5) * 8));
|
|
56
|
+
s.latency = Math.max(1, s.latency + (Math.random() - 0.5) * 3);
|
|
57
|
+
// Tiny chance of going down
|
|
58
|
+
if (Math.random() < 0.005) s.status = 'down';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const renderFrame = (): string => {
|
|
64
|
+
const { cols } = termSize();
|
|
65
|
+
const innerWidth = Math.max(40, Math.min(cols - 4, 80));
|
|
66
|
+
|
|
67
|
+
// ── Header with theme gradient ──
|
|
68
|
+
const title = themes.banner('STATUS', { font: 'small', perCharColor: false });
|
|
69
|
+
|
|
70
|
+
// ── Service rows ──
|
|
71
|
+
const rows: string[][] = [
|
|
72
|
+
['Service', 'Status', 'Latency', 'Load'],
|
|
73
|
+
...stats.map((s) => {
|
|
74
|
+
const statusIcon =
|
|
75
|
+
s.status === 'up' ? themes.accent('● up')
|
|
76
|
+
: s.status === 'down' ? themes.error('● down')
|
|
77
|
+
: themes.warning('● pending');
|
|
78
|
+
|
|
79
|
+
const latencyStr = s.status === 'up'
|
|
80
|
+
? (s.latency < 20 ? themes.accent : s.latency < 50 ? themes.warning : themes.error)
|
|
81
|
+
(`${s.latency.toFixed(0)}ms`)
|
|
82
|
+
: themes.muted('—');
|
|
83
|
+
|
|
84
|
+
const loadBar = s.status === 'up'
|
|
85
|
+
? components.progressBar(s.load, {
|
|
86
|
+
width: 20,
|
|
87
|
+
gradient: ['#00ff88', '#fdcb6e', '#ff6b6b'],
|
|
88
|
+
showPercentage: true,
|
|
89
|
+
})
|
|
90
|
+
: themes.muted('—');
|
|
91
|
+
|
|
92
|
+
return [s.service, statusIcon, latencyStr, loadBar];
|
|
93
|
+
}),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const table = components.table(rows, { borderStyle: 'rounded', maxColWidth: innerWidth / 4 });
|
|
97
|
+
|
|
98
|
+
// ── Footer ──
|
|
99
|
+
const upCount = stats.filter((s) => s.status === 'up').length;
|
|
100
|
+
const downCount = stats.filter((s) => s.status === 'down').length;
|
|
101
|
+
const footer = themes.muted(
|
|
102
|
+
`${upCount} up · ${downCount} down · ${stats.length} total · ` +
|
|
103
|
+
`${cols}×${termSize().rows} terminal · Ctrl+C to exit`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return [title, '', table, '', footer].join('\n');
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const main = async (): Promise<void> => {
|
|
110
|
+
// Hide cursor while live UI runs
|
|
111
|
+
write(cursor.hide());
|
|
112
|
+
|
|
113
|
+
// Throttled resize redraw — coalesce rapid resize events into one render
|
|
114
|
+
const onAnyResize = throttle(() => {
|
|
115
|
+
// Frame engine will pick up the new size on its next tick
|
|
116
|
+
}, 100);
|
|
117
|
+
const offResize = onResize(onAnyResize);
|
|
118
|
+
|
|
119
|
+
// Start live engine at 4 fps (smooth but not CPU-hungry)
|
|
120
|
+
const live = frames.live({ fps: 4, autoStart: true });
|
|
121
|
+
|
|
122
|
+
// Drift loop
|
|
123
|
+
const ctrl = new AbortController();
|
|
124
|
+
process.on('SIGINT', () => {
|
|
125
|
+
ctrl.abort();
|
|
126
|
+
live.stop({ clear: false });
|
|
127
|
+
offResize();
|
|
128
|
+
write(cursor.show());
|
|
129
|
+
write('\n');
|
|
130
|
+
console.log(themes.muted('Dashboard stopped.'));
|
|
131
|
+
process.exit(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
while (!ctrl.signal.aborted) {
|
|
136
|
+
drift();
|
|
137
|
+
live.update(renderFrame());
|
|
138
|
+
await sleep(250, { signal: ctrl.signal });
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Aborted — fall through to cleanup
|
|
142
|
+
} finally {
|
|
143
|
+
live.stop({ clear: false });
|
|
144
|
+
offResize();
|
|
145
|
+
write(cursor.show());
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
main().catch((err) => {
|
|
150
|
+
console.error(themes.error('✗ ' + (err as Error).message));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// EXAMPLE 3 — Pixel art game loop
|
|
3
|
+
//
|
|
4
|
+
// Demonstrates:
|
|
5
|
+
// - Canvas with dirty-region rendering for FPS
|
|
6
|
+
// - Sprite drawing with alpha blending
|
|
7
|
+
// - Gradient backgrounds with Bayer dithering
|
|
8
|
+
// - Frame-rate locked game loop (drift-corrected)
|
|
9
|
+
// - Braille mode for high-resolution detail
|
|
10
|
+
//
|
|
11
|
+
// A bouncing rocket on a sunset gradient with a star field.
|
|
12
|
+
//
|
|
13
|
+
// Run:
|
|
14
|
+
// npx ts-node examples/03-pixel-art-game.ts
|
|
15
|
+
// ─────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createCanvas,
|
|
19
|
+
images,
|
|
20
|
+
themes,
|
|
21
|
+
cursor,
|
|
22
|
+
screen,
|
|
23
|
+
write,
|
|
24
|
+
sleep,
|
|
25
|
+
termSize,
|
|
26
|
+
type RGBA,
|
|
27
|
+
} from '../dist/index.js';
|
|
28
|
+
|
|
29
|
+
const WIDTH = 60;
|
|
30
|
+
const HEIGHT = 30;
|
|
31
|
+
|
|
32
|
+
// ── Build static background once ───────────────
|
|
33
|
+
const buildBackground = (): ReturnType<typeof createCanvas> => {
|
|
34
|
+
const canvas = createCanvas(WIDTH, HEIGHT);
|
|
35
|
+
|
|
36
|
+
// Sunset gradient with Bayer dithering for smooth bands
|
|
37
|
+
const gradStr = images.gradientRect({
|
|
38
|
+
width: WIDTH,
|
|
39
|
+
height: HEIGHT,
|
|
40
|
+
colors: ['#1a1a2e', '#16213e', '#fd7272', '#f9ca24'],
|
|
41
|
+
style: 'vertical',
|
|
42
|
+
dither: 'bayer',
|
|
43
|
+
});
|
|
44
|
+
// gradientRect returns rendered text; instead we build pixels manually
|
|
45
|
+
// so we can composite sprites on top.
|
|
46
|
+
for (let y = 0; y < HEIGHT; y++) {
|
|
47
|
+
for (let x = 0; x < WIDTH; x++) {
|
|
48
|
+
// Simple vertical sunset
|
|
49
|
+
const t = y / (HEIGHT - 1);
|
|
50
|
+
const r = Math.round(0x1a + (0xf9 - 0x1a) * t);
|
|
51
|
+
const g = Math.round(0x1a + (0xca - 0x1a) * t);
|
|
52
|
+
const b = Math.round(0x2e + (0x24 - 0x2e) * t);
|
|
53
|
+
canvas.set(x, y, { r, g, b });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Star field — random white dots in upper half
|
|
58
|
+
for (let i = 0; i < 30; i++) {
|
|
59
|
+
const x = Math.floor(Math.random() * WIDTH);
|
|
60
|
+
const y = Math.floor(Math.random() * (HEIGHT / 2));
|
|
61
|
+
const brightness = 150 + Math.floor(Math.random() * 100);
|
|
62
|
+
canvas.set(x, y, { r: brightness, g: brightness, b: brightness });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return canvas;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ── Rocket sprite (5×5) with alpha edges for soft blending ──
|
|
69
|
+
const ROCKET = [
|
|
70
|
+
[null, null, { r: 255, g: 255, b: 255 }, null, null ],
|
|
71
|
+
[null, { r: 255, g: 100, b: 100 }, { r: 255, g: 200, b: 200 }, { r: 255, g: 100, b: 100 }, null ],
|
|
72
|
+
[{ r: 200, g: 50, b: 50, a: 0.7 } as RGBA, { r: 255, g: 100, b: 100 }, { r: 255, g: 150, b: 150 }, { r: 255, g: 100, b: 100 }, { r: 200, g: 50, b: 50, a: 0.7 } as RGBA],
|
|
73
|
+
[null, { r: 255, g: 200, b: 50 }, { r: 255, g: 240, b: 100 }, { r: 255, g: 200, b: 50 }, null ],
|
|
74
|
+
[null, null, { r: 255, g: 150, b: 50, a: 0.6 } as RGBA, null, null ],
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// ── Main loop ──────────────────────────────────
|
|
78
|
+
const main = async (): Promise<void> => {
|
|
79
|
+
const { rows } = termSize();
|
|
80
|
+
if (rows < HEIGHT + 5) {
|
|
81
|
+
console.log(themes.error(`Terminal too small. Needs at least ${HEIGHT + 5} rows.`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
write(screen.clear());
|
|
86
|
+
write(cursor.hide());
|
|
87
|
+
write(cursor.to(1, 1));
|
|
88
|
+
console.log(themes.banner('ROCKET', { font: 'small' }));
|
|
89
|
+
|
|
90
|
+
const bg = buildBackground();
|
|
91
|
+
|
|
92
|
+
let rocketX = WIDTH / 2;
|
|
93
|
+
let rocketY = HEIGHT - 8;
|
|
94
|
+
let vx = 0.4;
|
|
95
|
+
let vy = -0.25;
|
|
96
|
+
|
|
97
|
+
const ctrl = new AbortController();
|
|
98
|
+
process.on('SIGINT', () => {
|
|
99
|
+
ctrl.abort();
|
|
100
|
+
write(cursor.show());
|
|
101
|
+
write(cursor.to(1, HEIGHT + 8));
|
|
102
|
+
console.log(themes.muted('\nGame stopped.'));
|
|
103
|
+
process.exit(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Frame loop — drift-corrected via wall clock
|
|
107
|
+
const FRAME_MS = 60; // ~16 FPS
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
let frame = 0;
|
|
110
|
+
|
|
111
|
+
const FPS_HIST: number[] = [];
|
|
112
|
+
let lastFrameTime = Date.now();
|
|
113
|
+
|
|
114
|
+
while (!ctrl.signal.aborted) {
|
|
115
|
+
// Clone background to a frame canvas (so sprites don't mutate the bg)
|
|
116
|
+
const frameCanvas = createCanvas(WIDTH, HEIGHT);
|
|
117
|
+
for (let y = 0; y < HEIGHT; y++) {
|
|
118
|
+
for (let x = 0; x < WIDTH; x++) {
|
|
119
|
+
frameCanvas.set(x, y, bg.get(x, y));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Update rocket physics
|
|
124
|
+
rocketX += vx;
|
|
125
|
+
rocketY += vy;
|
|
126
|
+
if (rocketX <= 2 || rocketX >= WIDTH - 5) vx = -vx;
|
|
127
|
+
if (rocketY <= 2 || rocketY >= HEIGHT - 5) vy = -vy;
|
|
128
|
+
|
|
129
|
+
// Draw the rocket with alpha blending
|
|
130
|
+
frameCanvas.drawSprite(Math.round(rocketX), Math.round(rocketY), ROCKET);
|
|
131
|
+
|
|
132
|
+
// FPS calc
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const delta = now - lastFrameTime;
|
|
135
|
+
lastFrameTime = now;
|
|
136
|
+
if (delta > 0) {
|
|
137
|
+
FPS_HIST.push(1000 / delta);
|
|
138
|
+
if (FPS_HIST.length > 30) FPS_HIST.shift();
|
|
139
|
+
}
|
|
140
|
+
const avgFps = FPS_HIST.reduce((s, n) => s + n, 0) / Math.max(1, FPS_HIST.length);
|
|
141
|
+
|
|
142
|
+
// Render
|
|
143
|
+
write(cursor.to(1, 7));
|
|
144
|
+
write(frameCanvas.render());
|
|
145
|
+
write(cursor.to(1, HEIGHT + 8));
|
|
146
|
+
write(themes.muted(
|
|
147
|
+
`Frame ${frame.toString().padStart(4)} · ` +
|
|
148
|
+
`${avgFps.toFixed(1)} fps · ` +
|
|
149
|
+
`Ctrl+C to exit`,
|
|
150
|
+
));
|
|
151
|
+
|
|
152
|
+
// Drift-correct sleep
|
|
153
|
+
frame++;
|
|
154
|
+
const targetTime = startTime + frame * FRAME_MS;
|
|
155
|
+
const waitMs = Math.max(0, targetTime - Date.now());
|
|
156
|
+
try {
|
|
157
|
+
await sleep(waitMs, { signal: ctrl.signal });
|
|
158
|
+
} catch {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
write(cursor.show());
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
main().catch((err) => {
|
|
167
|
+
write(cursor.show());
|
|
168
|
+
console.error(themes.error('✗ ' + (err as Error).message));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// EXAMPLE 4 — Interactive prompt with menu + multi-loader
|
|
3
|
+
//
|
|
4
|
+
// Demonstrates:
|
|
5
|
+
// - components.menu (interactive arrow-key navigation)
|
|
6
|
+
// - MENU_CANCELLED handling
|
|
7
|
+
// - loader.multi() — concurrent named operations
|
|
8
|
+
// - configure() side effects + onConfigChange
|
|
9
|
+
// - Theme switching at runtime
|
|
10
|
+
// - createTheme() for isolated theme instances
|
|
11
|
+
//
|
|
12
|
+
// Walks the user through choosing a theme, then runs parallel
|
|
13
|
+
// "deployment" operations under a multi-loader.
|
|
14
|
+
//
|
|
15
|
+
// Run:
|
|
16
|
+
// npx ts-node examples/04-interactive-deploy.ts
|
|
17
|
+
// ─────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
components,
|
|
21
|
+
loader,
|
|
22
|
+
themes,
|
|
23
|
+
createTheme,
|
|
24
|
+
configure,
|
|
25
|
+
onConfigChange,
|
|
26
|
+
sleep,
|
|
27
|
+
MENU_CANCELLED,
|
|
28
|
+
} from '../dist/index.js';
|
|
29
|
+
|
|
30
|
+
const main = async (): Promise<void> => {
|
|
31
|
+
// ── Subscribe to config changes globally ──
|
|
32
|
+
const offChange = onConfigChange((c) => {
|
|
33
|
+
// Each time the theme changes, the global `themes` follows automatically
|
|
34
|
+
// thanks to configure() side effects. We just log it.
|
|
35
|
+
console.log(themes.muted(` [config] theme=${c.theme} colorMode=${c.colorMode}`));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── Step 1: Banner ─────────────────────────────
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(themes.banner('DEPLOY', { font: 'small' }));
|
|
41
|
+
console.log(components.section(themes.primary('Configure deployment'), { width: 60 }));
|
|
42
|
+
console.log();
|
|
43
|
+
|
|
44
|
+
// ── Step 2: Pick a theme via menu ─────────────
|
|
45
|
+
console.log(themes.text('Select a theme for the output:'));
|
|
46
|
+
console.log();
|
|
47
|
+
|
|
48
|
+
const themeChoices = ['dracula', 'nord', 'monokai', 'matrix', 'cyberpunk'];
|
|
49
|
+
const choice = await components.menu(themeChoices, {
|
|
50
|
+
title: ' Available themes',
|
|
51
|
+
pointer: '▸',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (choice === MENU_CANCELLED) {
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(themes.warning('⚠ Cancelled by user'));
|
|
57
|
+
offChange();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const themeName = themeChoices[choice as number]!;
|
|
62
|
+
configure({ theme: themeName });
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(themes.accent(`✓ Theme set to ${themeName}`));
|
|
65
|
+
console.log();
|
|
66
|
+
|
|
67
|
+
// ── Step 3: Pick deployment regions (multi-select) ──
|
|
68
|
+
const regionChoices = ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-south-1', 'sa-east-1'];
|
|
69
|
+
console.log(themes.text('Select target regions (Space to toggle, Enter to confirm):'));
|
|
70
|
+
console.log();
|
|
71
|
+
|
|
72
|
+
const regions = await components.menu(regionChoices, {
|
|
73
|
+
title: ' Regions',
|
|
74
|
+
pointer: '▸',
|
|
75
|
+
multiSelect: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (regions === MENU_CANCELLED) {
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(themes.warning('⚠ Cancelled by user'));
|
|
81
|
+
offChange();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const selectedRegions = (regions as number[]).map((i) => regionChoices[i]!);
|
|
86
|
+
if (selectedRegions.length === 0) {
|
|
87
|
+
console.log(themes.warning('⚠ No regions selected — aborting'));
|
|
88
|
+
offChange();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(components.box(
|
|
94
|
+
themes.primary('Plan:') + '\n' +
|
|
95
|
+
selectedRegions.map((r) => ` • ${r}`).join('\n'),
|
|
96
|
+
{ borderStyle: 'rounded', padding: 1 },
|
|
97
|
+
));
|
|
98
|
+
console.log();
|
|
99
|
+
|
|
100
|
+
// ── Step 4: Multi-loader for parallel deployments ──
|
|
101
|
+
console.log(themes.text('Starting parallel deployments...'));
|
|
102
|
+
console.log();
|
|
103
|
+
|
|
104
|
+
const m = loader.multi();
|
|
105
|
+
const items = selectedRegions.map((region) => {
|
|
106
|
+
const item = m.add(`Deploying to ${region}`, { color: '#48dbfb' });
|
|
107
|
+
return { region, item };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Run each region in parallel
|
|
111
|
+
await Promise.all(items.map(async ({ region, item }) => {
|
|
112
|
+
// Stage 1: build
|
|
113
|
+
item.update(`[${region}] Building image...`);
|
|
114
|
+
await sleep(400 + Math.random() * 600);
|
|
115
|
+
|
|
116
|
+
// Stage 2: upload
|
|
117
|
+
item.update(`[${region}] Uploading...`);
|
|
118
|
+
await sleep(600 + Math.random() * 800);
|
|
119
|
+
|
|
120
|
+
// Stage 3: switch
|
|
121
|
+
item.update(`[${region}] Switching traffic...`);
|
|
122
|
+
await sleep(300 + Math.random() * 400);
|
|
123
|
+
|
|
124
|
+
// Random failure chance for one region
|
|
125
|
+
if (Math.random() < 0.15) {
|
|
126
|
+
item.fail(`[${region}] Failed: health check timeout`);
|
|
127
|
+
} else {
|
|
128
|
+
item.succeed(`[${region}] Live`);
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// Wait for the multi-loader to settle (final renders)
|
|
133
|
+
await sleep(150);
|
|
134
|
+
|
|
135
|
+
// ── Step 5: Multi-tenant theme demo ───────────
|
|
136
|
+
// Create an isolated theme instance for the summary
|
|
137
|
+
// (does not affect global state)
|
|
138
|
+
const summary = createTheme('matrix');
|
|
139
|
+
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(components.section(summary.primary('Deployment summary'), { width: 60 }));
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
144
|
+
console.log(components.timeline(
|
|
145
|
+
selectedRegions.map((r) => ({
|
|
146
|
+
label: `Deployed to ${r}`,
|
|
147
|
+
time: new Date().toLocaleTimeString(),
|
|
148
|
+
done: true,
|
|
149
|
+
})),
|
|
150
|
+
));
|
|
151
|
+
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(summary.muted('Global theme remains: ' + themes.current().name));
|
|
154
|
+
console.log(summary.muted('Isolated summary theme: ' + summary.current().name));
|
|
155
|
+
console.log();
|
|
156
|
+
|
|
157
|
+
offChange();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
main().catch((err) => {
|
|
161
|
+
console.error(themes.error('✗ ' + (err as Error).message));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
});
|