@bastani/atomic 0.5.0-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/LICENSE +24 -0
- package/README.md +956 -0
- package/assets/settings.schema.json +52 -0
- package/package.json +68 -0
- package/src/cli.ts +197 -0
- package/src/commands/cli/chat/client.ts +18 -0
- package/src/commands/cli/chat/index.ts +247 -0
- package/src/commands/cli/chat.ts +8 -0
- package/src/commands/cli/config.ts +55 -0
- package/src/commands/cli/init/index.ts +452 -0
- package/src/commands/cli/init/onboarding.ts +45 -0
- package/src/commands/cli/init/scm.ts +190 -0
- package/src/commands/cli/init.ts +8 -0
- package/src/commands/cli/update.ts +46 -0
- package/src/commands/cli/workflow.ts +164 -0
- package/src/lib/merge.ts +65 -0
- package/src/lib/path-root-guard.ts +38 -0
- package/src/lib/spawn.ts +467 -0
- package/src/scripts/bump-version.ts +94 -0
- package/src/scripts/constants-base.ts +14 -0
- package/src/scripts/constants.ts +34 -0
- package/src/sdk/components/color-utils.ts +20 -0
- package/src/sdk/components/connectors.test.ts +661 -0
- package/src/sdk/components/connectors.ts +156 -0
- package/src/sdk/components/edge.tsx +11 -0
- package/src/sdk/components/error-boundary.tsx +38 -0
- package/src/sdk/components/graph-theme.ts +36 -0
- package/src/sdk/components/header.tsx +60 -0
- package/src/sdk/components/layout.test.ts +924 -0
- package/src/sdk/components/layout.ts +186 -0
- package/src/sdk/components/node-card.tsx +68 -0
- package/src/sdk/components/orchestrator-panel-contexts.ts +26 -0
- package/src/sdk/components/orchestrator-panel-store.test.ts +561 -0
- package/src/sdk/components/orchestrator-panel-store.ts +118 -0
- package/src/sdk/components/orchestrator-panel-types.ts +21 -0
- package/src/sdk/components/orchestrator-panel.tsx +143 -0
- package/src/sdk/components/session-graph-panel.tsx +364 -0
- package/src/sdk/components/status-helpers.ts +32 -0
- package/src/sdk/components/statusline.tsx +63 -0
- package/src/sdk/define-workflow.ts +98 -0
- package/src/sdk/errors.ts +39 -0
- package/src/sdk/index.ts +38 -0
- package/src/sdk/providers/claude.ts +316 -0
- package/src/sdk/providers/copilot.ts +43 -0
- package/src/sdk/providers/opencode.ts +43 -0
- package/src/sdk/runtime/discovery.ts +172 -0
- package/src/sdk/runtime/executor.test.ts +415 -0
- package/src/sdk/runtime/executor.ts +695 -0
- package/src/sdk/runtime/loader.ts +372 -0
- package/src/sdk/runtime/panel.tsx +9 -0
- package/src/sdk/runtime/theme.ts +76 -0
- package/src/sdk/runtime/tmux.ts +542 -0
- package/src/sdk/types.ts +114 -0
- package/src/sdk/workflows.ts +85 -0
- package/src/services/config/atomic-config.ts +124 -0
- package/src/services/config/atomic-global-config.ts +361 -0
- package/src/services/config/config-path.ts +19 -0
- package/src/services/config/definitions.ts +176 -0
- package/src/services/config/index.ts +7 -0
- package/src/services/config/settings-schema.ts +2 -0
- package/src/services/config/settings.ts +149 -0
- package/src/services/system/copy.ts +381 -0
- package/src/services/system/detect.ts +161 -0
- package/src/services/system/download.ts +325 -0
- package/src/services/system/file-lock.ts +289 -0
- package/src/services/system/skills.ts +67 -0
- package/src/theme/colors.ts +25 -0
- package/src/version.ts +7 -0
package/src/lib/spawn.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared spawn utilities for postinstall and lifecycle scripts.
|
|
3
|
+
*
|
|
4
|
+
* Provides a thin async wrapper around Bun.spawn and a PATH-prepend helper,
|
|
5
|
+
* eliminating duplication across postinstall-playwright, postinstall-liteparse, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
export interface SpawnResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
details: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RunCommandOptions {
|
|
16
|
+
/** When true, stdout/stderr are inherited so the user sees live output. */
|
|
17
|
+
inherit?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run a command asynchronously and collect its output.
|
|
22
|
+
* Returns a result object instead of throwing on failure.
|
|
23
|
+
*
|
|
24
|
+
* When `inherit` is true, output streams directly to the terminal so the
|
|
25
|
+
* user can follow installation progress in real time.
|
|
26
|
+
*/
|
|
27
|
+
export async function runCommand(cmd: string[], options?: RunCommandOptions): Promise<SpawnResult> {
|
|
28
|
+
try {
|
|
29
|
+
if (options?.inherit) {
|
|
30
|
+
const proc = Bun.spawn({
|
|
31
|
+
cmd,
|
|
32
|
+
stdout: "inherit",
|
|
33
|
+
stderr: "inherit",
|
|
34
|
+
env: process.env,
|
|
35
|
+
});
|
|
36
|
+
const exitCode = await proc.exited;
|
|
37
|
+
return { success: exitCode === 0, details: "" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const proc = Bun.spawn({
|
|
41
|
+
cmd,
|
|
42
|
+
stdout: "pipe",
|
|
43
|
+
stderr: "pipe",
|
|
44
|
+
env: process.env,
|
|
45
|
+
});
|
|
46
|
+
const [stderr, stdout, exitCode] = await Promise.all([
|
|
47
|
+
new Response(proc.stderr).text(),
|
|
48
|
+
new Response(proc.stdout).text(),
|
|
49
|
+
proc.exited,
|
|
50
|
+
]);
|
|
51
|
+
return {
|
|
52
|
+
success: exitCode === 0,
|
|
53
|
+
details: stderr.trim().length > 0 ? stderr.trim() : stdout.trim(),
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
details: error instanceof Error ? error.message : String(error),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Prepend a directory to the PATH environment variable (if not already present).
|
|
65
|
+
*/
|
|
66
|
+
export function prependPath(directory: string): void {
|
|
67
|
+
const pathDelimiter = process.platform === "win32" ? ";" : ":";
|
|
68
|
+
const currentPath = process.env.PATH ?? "";
|
|
69
|
+
const entries = currentPath.split(pathDelimiter);
|
|
70
|
+
if (!entries.includes(directory)) {
|
|
71
|
+
process.env.PATH = directory + pathDelimiter + currentPath;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the user's home directory from environment variables.
|
|
77
|
+
*/
|
|
78
|
+
export function getHomeDir(): string | undefined {
|
|
79
|
+
return process.env.HOME ?? process.env.USERPROFILE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ensure npm is installed, attempting to install Node.js via available system
|
|
84
|
+
* package managers when missing.
|
|
85
|
+
*
|
|
86
|
+
* No-op when npm is already on PATH.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async function installNodeViaFnm(): Promise<boolean> {
|
|
91
|
+
// Install fnm if not present.
|
|
92
|
+
if (!Bun.which("fnm")) {
|
|
93
|
+
let installed = false;
|
|
94
|
+
// macOS: prefer Homebrew
|
|
95
|
+
if (process.platform === "darwin" && Bun.which("brew")) {
|
|
96
|
+
const brew = await runCommand(
|
|
97
|
+
[Bun.which("brew")!, "install", "fnm"],
|
|
98
|
+
{ inherit: true },
|
|
99
|
+
);
|
|
100
|
+
installed = brew.success;
|
|
101
|
+
}
|
|
102
|
+
// Windows: prefer winget
|
|
103
|
+
if (!installed && process.platform === "win32" && Bun.which("winget")) {
|
|
104
|
+
const winget = await runCommand(
|
|
105
|
+
[Bun.which("winget")!, "install", "Schniz.fnm"],
|
|
106
|
+
{ inherit: true },
|
|
107
|
+
);
|
|
108
|
+
if (winget.success) {
|
|
109
|
+
// Refresh PATH — winget installs to a location on the user PATH.
|
|
110
|
+
const userPath = process.env.LOCALAPPDATA
|
|
111
|
+
? join(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links")
|
|
112
|
+
: null;
|
|
113
|
+
if (userPath) prependPath(userPath);
|
|
114
|
+
}
|
|
115
|
+
installed = winget.success;
|
|
116
|
+
}
|
|
117
|
+
// Linux / fallback: use the curl installer (requires a shell)
|
|
118
|
+
if (!installed) {
|
|
119
|
+
const shell = Bun.which("bash") ?? Bun.which("sh");
|
|
120
|
+
if (!shell) return false;
|
|
121
|
+
|
|
122
|
+
const curl = await runCommand(
|
|
123
|
+
[shell, "-lc", "curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell"],
|
|
124
|
+
{ inherit: true },
|
|
125
|
+
);
|
|
126
|
+
if (!curl.success) return false;
|
|
127
|
+
|
|
128
|
+
// Add fnm to PATH for the current session.
|
|
129
|
+
const home = getHomeDir() ?? "/tmp";
|
|
130
|
+
const fnmDir = process.env.FNM_DIR ?? join(home, ".local", "share", "fnm");
|
|
131
|
+
prependPath(fnmDir);
|
|
132
|
+
// Some systems install to ~/.fnm instead
|
|
133
|
+
prependPath(join(home, ".fnm"));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fnmPath = Bun.which("fnm");
|
|
138
|
+
if (!fnmPath) return false;
|
|
139
|
+
|
|
140
|
+
// Install LTS Node.js via fnm.
|
|
141
|
+
const fnmInstall = await runCommand(
|
|
142
|
+
[fnmPath, "install", "--lts"],
|
|
143
|
+
{ inherit: true },
|
|
144
|
+
);
|
|
145
|
+
if (!fnmInstall.success) return false;
|
|
146
|
+
|
|
147
|
+
// Activate the installed version by adding its bin dir to PATH.
|
|
148
|
+
const envShell = process.platform === "win32" ? "cmd" : "bash";
|
|
149
|
+
const envResult = Bun.spawnSync({
|
|
150
|
+
cmd: [fnmPath, "env", "--shell", envShell],
|
|
151
|
+
stdout: "pipe",
|
|
152
|
+
stderr: "pipe",
|
|
153
|
+
});
|
|
154
|
+
if (envResult.success) {
|
|
155
|
+
const envOutput = envResult.stdout.toString();
|
|
156
|
+
if (process.platform === "win32") {
|
|
157
|
+
// cmd output: SET "PATH=C:\...\fnm_multishells\...;..."
|
|
158
|
+
const pathMatch = envOutput.match(/SET "PATH=([^"]+?)"/i);
|
|
159
|
+
if (pathMatch?.[1]) {
|
|
160
|
+
const firstEntry = pathMatch[1].split(";")[0];
|
|
161
|
+
if (firstEntry) prependPath(firstEntry);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// bash output: export PATH="/.../fnm_multishells/...:..."
|
|
165
|
+
const pathMatch = envOutput.match(/export PATH="([^"]+?):/);
|
|
166
|
+
if (pathMatch?.[1]) {
|
|
167
|
+
prependPath(pathMatch[1]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return !!Bun.which("node");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function ensureNpmInstalled(): Promise<void> {
|
|
176
|
+
if (Bun.which("npm")) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Preferred: install via fnm (no root required, works on all platforms).
|
|
181
|
+
if (await installNodeViaFnm()) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (process.platform === "win32") {
|
|
186
|
+
// Fallback: direct Node.js installation via Windows package managers.
|
|
187
|
+
if (Bun.which("winget")) {
|
|
188
|
+
await runCommand([
|
|
189
|
+
"winget",
|
|
190
|
+
"install",
|
|
191
|
+
"--id",
|
|
192
|
+
"OpenJS.NodeJS.LTS",
|
|
193
|
+
"-e",
|
|
194
|
+
"--silent",
|
|
195
|
+
"--accept-source-agreements",
|
|
196
|
+
"--accept-package-agreements",
|
|
197
|
+
], { inherit: true });
|
|
198
|
+
} else if (Bun.which("choco")) {
|
|
199
|
+
await runCommand(["choco", "install", "nodejs-lts", "-y", "--no-progress"], { inherit: true });
|
|
200
|
+
} else if (Bun.which("scoop")) {
|
|
201
|
+
await runCommand(["scoop", "install", "nodejs-lts"], { inherit: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const programFiles = process.env.ProgramFiles;
|
|
205
|
+
if (programFiles) {
|
|
206
|
+
prependPath(join(programFiles, "nodejs"));
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const shell = Bun.which("bash") ?? Bun.which("sh");
|
|
212
|
+
if (!shell) {
|
|
213
|
+
throw new Error("Neither bash nor sh is available to install Node.js.");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fallback: Homebrew, NodeSource, then system package managers.
|
|
217
|
+
const installers = [
|
|
218
|
+
'if command -v brew >/dev/null 2>&1; then brew install node && brew link --overwrite node 2>/dev/null; fi',
|
|
219
|
+
'if command -v apt-get >/dev/null 2>&1; then SUDO=""; [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1 && SUDO="sudo"; $SUDO apt-get update && $SUDO apt-get install -y nodejs npm; fi',
|
|
220
|
+
'if command -v dnf >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo dnf install -y nodejs npm; elif [ "$(id -u)" -eq 0 ]; then dnf install -y nodejs npm; fi; fi',
|
|
221
|
+
'if command -v yum >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo yum install -y nodejs npm; elif [ "$(id -u)" -eq 0 ]; then yum install -y nodejs npm; fi; fi',
|
|
222
|
+
'if command -v pacman >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo pacman -Sy --noconfirm nodejs npm; elif [ "$(id -u)" -eq 0 ]; then pacman -Sy --noconfirm nodejs npm; fi; fi',
|
|
223
|
+
'if command -v zypper >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo zypper --non-interactive install nodejs npm; elif [ "$(id -u)" -eq 0 ]; then zypper --non-interactive install nodejs npm; fi; fi',
|
|
224
|
+
'if command -v apk >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo apk add --no-cache nodejs npm; elif [ "$(id -u)" -eq 0 ]; then apk add --no-cache nodejs npm; fi; fi',
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const script of installers) {
|
|
228
|
+
if (Bun.which("npm")) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await runCommand([shell, "-lc", script], { inherit: true });
|
|
232
|
+
if (Bun.which("npm")) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Upgrade npm to the latest version.
|
|
240
|
+
* Falls back to installing Node.js/npm if it is not yet present.
|
|
241
|
+
*/
|
|
242
|
+
export async function upgradeNpm(): Promise<void> {
|
|
243
|
+
const npmPath = Bun.which("npm");
|
|
244
|
+
if (!npmPath) {
|
|
245
|
+
await ensureNpmInstalled();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const result = await runCommand([npmPath, "install", "-g", "npm@latest"]);
|
|
249
|
+
if (!result.success) {
|
|
250
|
+
const hint =
|
|
251
|
+
result.details?.includes("EACCES") || result.details?.includes("permission")
|
|
252
|
+
? "\nIf this is a permissions issue, try: sudo npm install -g npm@latest"
|
|
253
|
+
: "";
|
|
254
|
+
throw new Error(`npm self-upgrade failed: ${result.details}${hint}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Upgrade a global npm package to the latest version.
|
|
260
|
+
*/
|
|
261
|
+
export async function upgradeGlobalPackage(pkg: string): Promise<void> {
|
|
262
|
+
const versionedPkg = pkg.includes("@latest") ? pkg : `${pkg}@latest`;
|
|
263
|
+
const npmPath = Bun.which("npm");
|
|
264
|
+
if (npmPath) {
|
|
265
|
+
const result = await runCommand([npmPath, "install", "-g", versionedPkg]);
|
|
266
|
+
if (result.success) return;
|
|
267
|
+
throw new Error(`Failed to upgrade ${pkg}: npm: ${result.details}`);
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`npm is not available to upgrade ${pkg}.`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Upgrade @playwright/cli to the latest version globally. */
|
|
273
|
+
export async function upgradePlaywrightCli(): Promise<void> {
|
|
274
|
+
return upgradeGlobalPackage("@playwright/cli");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Upgrade @llamaindex/liteparse to the latest version globally. */
|
|
278
|
+
export async function upgradeLiteparse(): Promise<void> {
|
|
279
|
+
return upgradeGlobalPackage("@llamaindex/liteparse");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Ensure a terminal multiplexer (tmux on Unix, psmux on Windows) is installed.
|
|
284
|
+
* No-op when already present on PATH.
|
|
285
|
+
*/
|
|
286
|
+
export async function ensureTmuxInstalled(): Promise<void> {
|
|
287
|
+
// Check for any multiplexer binary
|
|
288
|
+
if (Bun.which("tmux") || Bun.which("psmux") || Bun.which("pmux")) return;
|
|
289
|
+
|
|
290
|
+
if (process.platform === "win32") {
|
|
291
|
+
// Windows: install psmux
|
|
292
|
+
const winget = Bun.which("winget");
|
|
293
|
+
if (winget) {
|
|
294
|
+
const result = await runCommand([winget, "install", "psmux", "--accept-source-agreements", "--accept-package-agreements"], { inherit: true });
|
|
295
|
+
if (result.success && (Bun.which("psmux") || Bun.which("tmux"))) return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const scoop = Bun.which("scoop");
|
|
299
|
+
if (scoop) {
|
|
300
|
+
await runCommand([scoop, "bucket", "add", "psmux", "https://github.com/psmux/scoop-psmux"], { inherit: true });
|
|
301
|
+
const result = await runCommand([scoop, "install", "psmux"], { inherit: true });
|
|
302
|
+
if (result.success && (Bun.which("psmux") || Bun.which("tmux"))) return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const choco = Bun.which("choco");
|
|
306
|
+
if (choco) {
|
|
307
|
+
const result = await runCommand([choco, "install", "psmux", "-y", "--no-progress"], { inherit: true });
|
|
308
|
+
if (result.success && (Bun.which("psmux") || Bun.which("tmux"))) return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const cargo = Bun.which("cargo");
|
|
312
|
+
if (cargo) {
|
|
313
|
+
const result = await runCommand([cargo, "install", "psmux"], { inherit: true });
|
|
314
|
+
if (result.success) {
|
|
315
|
+
const home = getHomeDir();
|
|
316
|
+
if (home) prependPath(join(home, ".cargo", "bin"));
|
|
317
|
+
if (Bun.which("psmux") || Bun.which("tmux")) return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Unix / macOS
|
|
324
|
+
if (process.platform === "darwin") {
|
|
325
|
+
const brew = Bun.which("brew");
|
|
326
|
+
if (brew) {
|
|
327
|
+
const result = await runCommand([brew, "install", "tmux"], { inherit: true });
|
|
328
|
+
if (result.success && Bun.which("tmux")) return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Linux package managers
|
|
333
|
+
const shell = Bun.which("bash") ?? Bun.which("sh");
|
|
334
|
+
if (!shell) return;
|
|
335
|
+
|
|
336
|
+
const managers: string[] = [
|
|
337
|
+
"command -v apt-get >/dev/null 2>&1 && sudo apt-get update -qq && sudo apt-get install -y tmux",
|
|
338
|
+
"command -v dnf >/dev/null 2>&1 && sudo dnf install -y tmux",
|
|
339
|
+
"command -v yum >/dev/null 2>&1 && sudo yum install -y tmux",
|
|
340
|
+
"command -v pacman >/dev/null 2>&1 && sudo pacman -Sy --noconfirm tmux",
|
|
341
|
+
"command -v zypper >/dev/null 2>&1 && sudo zypper --non-interactive install tmux",
|
|
342
|
+
"command -v apk >/dev/null 2>&1 && sudo apk add --no-cache tmux",
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
for (const script of managers) {
|
|
346
|
+
await runCommand([shell, "-lc", script], { inherit: true });
|
|
347
|
+
if (Bun.which("tmux")) return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Ensure bun is installed and available on PATH.
|
|
353
|
+
* No-op when already present.
|
|
354
|
+
*/
|
|
355
|
+
export async function ensureBunInstalled(): Promise<void> {
|
|
356
|
+
if (Bun.which("bun")) return;
|
|
357
|
+
|
|
358
|
+
const home = getHomeDir();
|
|
359
|
+
|
|
360
|
+
if (process.platform === "win32") {
|
|
361
|
+
// Windows
|
|
362
|
+
const winget = Bun.which("winget");
|
|
363
|
+
if (winget) {
|
|
364
|
+
const result = await runCommand([winget, "install", "Oven-sh.Bun", "--accept-source-agreements", "--accept-package-agreements"], { inherit: true });
|
|
365
|
+
if (result.success) {
|
|
366
|
+
if (home) prependPath(join(home, ".bun", "bin"));
|
|
367
|
+
if (Bun.which("bun")) return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const scoop = Bun.which("scoop");
|
|
372
|
+
if (scoop) {
|
|
373
|
+
const result = await runCommand([scoop, "install", "bun"], { inherit: true });
|
|
374
|
+
if (result.success && Bun.which("bun")) return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const npmPath = Bun.which("npm");
|
|
378
|
+
if (npmPath) {
|
|
379
|
+
const result = await runCommand([npmPath, "install", "-g", "bun"], { inherit: true });
|
|
380
|
+
if (result.success && Bun.which("bun")) return;
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Unix / macOS
|
|
386
|
+
const shell = Bun.which("bash") ?? Bun.which("sh");
|
|
387
|
+
if (shell) {
|
|
388
|
+
const result = await runCommand(
|
|
389
|
+
[shell, "-lc", "curl -fsSL https://bun.sh/install | bash"],
|
|
390
|
+
{ inherit: true },
|
|
391
|
+
);
|
|
392
|
+
if (result.success) {
|
|
393
|
+
if (home) prependPath(join(home, ".bun", "bin"));
|
|
394
|
+
if (Bun.which("bun")) return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// macOS Homebrew fallback
|
|
399
|
+
if (process.platform === "darwin") {
|
|
400
|
+
const brew = Bun.which("brew");
|
|
401
|
+
if (brew) {
|
|
402
|
+
const result = await runCommand([brew, "install", "oven-sh/bun/bun"], { inherit: true });
|
|
403
|
+
if (result.success && Bun.which("bun")) return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Ensure tmux/psmux is installed. Used as a ToolingStep in the update pipeline.
|
|
410
|
+
* Does not attempt version upgrades — just ensures the tool exists.
|
|
411
|
+
*/
|
|
412
|
+
export async function upgradeTmux(): Promise<void> {
|
|
413
|
+
await ensureTmuxInstalled();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Upgrade bun to the latest version, or install if missing.
|
|
418
|
+
*/
|
|
419
|
+
export async function upgradeBun(): Promise<void> {
|
|
420
|
+
const bunPath = Bun.which("bun");
|
|
421
|
+
if (!bunPath) {
|
|
422
|
+
await ensureBunInstalled();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const result = await runCommand([bunPath, "upgrade"]);
|
|
426
|
+
if (!result.success) {
|
|
427
|
+
throw new Error(`bun upgrade failed: ${result.details}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Shared tooling-setup helpers (used by postinstall and update commands)
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
export class ToolingSetupError extends Error {
|
|
436
|
+
constructor(public readonly failures: string[]) {
|
|
437
|
+
const list = failures.map((f) => ` - ${f}`).join("\n");
|
|
438
|
+
super(
|
|
439
|
+
`Tooling setup failed:\n${list}\n\n` +
|
|
440
|
+
`Re-run \`bun install\` to retry, or install the failed tools manually.`,
|
|
441
|
+
);
|
|
442
|
+
this.name = "ToolingSetupError";
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface ToolingStep {
|
|
447
|
+
label: string;
|
|
448
|
+
fn: () => Promise<unknown>;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function collectFailures(
|
|
452
|
+
steps: ToolingStep[],
|
|
453
|
+
results: PromiseSettledResult<unknown>[],
|
|
454
|
+
): string[] {
|
|
455
|
+
const failures: string[] = [];
|
|
456
|
+
for (let i = 0; i < results.length; i++) {
|
|
457
|
+
const result = results[i];
|
|
458
|
+
if (result && result.status === "rejected") {
|
|
459
|
+
const reason = result.reason instanceof Error
|
|
460
|
+
? result.reason.message
|
|
461
|
+
: String(result.reason);
|
|
462
|
+
const label = steps[i]?.label ?? `step ${i}`;
|
|
463
|
+
failures.push(`${label}: ${reason}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return failures;
|
|
467
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Bumps the package version across all files that need it.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run src/scripts/bump-version.ts <version>
|
|
7
|
+
* bun run src/scripts/bump-version.ts --from-branch
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* bun run src/scripts/bump-version.ts 0.4.46
|
|
11
|
+
* bun run src/scripts/bump-version.ts 0.4.46-0
|
|
12
|
+
* bun run src/scripts/bump-version.ts --from-branch # extracts version from current branch name
|
|
13
|
+
*
|
|
14
|
+
* The --from-branch flag reads the current git branch and extracts the version
|
|
15
|
+
* from branch names matching:
|
|
16
|
+
* release/v0.4.46 → 0.4.46
|
|
17
|
+
* prerelease/v0.4.46-0 → 0.4.46-0
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { $ } from "bun";
|
|
21
|
+
import { resolve } from "path";
|
|
22
|
+
import { VERSION_FILES } from "./constants-base.ts";
|
|
23
|
+
|
|
24
|
+
const ROOT = resolve(import.meta.dir, "../..");
|
|
25
|
+
|
|
26
|
+
function parseVersionFromBranch(branch: string): string {
|
|
27
|
+
const match = branch.match(/^(?:release|prerelease)\/v(.+)$/);
|
|
28
|
+
if (!match) {
|
|
29
|
+
console.error(
|
|
30
|
+
`Error: branch "${branch}" does not match release/v<version> or prerelease/v<version>`
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
return match[1] as string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateVersion(version: string): void {
|
|
38
|
+
// Accept semver with optional prerelease suffix: 0.4.46, 0.4.46-0, 1.0.0-1
|
|
39
|
+
if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
|
|
40
|
+
console.error(
|
|
41
|
+
`Error: "${version}" is not a valid semver version`
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getVersion(): Promise<string> {
|
|
48
|
+
const arg = process.argv[2];
|
|
49
|
+
|
|
50
|
+
if (!arg) {
|
|
51
|
+
console.error(
|
|
52
|
+
"Usage: bun run src/scripts/bump-version.ts <version|--from-branch>"
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (arg === "--from-branch") {
|
|
58
|
+
const branch = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
|
|
59
|
+
return parseVersionFromBranch(branch);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strip leading 'v' if provided
|
|
63
|
+
return arg.replace(/^v/, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function bumpFile(filePath: string, version: string): Promise<void> {
|
|
67
|
+
const fullPath = resolve(ROOT, filePath);
|
|
68
|
+
const content = await Bun.file(fullPath).json();
|
|
69
|
+
const oldVersion = content.version;
|
|
70
|
+
|
|
71
|
+
if (oldVersion === version) {
|
|
72
|
+
console.log(` ${filePath}: already at ${version}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
content.version = version;
|
|
77
|
+
await Bun.write(fullPath, JSON.stringify(content, null, 2) + "\n");
|
|
78
|
+
console.log(` ${filePath}: ${oldVersion} → ${version}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main(): Promise<void> {
|
|
82
|
+
const version = await getVersion();
|
|
83
|
+
validateVersion(version);
|
|
84
|
+
|
|
85
|
+
console.log(`Bumping version to ${version}\n`);
|
|
86
|
+
|
|
87
|
+
for (const file of VERSION_FILES) {
|
|
88
|
+
await bumpFile(file, version);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log("\nDone.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight shared constants for build/release scripts.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally free of heavy dependencies so that
|
|
5
|
+
* scripts like bump-version can run before `bun install` in CI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** npm package name. */
|
|
9
|
+
export const SDK_PACKAGE_NAME = "@bastani/atomic";
|
|
10
|
+
|
|
11
|
+
/** package.json files whose `version` field is bumped together. */
|
|
12
|
+
export const VERSION_FILES = [
|
|
13
|
+
"package.json",
|
|
14
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for build/release scripts.
|
|
3
|
+
*
|
|
4
|
+
* Centralises values that appear across multiple scripts so a single
|
|
5
|
+
* change propagates everywhere.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AGENTS } from "@/sdk/workflows.ts";
|
|
9
|
+
import type { AgentType } from "@/sdk/workflows.ts";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
SDK_PACKAGE_NAME,
|
|
13
|
+
VERSION_FILES,
|
|
14
|
+
} from "./constants-base.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps each agent to its config directory (relative to the repo root).
|
|
18
|
+
*
|
|
19
|
+
* Used by the config archive script and validation steps. The mapping
|
|
20
|
+
* is: claude → .claude, opencode → .opencode, copilot → .github.
|
|
21
|
+
*/
|
|
22
|
+
const AGENT_CONFIG_ROOT: Record<AgentType, string> = {
|
|
23
|
+
claude: ".claude",
|
|
24
|
+
opencode: ".opencode",
|
|
25
|
+
copilot: ".github",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Directories copied recursively into the config archive (agents). */
|
|
29
|
+
export const CONFIG_DIRS = AGENTS.map(
|
|
30
|
+
(agent) => `${AGENT_CONFIG_ROOT[agent]}/agents`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Individual files copied into the config archive. */
|
|
34
|
+
export const CONFIG_FILES = [".github/lsp.json"];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// ─── Color Utilities ──────────────────────────────
|
|
2
|
+
|
|
3
|
+
export function hexToRgb(hex: string): [number, number, number] {
|
|
4
|
+
const n = parseInt(hex.slice(1), 16);
|
|
5
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function rgbToHex(r: number, g: number, b: number): string {
|
|
9
|
+
return "#" + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function lerpColor(a: string, b: string, t: number): string {
|
|
13
|
+
const [ar, ag, ab] = hexToRgb(a);
|
|
14
|
+
const [br, bg, bb] = hexToRgb(b);
|
|
15
|
+
return rgbToHex(
|
|
16
|
+
Math.round(ar + (br - ar) * t),
|
|
17
|
+
Math.round(ag + (bg - ag) * t),
|
|
18
|
+
Math.round(ab + (bb - ab) * t),
|
|
19
|
+
);
|
|
20
|
+
}
|