@bastani/atomic 0.5.3-0 → 0.5.3-1
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/package.json
CHANGED
|
@@ -44,107 +44,7 @@ import {
|
|
|
44
44
|
applyManagedOnboardingFiles,
|
|
45
45
|
hasProjectOnboardingFiles,
|
|
46
46
|
} from "./onboarding.ts";
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
const ATOMIC_BLOCK_LOGO = [
|
|
50
|
-
"█▀▀█ ▀▀█▀▀ █▀▀█ █▀▄▀█ ▀█▀ █▀▀",
|
|
51
|
-
"█▄▄█ █ █ █ █ ▀ █ █ █ ",
|
|
52
|
-
"▀ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀",
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Catppuccin-inspired gradient (dark terminal)
|
|
56
|
-
const GRADIENT_DARK = [
|
|
57
|
-
"#f5e0dc", "#f2cdcd", "#f5c2e7", "#cba6f7",
|
|
58
|
-
"#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5",
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
// Catppuccin-inspired gradient (light terminal)
|
|
62
|
-
const GRADIENT_LIGHT = [
|
|
63
|
-
"#dc8a78", "#dd7878", "#ea76cb", "#8839ef",
|
|
64
|
-
"#7287fd", "#1e66f5", "#209fb5", "#04a5e5", "#179299",
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
// 256-color approximation of the gradient
|
|
68
|
-
const GRADIENT_256 = [224, 218, 219, 183, 147, 111, 117, 159, 115];
|
|
69
|
-
|
|
70
|
-
function hexToRgb(hex: string): [number, number, number] {
|
|
71
|
-
const h = hex.replace("#", "");
|
|
72
|
-
return [
|
|
73
|
-
parseInt(h.substring(0, 2), 16),
|
|
74
|
-
parseInt(h.substring(2, 4), 16),
|
|
75
|
-
parseInt(h.substring(4, 6), 16),
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function interpolateHex(gradient: string[], t: number): [number, number, number] {
|
|
80
|
-
const pos = Math.max(0, Math.min(1, t)) * (gradient.length - 1);
|
|
81
|
-
const lo = Math.floor(pos);
|
|
82
|
-
const hi = Math.min(lo + 1, gradient.length - 1);
|
|
83
|
-
const frac = pos - lo;
|
|
84
|
-
const [r1, g1, b1] = hexToRgb(gradient[lo]!);
|
|
85
|
-
const [r2, g2, b2] = hexToRgb(gradient[hi]!);
|
|
86
|
-
return [
|
|
87
|
-
Math.round(r1 + (r2 - r1) * frac),
|
|
88
|
-
Math.round(g1 + (g2 - g1) * frac),
|
|
89
|
-
Math.round(b1 + (b2 - b1) * frac),
|
|
90
|
-
];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function interpolate256(gradient: number[], t: number): number {
|
|
94
|
-
const pos = Math.max(0, Math.min(1, t)) * (gradient.length - 1);
|
|
95
|
-
const lo = Math.floor(pos);
|
|
96
|
-
return gradient[lo]!;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function colorizeLineTrueColor(line: string, gradient: string[]): string {
|
|
100
|
-
let out = "";
|
|
101
|
-
const len = line.length;
|
|
102
|
-
for (let i = 0; i < len; i++) {
|
|
103
|
-
const ch = line[i]!;
|
|
104
|
-
if (ch === " ") {
|
|
105
|
-
out += ch;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const [r, g, b] = interpolateHex(gradient, len > 1 ? i / (len - 1) : 0);
|
|
109
|
-
out += `\x1b[38;2;${r};${g};${b}m${ch}`;
|
|
110
|
-
}
|
|
111
|
-
return out + "\x1b[0m";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function colorizeLine256(line: string, gradient: number[]): string {
|
|
115
|
-
let out = "";
|
|
116
|
-
const len = line.length;
|
|
117
|
-
for (let i = 0; i < len; i++) {
|
|
118
|
-
const ch = line[i]!;
|
|
119
|
-
if (ch === " ") {
|
|
120
|
-
out += ch;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
const code = interpolate256(gradient, len > 1 ? i / (len - 1) : 0);
|
|
124
|
-
out += `\x1b[38;5;${code}m${ch}`;
|
|
125
|
-
}
|
|
126
|
-
return out + "\x1b[0m";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function displayBlockBanner(): void {
|
|
130
|
-
const isDark = !(process.env.COLORFGBG ?? "").startsWith("0;");
|
|
131
|
-
const truecolor = supportsTrueColor();
|
|
132
|
-
const color256 = supports256Color();
|
|
133
|
-
const hasColor = supportsColor();
|
|
134
|
-
|
|
135
|
-
console.log();
|
|
136
|
-
for (const line of ATOMIC_BLOCK_LOGO) {
|
|
137
|
-
if (truecolor) {
|
|
138
|
-
const gradient = isDark ? GRADIENT_DARK : GRADIENT_LIGHT;
|
|
139
|
-
console.log(` ${colorizeLineTrueColor(line, gradient)}`);
|
|
140
|
-
} else if (color256 && hasColor) {
|
|
141
|
-
console.log(` ${colorizeLine256(line, GRADIENT_256)}`);
|
|
142
|
-
} else {
|
|
143
|
-
console.log(` ${line}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
console.log();
|
|
147
|
-
}
|
|
47
|
+
import { displayBlockBanner } from "@/theme/logo.ts";
|
|
148
48
|
|
|
149
49
|
/**
|
|
150
50
|
* Thrown when the user cancels an interactive prompt during init.
|
|
@@ -238,26 +238,20 @@ function createPainter(): Paint {
|
|
|
238
238
|
* Layout:
|
|
239
239
|
* N workflows
|
|
240
240
|
*
|
|
241
|
-
*
|
|
242
241
|
* local (.atomic/workflows)
|
|
243
242
|
*
|
|
244
243
|
* Claude
|
|
245
|
-
*
|
|
246
244
|
* <name>
|
|
247
245
|
* <name>
|
|
248
246
|
*
|
|
249
247
|
* OpenCode
|
|
250
|
-
*
|
|
251
248
|
* <name>
|
|
252
249
|
*
|
|
253
|
-
*
|
|
254
250
|
* global (~/.atomic/workflows)
|
|
255
251
|
*
|
|
256
252
|
* Claude
|
|
257
|
-
*
|
|
258
253
|
* <name>
|
|
259
254
|
*
|
|
260
|
-
*
|
|
261
255
|
* run: atomic workflow -n <name> -a <agent>
|
|
262
256
|
*/
|
|
263
257
|
function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
@@ -309,15 +303,13 @@ function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
|
309
303
|
|
|
310
304
|
// One stanza per source section, with nested provider sub-groups inside.
|
|
311
305
|
// Rhythm:
|
|
312
|
-
//
|
|
313
|
-
// 1 blank before each provider heading (
|
|
314
|
-
// 1 blank before each provider's entries
|
|
306
|
+
// 1 blank before each source heading (section break)
|
|
307
|
+
// 1 blank before each provider heading (grouped with its entries)
|
|
315
308
|
for (const source of SOURCE_ORDER) {
|
|
316
309
|
const byAgent = bySource.get(source);
|
|
317
310
|
if (!byAgent || byAgent.size === 0) continue;
|
|
318
311
|
|
|
319
|
-
//
|
|
320
|
-
lines.push("");
|
|
312
|
+
// Section break before the source section.
|
|
321
313
|
lines.push("");
|
|
322
314
|
|
|
323
315
|
// Source heading: bold semantic colour + dim inline directory hint.
|
|
@@ -338,7 +330,6 @@ function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
|
338
330
|
lines.push(
|
|
339
331
|
" " + paint("accent", AGENT_DISPLAY_NAMES[agent], { bold: true }),
|
|
340
332
|
);
|
|
341
|
-
lines.push("");
|
|
342
333
|
|
|
343
334
|
for (const name of names) {
|
|
344
335
|
lines.push(" " + paint("text", name));
|
|
@@ -346,8 +337,7 @@ function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
|
346
337
|
}
|
|
347
338
|
}
|
|
348
339
|
|
|
349
|
-
// Footer — dim run hint, separated by
|
|
350
|
-
lines.push("");
|
|
340
|
+
// Footer — dim run hint, separated by a section break.
|
|
351
341
|
lines.push("");
|
|
352
342
|
lines.push(
|
|
353
343
|
" " + paint("dim", "run: atomic workflow -n <name> -a <agent>"),
|
|
@@ -455,7 +455,7 @@ async function initProviderClientAndSession(
|
|
|
455
455
|
const { CopilotClient, approveAll } = await import("@github/copilot-sdk");
|
|
456
456
|
const copilotClientOpts = clientOpts as StageClientOptions<"copilot">;
|
|
457
457
|
const copilotSessionOpts = sessionOpts as StageSessionOptions<"copilot">;
|
|
458
|
-
const client = new CopilotClient({ cliUrl: serverUrl
|
|
458
|
+
const client = new CopilotClient({ ...copilotClientOpts, cliUrl: serverUrl });
|
|
459
459
|
await client.start();
|
|
460
460
|
const session = await client.createSession({
|
|
461
461
|
onPermissionRequest: approveAll,
|
|
@@ -468,7 +468,7 @@ async function initProviderClientAndSession(
|
|
|
468
468
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
469
469
|
const ocClientOpts = clientOpts as StageClientOptions<"opencode">;
|
|
470
470
|
const ocSessionOpts = sessionOpts as StageSessionOptions<"opencode">;
|
|
471
|
-
const client = createOpencodeClient({ baseUrl: serverUrl
|
|
471
|
+
const client = createOpencodeClient({ ...ocClientOpts, baseUrl: serverUrl });
|
|
472
472
|
const sessionResult = await client.session.create(ocSessionOpts);
|
|
473
473
|
await client.tui.selectSession({ sessionID: sessionResult.data!.id });
|
|
474
474
|
return { client, session: sessionResult.data! };
|
|
@@ -10,21 +10,25 @@
|
|
|
10
10
|
* Instead, we detect a fresh install or upgrade lazily on CLI startup by
|
|
11
11
|
* comparing the bundled `VERSION` constant against a marker file at
|
|
12
12
|
* `~/.atomic/.synced-version`. On a mismatch we run the same setup the
|
|
13
|
-
* production bootstrap installers (`install.sh` / `install.ps1`) provide
|
|
13
|
+
* production bootstrap installers (`install.sh` / `install.ps1`) provide,
|
|
14
|
+
* grouped into two parallel phases:
|
|
14
15
|
*
|
|
15
|
-
* 1
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* 5. global agent configs (~/.claude/agents, ~/.opencode/agents, ~/.copilot/agents, lsp-config.json)
|
|
20
|
-
* 6. global workflows (~/.atomic/workflows/{hello,hello-parallel,ralph,...})
|
|
21
|
-
* 7. global skills (npx skills add ...)
|
|
16
|
+
* Phase 1 (parallel — no inter-dependencies):
|
|
17
|
+
* 1. Node.js / npm (installed via fnm, no system pkg-mgr)
|
|
18
|
+
* 2. tmux / psmux (terminal multiplexer for `chat` / `workflow`)
|
|
19
|
+
* 3. global agent configs (file copies — no network)
|
|
22
20
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* Phase 2 (parallel — all need npm from Phase 1):
|
|
22
|
+
* 4. @playwright/cli (npm install -g)
|
|
23
|
+
* 5. @llamaindex/liteparse (npm install -g)
|
|
24
|
+
* 6. global skills (npx skills add ...)
|
|
25
|
+
*
|
|
26
|
+
* Steps within each phase run concurrently; phases run sequentially.
|
|
27
|
+
* Failures are collected and reported as a summary at the end, but never
|
|
28
|
+
* abort the run — partial setup matches the production installer's
|
|
29
|
+
* "best-effort" semantics. The marker is written after every run (success
|
|
30
|
+
* or partial) so we don't re-attempt the whole setup on every subsequent
|
|
31
|
+
* launch.
|
|
28
32
|
*/
|
|
29
33
|
|
|
30
34
|
import { join } from "path";
|
|
@@ -40,6 +44,7 @@ import {
|
|
|
40
44
|
import { installGlobalAgents } from "@/services/system/agents.ts";
|
|
41
45
|
import { installGlobalSkills } from "@/services/system/skills.ts";
|
|
42
46
|
import { runSteps, printSummary } from "@/services/system/install-ui.ts";
|
|
47
|
+
import { displayBlockBanner } from "@/theme/logo.ts";
|
|
43
48
|
|
|
44
49
|
/** Path to the version marker. Honors ATOMIC_SETTINGS_HOME for tests. */
|
|
45
50
|
function syncMarkerPath(): string {
|
|
@@ -88,26 +93,34 @@ export async function autoSyncIfStale(): Promise<void> {
|
|
|
88
93
|
`\n ${COLORS.dim}Setting up atomic ${COLORS.reset}${COLORS.bold}v${VERSION}${COLORS.reset}${COLORS.dim}…${COLORS.reset}\n`,
|
|
89
94
|
);
|
|
90
95
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
96
|
+
// Steps are split into two parallel phases:
|
|
97
|
+
//
|
|
98
|
+
// Phase 1 — core tools + file copies (no inter-dependencies):
|
|
99
|
+
// npm is installed via fnm (not a system package manager), so it
|
|
100
|
+
// won't contend with tmux's apt-get/dnf install. Agent config
|
|
101
|
+
// copies are pure file I/O with no network or npm dependency.
|
|
102
|
+
//
|
|
103
|
+
// Phase 2 — npm-dependent tasks (run after Phase 1):
|
|
104
|
+
// @playwright/cli, @llamaindex/liteparse, and `npx skills` all
|
|
105
|
+
// need npm/npx. They install independent packages, so they can
|
|
106
|
+
// run concurrently.
|
|
100
107
|
//
|
|
101
108
|
// Each step's failure is caught inside `runSteps` (not thrown), so
|
|
102
109
|
// subsequent steps still run even if one fails — matches install.sh's
|
|
103
110
|
// best-effort contract.
|
|
104
111
|
const results = await runSteps([
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
// Phase 1 — parallel
|
|
113
|
+
[
|
|
114
|
+
{ label: "Node.js / npm", fn: () => ensureNpmInstalled({ quiet: true }) },
|
|
115
|
+
{ label: "tmux / psmux", fn: () => ensureTmuxInstalled({ quiet: true }) },
|
|
116
|
+
{ label: "global agent configs", fn: installGlobalAgents },
|
|
117
|
+
],
|
|
118
|
+
// Phase 2 — parallel, after Phase 1
|
|
119
|
+
[
|
|
120
|
+
{ label: "@playwright/cli", fn: upgradePlaywrightCli },
|
|
121
|
+
{ label: "@llamaindex/liteparse", fn: upgradeLiteparse },
|
|
122
|
+
{ label: "global skills", fn: installGlobalSkills },
|
|
123
|
+
],
|
|
111
124
|
]);
|
|
112
125
|
|
|
113
126
|
// Always write the marker — partial setup is the production-installer
|
|
@@ -115,7 +128,7 @@ export async function autoSyncIfStale(): Promise<void> {
|
|
|
115
128
|
// @bastani/atomic` is the recovery path for a failed setup.
|
|
116
129
|
await markSynced();
|
|
117
130
|
|
|
118
|
-
|
|
131
|
+
displayBlockBanner();
|
|
119
132
|
printSummary(results);
|
|
120
133
|
|
|
121
134
|
const failures = results.filter((r) => !r.ok);
|
|
@@ -124,6 +137,8 @@ export async function autoSyncIfStale(): Promise<void> {
|
|
|
124
137
|
`\n ${COLORS.dim}Re-run \`bun install -g @bastani/atomic\` after resolving the issues to retry.${COLORS.reset}\n`,
|
|
125
138
|
);
|
|
126
139
|
} else {
|
|
127
|
-
console.log(
|
|
140
|
+
console.log(
|
|
141
|
+
`\n ${COLORS.dim}Learn more at ${COLORS.reset}${COLORS.blue}https://deepwiki.com/flora131/atomic${COLORS.reset}\n`,
|
|
142
|
+
);
|
|
128
143
|
}
|
|
129
144
|
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress UI primitives for the first-run install flow (auto-sync).
|
|
3
3
|
*
|
|
4
|
-
* Renders
|
|
4
|
+
* Renders an OpenCode-inspired single-line progress bar:
|
|
5
5
|
*
|
|
6
|
-
* ⠋
|
|
6
|
+
* ⠋ ■■■■■■■■■■■■■■■■■■・・・・・・・・・・・・ 50% tmux / psmux
|
|
7
|
+
*
|
|
8
|
+
* where the braille spinner is provided by `@clack/prompts` and the bar
|
|
9
|
+
* uses Catppuccin Mocha accent colors (Blue for progress, Green for
|
|
10
|
+
* success, Red for error) with true-color → 256-color → basic ANSI
|
|
11
|
+
* fallback.
|
|
12
|
+
*
|
|
13
|
+
* Steps are grouped into **phases**. Steps within a phase run in parallel
|
|
14
|
+
* (via `Promise.all`); phases themselves run sequentially so later phases
|
|
15
|
+
* can depend on earlier ones (e.g. npm must be available before
|
|
16
|
+
* `npm install -g` tasks). The progress bar advances and the label
|
|
17
|
+
* updates in real-time as individual steps complete within a phase.
|
|
7
18
|
*
|
|
8
|
-
* where the braille spinner animation is provided by `@clack/prompts`
|
|
9
|
-
* and the bracketed bar tracks overall step progress (completed / total).
|
|
10
19
|
* A final summary (✓/✗ per step) is printed after all steps finish, and
|
|
11
20
|
* any captured stderr/stdout from a failed step is shown beneath it.
|
|
12
21
|
*
|
|
@@ -16,16 +25,20 @@
|
|
|
16
25
|
|
|
17
26
|
import { spinner } from "@clack/prompts";
|
|
18
27
|
import { COLORS } from "@/theme/colors.ts";
|
|
28
|
+
import {
|
|
29
|
+
supportsTrueColor,
|
|
30
|
+
supports256Color,
|
|
31
|
+
} from "@/services/system/detect.ts";
|
|
19
32
|
|
|
20
|
-
const BAR_WIDTH =
|
|
21
|
-
const BAR_FILLED = "
|
|
22
|
-
const BAR_EMPTY = "
|
|
33
|
+
const BAR_WIDTH = 30;
|
|
34
|
+
const BAR_FILLED = "■";
|
|
35
|
+
const BAR_EMPTY = "・";
|
|
23
36
|
|
|
24
37
|
/**
|
|
25
|
-
* Semantic bar states:
|
|
26
|
-
* progress →
|
|
27
|
-
* success →
|
|
28
|
-
* error →
|
|
38
|
+
* Semantic bar states mapped to Catppuccin Mocha colors:
|
|
39
|
+
* progress → Blue #89b4fa (accent; "in flight")
|
|
40
|
+
* success → Green #a6e3a1 (universal "completed")
|
|
41
|
+
* error → Red #f38ba8 (universal "failed")
|
|
29
42
|
*
|
|
30
43
|
* The empty track stays dim regardless — only the filled portion carries
|
|
31
44
|
* the status signal, which keeps the bar legible while still telegraphing
|
|
@@ -34,6 +47,28 @@ const BAR_EMPTY = "░";
|
|
|
34
47
|
type BarState = "progress" | "success" | "error";
|
|
35
48
|
|
|
36
49
|
function fillColor(state: BarState): string {
|
|
50
|
+
if (supportsTrueColor()) {
|
|
51
|
+
switch (state) {
|
|
52
|
+
case "success":
|
|
53
|
+
return "\x1b[38;2;166;227;161m"; // Catppuccin Green #a6e3a1
|
|
54
|
+
case "error":
|
|
55
|
+
return "\x1b[38;2;243;139;168m"; // Catppuccin Red #f38ba8
|
|
56
|
+
case "progress":
|
|
57
|
+
default:
|
|
58
|
+
return "\x1b[38;2;137;180;250m"; // Catppuccin Blue #89b4fa
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (supports256Color()) {
|
|
62
|
+
switch (state) {
|
|
63
|
+
case "success":
|
|
64
|
+
return "\x1b[38;5;150m";
|
|
65
|
+
case "error":
|
|
66
|
+
return "\x1b[38;5;211m";
|
|
67
|
+
case "progress":
|
|
68
|
+
default:
|
|
69
|
+
return "\x1b[38;5;111m";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
37
72
|
switch (state) {
|
|
38
73
|
case "success":
|
|
39
74
|
return COLORS.green;
|
|
@@ -45,7 +80,7 @@ function fillColor(state: BarState): string {
|
|
|
45
80
|
}
|
|
46
81
|
}
|
|
47
82
|
|
|
48
|
-
/** Render a
|
|
83
|
+
/** Render a progress bar: colored filled ■ + dim empty ・ */
|
|
49
84
|
function renderBar(
|
|
50
85
|
completed: number,
|
|
51
86
|
total: number,
|
|
@@ -56,7 +91,6 @@ function renderBar(
|
|
|
56
91
|
const filled = Math.round(ratio * BAR_WIDTH);
|
|
57
92
|
const empty = BAR_WIDTH - filled;
|
|
58
93
|
return (
|
|
59
|
-
COLORS.bold +
|
|
60
94
|
fillColor(state) +
|
|
61
95
|
BAR_FILLED.repeat(filled) +
|
|
62
96
|
COLORS.reset +
|
|
@@ -73,8 +107,12 @@ function formatLine(
|
|
|
73
107
|
state: BarState = "progress",
|
|
74
108
|
): string {
|
|
75
109
|
const bar = renderBar(completed, total, state);
|
|
76
|
-
const
|
|
77
|
-
|
|
110
|
+
const safeTotal = Math.max(1, total);
|
|
111
|
+
const pct = Math.round(
|
|
112
|
+
Math.max(0, Math.min(1, completed / safeTotal)) * 100,
|
|
113
|
+
);
|
|
114
|
+
const percent = `${COLORS.dim}${String(pct).padStart(3)}%${COLORS.reset}`;
|
|
115
|
+
return `${bar} ${percent} ${label}`;
|
|
78
116
|
}
|
|
79
117
|
|
|
80
118
|
export interface StepResult {
|
|
@@ -84,48 +122,80 @@ export interface StepResult {
|
|
|
84
122
|
error?: string;
|
|
85
123
|
}
|
|
86
124
|
|
|
125
|
+
export interface Step {
|
|
126
|
+
label: string;
|
|
127
|
+
fn: () => Promise<unknown>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A phase is a group of steps that run in parallel. */
|
|
131
|
+
export type Phase = Step[];
|
|
132
|
+
|
|
87
133
|
/**
|
|
88
|
-
* Runs
|
|
89
|
-
* showing stepped progress.
|
|
90
|
-
*
|
|
134
|
+
* Runs phases of async steps with a single persistent spinner line
|
|
135
|
+
* showing stepped progress. Steps within each phase run in parallel;
|
|
136
|
+
* phases run sequentially so later phases can depend on earlier ones.
|
|
91
137
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
138
|
+
* Each step's failure is collected rather than thrown, mirroring
|
|
139
|
+
* auto-sync's "best-effort" contract.
|
|
140
|
+
*
|
|
141
|
+
* Returns the per-step results in phase/submission order so the caller
|
|
142
|
+
* can render a summary.
|
|
94
143
|
*/
|
|
95
|
-
export async function runSteps(
|
|
96
|
-
|
|
97
|
-
): Promise<StepResult[]> {
|
|
98
|
-
const total = steps.length;
|
|
144
|
+
export async function runSteps(phases: Phase[]): Promise<StepResult[]> {
|
|
145
|
+
const total = phases.reduce((n, phase) => n + phase.length, 0);
|
|
99
146
|
const results: StepResult[] = [];
|
|
100
147
|
const s = spinner();
|
|
148
|
+
let completed = 0;
|
|
101
149
|
|
|
102
150
|
// Start with 0/total so the user sees the bar immediately.
|
|
103
|
-
s.start(formatLine(0, total,
|
|
104
|
-
|
|
105
|
-
for (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
s.start(formatLine(0, total, phases[0]?.[0]?.label ?? ""));
|
|
152
|
+
|
|
153
|
+
for (const phase of phases) {
|
|
154
|
+
// Show all in-flight labels for this phase.
|
|
155
|
+
const inFlight = new Set(phase.map((step) => step.label));
|
|
156
|
+
s.message(formatLine(completed, total, [...inFlight].join(", ")));
|
|
157
|
+
|
|
158
|
+
// Run every step in this phase concurrently.
|
|
159
|
+
const phaseResults = await Promise.all(
|
|
160
|
+
phase.map(async (step): Promise<StepResult> => {
|
|
161
|
+
try {
|
|
162
|
+
await step.fn();
|
|
163
|
+
completed++;
|
|
164
|
+
inFlight.delete(step.label);
|
|
165
|
+
if (inFlight.size > 0) {
|
|
166
|
+
s.message(
|
|
167
|
+
formatLine(completed, total, [...inFlight].join(", ")),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return { label: step.label, ok: true };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const message =
|
|
173
|
+
error instanceof Error ? error.message : String(error);
|
|
174
|
+
completed++;
|
|
175
|
+
inFlight.delete(step.label);
|
|
176
|
+
if (inFlight.size > 0) {
|
|
177
|
+
s.message(
|
|
178
|
+
formatLine(completed, total, [...inFlight].join(", ")),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return { label: step.label, ok: false, error: message };
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
results.push(...phaseResults);
|
|
116
187
|
}
|
|
117
188
|
|
|
118
|
-
// Stop with a filled bar + final label
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// succeeded, red when any step failed. The per-step ✓/✗ rows printed
|
|
122
|
-
// afterwards provide the detailed breakdown.
|
|
189
|
+
// Stop with a filled bar + final label. Bar color flips to the
|
|
190
|
+
// universal status colors: green when every step succeeded, red when
|
|
191
|
+
// any step failed.
|
|
123
192
|
const okCount = results.filter((r) => r.ok).length;
|
|
124
193
|
const allOk = okCount === total;
|
|
194
|
+
const finalState: BarState = allOk ? "success" : "error";
|
|
125
195
|
const finalLabel = allOk
|
|
126
|
-
? `${
|
|
127
|
-
: `${
|
|
128
|
-
s.stop(formatLine(total, total, finalLabel,
|
|
196
|
+
? `${fillColor("success")}Setup complete${COLORS.reset}`
|
|
197
|
+
: `${fillColor("error")}Setup finished with errors${COLORS.reset}`;
|
|
198
|
+
s.stop(formatLine(total, total, finalLabel, finalState));
|
|
129
199
|
|
|
130
200
|
return results;
|
|
131
201
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic ASCII-art logo and Catppuccin gradient colorization.
|
|
3
|
+
*
|
|
4
|
+
* Shared between the init banner and the post-install completion screen.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
supportsTrueColor,
|
|
9
|
+
supports256Color,
|
|
10
|
+
supportsColor,
|
|
11
|
+
} from "@/services/system/detect.ts";
|
|
12
|
+
|
|
13
|
+
export const ATOMIC_BLOCK_LOGO = [
|
|
14
|
+
"█▀▀█ ▀▀█▀▀ █▀▀█ █▀▄▀█ ▀█▀ █▀▀",
|
|
15
|
+
"█▄▄█ █ █ █ █ ▀ █ █ █ ",
|
|
16
|
+
"▀ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** Catppuccin-inspired gradient (dark terminal). */
|
|
20
|
+
export const GRADIENT_DARK = [
|
|
21
|
+
"#f5e0dc", "#f2cdcd", "#f5c2e7", "#cba6f7",
|
|
22
|
+
"#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Catppuccin-inspired gradient (light terminal). */
|
|
26
|
+
export const GRADIENT_LIGHT = [
|
|
27
|
+
"#dc8a78", "#dd7878", "#ea76cb", "#8839ef",
|
|
28
|
+
"#7287fd", "#1e66f5", "#209fb5", "#04a5e5", "#179299",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** 256-color approximation of the gradient. */
|
|
32
|
+
export const GRADIENT_256 = [224, 218, 219, 183, 147, 111, 117, 159, 115];
|
|
33
|
+
|
|
34
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
35
|
+
const h = hex.replace("#", "");
|
|
36
|
+
return [
|
|
37
|
+
parseInt(h.substring(0, 2), 16),
|
|
38
|
+
parseInt(h.substring(2, 4), 16),
|
|
39
|
+
parseInt(h.substring(4, 6), 16),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function interpolateHex(gradient: string[], t: number): [number, number, number] {
|
|
44
|
+
const pos = Math.max(0, Math.min(1, t)) * (gradient.length - 1);
|
|
45
|
+
const lo = Math.floor(pos);
|
|
46
|
+
const hi = Math.min(lo + 1, gradient.length - 1);
|
|
47
|
+
const frac = pos - lo;
|
|
48
|
+
const [r1, g1, b1] = hexToRgb(gradient[lo]!);
|
|
49
|
+
const [r2, g2, b2] = hexToRgb(gradient[hi]!);
|
|
50
|
+
return [
|
|
51
|
+
Math.round(r1 + (r2 - r1) * frac),
|
|
52
|
+
Math.round(g1 + (g2 - g1) * frac),
|
|
53
|
+
Math.round(b1 + (b2 - b1) * frac),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function interpolate256(gradient: number[], t: number): number {
|
|
58
|
+
const pos = Math.max(0, Math.min(1, t)) * (gradient.length - 1);
|
|
59
|
+
const lo = Math.floor(pos);
|
|
60
|
+
return gradient[lo]!;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function colorizeLineTrueColor(line: string, gradient: string[]): string {
|
|
64
|
+
let out = "";
|
|
65
|
+
const len = line.length;
|
|
66
|
+
for (let i = 0; i < len; i++) {
|
|
67
|
+
const ch = line[i]!;
|
|
68
|
+
if (ch === " ") {
|
|
69
|
+
out += ch;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const [r, g, b] = interpolateHex(gradient, len > 1 ? i / (len - 1) : 0);
|
|
73
|
+
out += `\x1b[38;2;${r};${g};${b}m${ch}`;
|
|
74
|
+
}
|
|
75
|
+
return out + "\x1b[0m";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function colorizeLine256(line: string, gradient: number[]): string {
|
|
79
|
+
let out = "";
|
|
80
|
+
const len = line.length;
|
|
81
|
+
for (let i = 0; i < len; i++) {
|
|
82
|
+
const ch = line[i]!;
|
|
83
|
+
if (ch === " ") {
|
|
84
|
+
out += ch;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const code = interpolate256(gradient, len > 1 ? i / (len - 1) : 0);
|
|
88
|
+
out += `\x1b[38;5;${code}m${ch}`;
|
|
89
|
+
}
|
|
90
|
+
return out + "\x1b[0m";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Print the Atomic block logo with Catppuccin gradient colorization. */
|
|
94
|
+
export function displayBlockBanner(): void {
|
|
95
|
+
const isDark = !(process.env.COLORFGBG ?? "").startsWith("0;");
|
|
96
|
+
const truecolor = supportsTrueColor();
|
|
97
|
+
const color256 = supports256Color();
|
|
98
|
+
const hasColor = supportsColor();
|
|
99
|
+
|
|
100
|
+
console.log();
|
|
101
|
+
for (const line of ATOMIC_BLOCK_LOGO) {
|
|
102
|
+
if (truecolor) {
|
|
103
|
+
const gradient = isDark ? GRADIENT_DARK : GRADIENT_LIGHT;
|
|
104
|
+
console.log(` ${colorizeLineTrueColor(line, gradient)}`);
|
|
105
|
+
} else if (color256 && hasColor) {
|
|
106
|
+
console.log(` ${colorizeLine256(line, GRADIENT_256)}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(` ${line}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|