@bastani/atomic 0.5.0-1 → 0.5.0-2
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/.atomic/workflows/hello/claude/index.ts +44 -0
- package/.atomic/workflows/hello/copilot/index.ts +58 -0
- package/.atomic/workflows/hello/opencode/index.ts +58 -0
- package/.atomic/workflows/hello-parallel/claude/index.ts +76 -0
- package/.atomic/workflows/hello-parallel/copilot/index.ts +105 -0
- package/.atomic/workflows/hello-parallel/opencode/index.ts +115 -0
- package/.atomic/workflows/ralph/claude/index.ts +149 -0
- package/.atomic/workflows/ralph/copilot/index.ts +162 -0
- package/.atomic/workflows/ralph/helpers/git.ts +34 -0
- package/.atomic/workflows/ralph/helpers/prompts.ts +538 -0
- package/.atomic/workflows/ralph/helpers/review.ts +32 -0
- package/.atomic/workflows/ralph/opencode/index.ts +164 -0
- package/.atomic/workflows/tsconfig.json +22 -0
- package/.claude/agents/code-simplifier.md +52 -0
- package/.claude/agents/codebase-analyzer.md +166 -0
- package/.claude/agents/codebase-locator.md +122 -0
- package/.claude/agents/codebase-online-researcher.md +148 -0
- package/.claude/agents/codebase-pattern-finder.md +247 -0
- package/.claude/agents/codebase-research-analyzer.md +179 -0
- package/.claude/agents/codebase-research-locator.md +145 -0
- package/.claude/agents/debugger.md +91 -0
- package/.claude/agents/orchestrator.md +19 -0
- package/.claude/agents/planner.md +106 -0
- package/.claude/agents/reviewer.md +97 -0
- package/.claude/agents/worker.md +165 -0
- package/.github/agents/code-simplifier.md +52 -0
- package/.github/agents/codebase-analyzer.md +166 -0
- package/.github/agents/codebase-locator.md +122 -0
- package/.github/agents/codebase-online-researcher.md +146 -0
- package/.github/agents/codebase-pattern-finder.md +247 -0
- package/.github/agents/codebase-research-analyzer.md +179 -0
- package/.github/agents/codebase-research-locator.md +145 -0
- package/.github/agents/debugger.md +98 -0
- package/.github/agents/orchestrator.md +27 -0
- package/.github/agents/planner.md +131 -0
- package/.github/agents/reviewer.md +94 -0
- package/.github/agents/worker.md +237 -0
- package/.github/lsp.json +93 -0
- package/.opencode/agents/code-simplifier.md +62 -0
- package/.opencode/agents/codebase-analyzer.md +171 -0
- package/.opencode/agents/codebase-locator.md +127 -0
- package/.opencode/agents/codebase-online-researcher.md +152 -0
- package/.opencode/agents/codebase-pattern-finder.md +252 -0
- package/.opencode/agents/codebase-research-analyzer.md +183 -0
- package/.opencode/agents/codebase-research-locator.md +149 -0
- package/.opencode/agents/debugger.md +99 -0
- package/.opencode/agents/orchestrator.md +27 -0
- package/.opencode/agents/planner.md +146 -0
- package/.opencode/agents/reviewer.md +102 -0
- package/.opencode/agents/worker.md +165 -0
- package/README.md +355 -299
- package/assets/settings.schema.json +0 -5
- package/package.json +7 -2
- package/src/cli.ts +16 -8
- package/src/commands/cli/workflow.ts +209 -15
- package/src/lib/spawn.ts +106 -31
- package/src/sdk/runtime/loader.ts +1 -1
- package/src/services/config/config-path.ts +1 -1
- package/src/services/config/settings.ts +0 -9
- package/src/services/system/agents.ts +94 -0
- package/src/services/system/auto-sync.ts +131 -0
- package/src/services/system/install-ui.ts +158 -0
- package/src/services/system/skills.ts +26 -17
- package/src/services/system/workflows.ts +105 -0
- package/src/theme/colors.ts +2 -0
- package/src/commands/cli/update.ts +0 -46
- package/src/services/system/download.ts +0 -325
|
@@ -22,11 +22,6 @@
|
|
|
22
22
|
"format": "date-time",
|
|
23
23
|
"description": "ISO 8601 timestamp of the last configuration update."
|
|
24
24
|
},
|
|
25
|
-
"prerelease": {
|
|
26
|
-
"type": "boolean",
|
|
27
|
-
"description": "When true, 'atomic update' fetches the latest prerelease instead of the latest stable release. Set automatically by the installer when using the --prerelease flag.",
|
|
28
|
-
"default": false
|
|
29
|
-
},
|
|
30
25
|
"trustedPaths": {
|
|
31
26
|
"type": "array",
|
|
32
27
|
"description": "Globally trusted workspaces that have completed provider onboarding through 'atomic init'.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/atomic",
|
|
3
|
-
"version": "0.5.0-
|
|
3
|
+
"version": "0.5.0-2",
|
|
4
4
|
"description": "Configuration management CLI and SDK for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src",
|
|
34
|
-
"assets/settings.schema.json"
|
|
34
|
+
"assets/settings.schema.json",
|
|
35
|
+
".claude/agents",
|
|
36
|
+
".opencode/agents",
|
|
37
|
+
".github/agents",
|
|
38
|
+
".github/lsp.json",
|
|
39
|
+
".atomic/workflows"
|
|
35
40
|
],
|
|
36
41
|
"scripts": {
|
|
37
42
|
"dev": "bun run src/cli.ts",
|
package/src/cli.ts
CHANGED
|
@@ -147,14 +147,6 @@ Examples:
|
|
|
147
147
|
process.exit(exitCode);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
program
|
|
151
|
-
.command("update")
|
|
152
|
-
.description("Update atomic to the latest version and reinstall skills")
|
|
153
|
-
.action(async () => {
|
|
154
|
-
const { updateCommand } = await import("@/commands/cli/update.ts");
|
|
155
|
-
process.exit(await updateCommand());
|
|
156
|
-
});
|
|
157
|
-
|
|
158
150
|
// Add config command for managing CLI settings
|
|
159
151
|
const configCmd = program
|
|
160
152
|
.command("config")
|
|
@@ -182,6 +174,22 @@ export const program = createProgram();
|
|
|
182
174
|
*/
|
|
183
175
|
async function main(): Promise<void> {
|
|
184
176
|
try {
|
|
177
|
+
// Sync tooling deps and global skills on first launch after install
|
|
178
|
+
// or upgrade. Runs at most once per version bump (gated on a marker
|
|
179
|
+
// file under ~/.atomic). Skipped for `--version` / `--help` so info
|
|
180
|
+
// paths stay instant.
|
|
181
|
+
const argv = process.argv.slice(2);
|
|
182
|
+
const isInfoCommand =
|
|
183
|
+
argv.includes("--version") ||
|
|
184
|
+
argv.includes("-v") ||
|
|
185
|
+
argv.includes("--help") ||
|
|
186
|
+
argv.includes("-h");
|
|
187
|
+
|
|
188
|
+
if (!isInfoCommand) {
|
|
189
|
+
const { autoSyncIfStale } = await import("@/services/system/auto-sync.ts");
|
|
190
|
+
await autoSyncIfStale();
|
|
191
|
+
}
|
|
192
|
+
|
|
185
193
|
// Parse and execute the command
|
|
186
194
|
await program.parseAsync();
|
|
187
195
|
} catch (error) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { AGENT_CONFIG, type AgentKey } from "@/services/config/index.ts";
|
|
10
10
|
import { COLORS } from "@/theme/colors.ts";
|
|
11
|
-
import { isCommandInstalled } from "@/services/system/detect.ts";
|
|
11
|
+
import { isCommandInstalled, supportsColor, supportsTrueColor } from "@/services/system/detect.ts";
|
|
12
12
|
import { ensureTmuxInstalled, ensureBunInstalled } from "../../lib/spawn.ts";
|
|
13
13
|
import {
|
|
14
14
|
isTmuxInstalled,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
WorkflowLoader,
|
|
19
19
|
resetMuxBinaryCache,
|
|
20
20
|
} from "@/sdk/workflows.ts";
|
|
21
|
-
import type { AgentType } from "@/sdk/workflows.ts";
|
|
21
|
+
import type { AgentType, DiscoveredWorkflow } from "@/sdk/workflows.ts";
|
|
22
22
|
|
|
23
23
|
export async function workflowCommand(options: {
|
|
24
24
|
name?: string;
|
|
@@ -29,19 +29,7 @@ export async function workflowCommand(options: {
|
|
|
29
29
|
// List mode
|
|
30
30
|
if (options.list) {
|
|
31
31
|
const workflows = await discoverWorkflows(undefined, options.agent as AgentType | undefined);
|
|
32
|
-
|
|
33
|
-
if (workflows.length === 0) {
|
|
34
|
-
console.log("No workflows found.");
|
|
35
|
-
console.log("Create a workflow in .atomic/workflows/<name>/<agent>/index.ts");
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
console.log("Available workflows:\n");
|
|
40
|
-
for (const wf of workflows) {
|
|
41
|
-
const badge = wf.source === "local" ? "(local)" : "(global)";
|
|
42
|
-
console.log(` ${wf.agent}/${wf.name} ${COLORS.dim}${badge}${COLORS.reset}`);
|
|
43
|
-
console.log(` ${COLORS.dim}${wf.path}${COLORS.reset}`);
|
|
44
|
-
}
|
|
32
|
+
process.stdout.write(renderWorkflowList(workflows));
|
|
45
33
|
return 0;
|
|
46
34
|
}
|
|
47
35
|
|
|
@@ -162,3 +150,209 @@ export async function workflowCommand(options: {
|
|
|
162
150
|
return 1;
|
|
163
151
|
}
|
|
164
152
|
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Workflow list rendering
|
|
156
|
+
//
|
|
157
|
+
// Catppuccin Mocha palette rendered via 24-bit ANSI, with graceful fallback
|
|
158
|
+
// to basic ANSI on legacy terminals and plain text when colour is disabled.
|
|
159
|
+
// Hex values mirror src/sdk/runtime/theme.ts so the CLI output and the TUI
|
|
160
|
+
// speak one visual language.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
type PaletteKey = "text" | "dim" | "accent" | "success" | "mauve";
|
|
164
|
+
|
|
165
|
+
const PALETTE: Record<PaletteKey, readonly [number, number, number]> = {
|
|
166
|
+
text: [205, 214, 244], // #cdd6f4 — primary text
|
|
167
|
+
dim: [127, 132, 156], // #7f849c — secondary text
|
|
168
|
+
accent: [137, 180, 250], // #89b4fa — blue accent
|
|
169
|
+
success: [166, 227, 161], // #a6e3a1 — green (local source)
|
|
170
|
+
mauve: [203, 166, 247], // #cba6f7 — mauve (global source)
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
interface PaintOptions {
|
|
174
|
+
bold?: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
type Paint = (key: PaletteKey, text: string, opts?: PaintOptions) => string;
|
|
178
|
+
|
|
179
|
+
/** Stable agent sort order; keeps output deterministic across runs. */
|
|
180
|
+
const AGENT_ORDER: readonly AgentType[] = ["claude", "opencode", "copilot"];
|
|
181
|
+
/** Display names shown as provider sub-headings; honours proper branding. */
|
|
182
|
+
const AGENT_DISPLAY_NAMES: Record<AgentType, string> = {
|
|
183
|
+
claude: "Claude",
|
|
184
|
+
opencode: "OpenCode",
|
|
185
|
+
copilot: "Copilot CLI",
|
|
186
|
+
};
|
|
187
|
+
/** Local first — project-scoped workflows are the most immediately relevant. */
|
|
188
|
+
const SOURCE_ORDER: readonly DiscoveredWorkflow["source"][] = ["local", "global"];
|
|
189
|
+
/** Friendly directory labels shown inline with each section heading. */
|
|
190
|
+
const SOURCE_DIRS: Record<DiscoveredWorkflow["source"], string> = {
|
|
191
|
+
local: ".atomic/workflows",
|
|
192
|
+
global: "~/.atomic/workflows",
|
|
193
|
+
};
|
|
194
|
+
/** Section heading colour per source — preserves the source-type semantic. */
|
|
195
|
+
const SOURCE_COLORS: Record<DiscoveredWorkflow["source"], PaletteKey> = {
|
|
196
|
+
local: "success",
|
|
197
|
+
global: "mauve",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build a colour-aware painter for the current terminal.
|
|
202
|
+
* Truecolor terminals get the full Catppuccin palette; legacy terminals
|
|
203
|
+
* degrade to basic ANSI; NO_COLOR emits plain text. The optional `bold`
|
|
204
|
+
* flag adds weight contrast — essential for typographic hierarchy in a
|
|
205
|
+
* monospace medium where size and family are fixed.
|
|
206
|
+
*/
|
|
207
|
+
function createPainter(): Paint {
|
|
208
|
+
if (supportsTrueColor()) {
|
|
209
|
+
return (key, text, opts) => {
|
|
210
|
+
const [r, g, b] = PALETTE[key];
|
|
211
|
+
const sgr = opts?.bold
|
|
212
|
+
? `\x1b[1;38;2;${r};${g};${b}m`
|
|
213
|
+
: `\x1b[38;2;${r};${g};${b}m`;
|
|
214
|
+
return `${sgr}${text}\x1b[0m`;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (supportsColor()) {
|
|
218
|
+
const ANSI: Record<PaletteKey, string> = {
|
|
219
|
+
text: "",
|
|
220
|
+
dim: "\x1b[2m",
|
|
221
|
+
accent: "\x1b[34m",
|
|
222
|
+
success: "\x1b[32m",
|
|
223
|
+
mauve: "\x1b[35m",
|
|
224
|
+
};
|
|
225
|
+
return (key, text, opts) => {
|
|
226
|
+
const weight = opts?.bold ? "\x1b[1m" : "";
|
|
227
|
+
return `${weight}${ANSI[key]}${text}\x1b[0m`;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return (_key, text) => text;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Render `atomic workflow --list` output as a printable string.
|
|
235
|
+
*
|
|
236
|
+
* Three-level hierarchy: source → provider → workflow name.
|
|
237
|
+
*
|
|
238
|
+
* Layout:
|
|
239
|
+
* N workflows
|
|
240
|
+
*
|
|
241
|
+
*
|
|
242
|
+
* local (.atomic/workflows)
|
|
243
|
+
*
|
|
244
|
+
* Claude
|
|
245
|
+
*
|
|
246
|
+
* <name>
|
|
247
|
+
* <name>
|
|
248
|
+
*
|
|
249
|
+
* OpenCode
|
|
250
|
+
*
|
|
251
|
+
* <name>
|
|
252
|
+
*
|
|
253
|
+
*
|
|
254
|
+
* global (~/.atomic/workflows)
|
|
255
|
+
*
|
|
256
|
+
* Claude
|
|
257
|
+
*
|
|
258
|
+
* <name>
|
|
259
|
+
*
|
|
260
|
+
*
|
|
261
|
+
* run: atomic workflow -n <name> -a <agent>
|
|
262
|
+
*/
|
|
263
|
+
function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
264
|
+
const paint = createPainter();
|
|
265
|
+
const lines: string[] = [];
|
|
266
|
+
|
|
267
|
+
// Empty state — teach the user where workflows live.
|
|
268
|
+
if (workflows.length === 0) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push(" " + paint("text", "no workflows found", { bold: true }));
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push(" " + paint("dim", "create one at"));
|
|
273
|
+
lines.push(
|
|
274
|
+
" " +
|
|
275
|
+
paint("accent", ".atomic/workflows/<name>/<agent>/index.ts"),
|
|
276
|
+
);
|
|
277
|
+
lines.push("");
|
|
278
|
+
return lines.join("\n") + "\n";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Group by source → agent → sorted names. This gives the renderer O(1)
|
|
282
|
+
// lookups at both nesting levels and keeps the output deterministic.
|
|
283
|
+
type ByAgent = Map<AgentType, string[]>;
|
|
284
|
+
const bySource = new Map<DiscoveredWorkflow["source"], ByAgent>();
|
|
285
|
+
for (const wf of workflows) {
|
|
286
|
+
let byAgent = bySource.get(wf.source);
|
|
287
|
+
if (!byAgent) {
|
|
288
|
+
byAgent = new Map();
|
|
289
|
+
bySource.set(wf.source, byAgent);
|
|
290
|
+
}
|
|
291
|
+
const names = byAgent.get(wf.agent) ?? [];
|
|
292
|
+
names.push(wf.name);
|
|
293
|
+
byAgent.set(wf.agent, names);
|
|
294
|
+
}
|
|
295
|
+
for (const byAgent of bySource.values()) {
|
|
296
|
+
for (const names of byAgent.values()) {
|
|
297
|
+
names.sort((a, b) => a.localeCompare(b));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Top header — data-first: the count is bold (it's the actual info), the
|
|
302
|
+
// noun trails in dim. Handles singular "1 workflow" gracefully.
|
|
303
|
+
const count = workflows.length;
|
|
304
|
+
const noun = count === 1 ? "workflow" : "workflows";
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push(
|
|
307
|
+
" " + paint("text", String(count), { bold: true }) + " " + paint("dim", noun),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// One stanza per source section, with nested provider sub-groups inside.
|
|
311
|
+
// Rhythm:
|
|
312
|
+
// 2 blanks before each source heading (major break)
|
|
313
|
+
// 1 blank before each provider heading (tight, they're nested)
|
|
314
|
+
// 1 blank before each provider's entries
|
|
315
|
+
for (const source of SOURCE_ORDER) {
|
|
316
|
+
const byAgent = bySource.get(source);
|
|
317
|
+
if (!byAgent || byAgent.size === 0) continue;
|
|
318
|
+
|
|
319
|
+
// Major break before the source section.
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push("");
|
|
322
|
+
|
|
323
|
+
// Source heading: bold semantic colour + dim inline directory hint.
|
|
324
|
+
// `local (.atomic/workflows)` — label carries the weight, parens recede.
|
|
325
|
+
lines.push(
|
|
326
|
+
" " +
|
|
327
|
+
paint(SOURCE_COLORS[source], source, { bold: true }) +
|
|
328
|
+
paint("dim", ` (${SOURCE_DIRS[source]})`),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
for (const agent of AGENT_ORDER) {
|
|
332
|
+
const names = byAgent.get(agent);
|
|
333
|
+
if (!names || names.length === 0) continue;
|
|
334
|
+
|
|
335
|
+
// Provider heading: bold accent blue — a clearly different layer from
|
|
336
|
+
// both the semantic source heading above and the neutral entries below.
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push(
|
|
339
|
+
" " + paint("accent", AGENT_DISPLAY_NAMES[agent], { bold: true }),
|
|
340
|
+
);
|
|
341
|
+
lines.push("");
|
|
342
|
+
|
|
343
|
+
for (const name of names) {
|
|
344
|
+
lines.push(" " + paint("text", name));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Footer — dim run hint, separated by the same major-break rhythm.
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push("");
|
|
352
|
+
lines.push(
|
|
353
|
+
" " + paint("dim", "run: atomic workflow -n <name> -a <agent>"),
|
|
354
|
+
);
|
|
355
|
+
lines.push("");
|
|
356
|
+
|
|
357
|
+
return lines.join("\n") + "\n";
|
|
358
|
+
}
|
package/src/lib/spawn.ts
CHANGED
|
@@ -79,6 +79,22 @@ export function getHomeDir(): string | undefined {
|
|
|
79
79
|
return process.env.HOME ?? process.env.USERPROFILE;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Options for the user-facing ensure* installers.
|
|
84
|
+
*
|
|
85
|
+
* `quiet: true` captures subprocess output instead of streaming it to the
|
|
86
|
+
* terminal, so a higher-level spinner UI (see auto-sync's `runSteps`) can
|
|
87
|
+
* own the display. Failures collected in the captured buffer are thrown
|
|
88
|
+
* out of the ensure* function so the spinner can mark the step red and
|
|
89
|
+
* surface the captured tail in its summary.
|
|
90
|
+
*
|
|
91
|
+
* Default (`quiet: false`) preserves the historical inherit-stdout
|
|
92
|
+
* behavior used by the ad-hoc fallbacks in chat.ts / workflow.ts.
|
|
93
|
+
*/
|
|
94
|
+
export interface EnsureOptions {
|
|
95
|
+
quiet?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
/**
|
|
83
99
|
* Ensure npm is installed, attempting to install Node.js via available system
|
|
84
100
|
* package managers when missing.
|
|
@@ -87,7 +103,8 @@ export function getHomeDir(): string | undefined {
|
|
|
87
103
|
*/
|
|
88
104
|
|
|
89
105
|
|
|
90
|
-
async function installNodeViaFnm(): Promise<boolean> {
|
|
106
|
+
async function installNodeViaFnm(quiet: boolean): Promise<boolean> {
|
|
107
|
+
const inherit = !quiet;
|
|
91
108
|
// Install fnm if not present.
|
|
92
109
|
if (!Bun.which("fnm")) {
|
|
93
110
|
let installed = false;
|
|
@@ -95,7 +112,7 @@ async function installNodeViaFnm(): Promise<boolean> {
|
|
|
95
112
|
if (process.platform === "darwin" && Bun.which("brew")) {
|
|
96
113
|
const brew = await runCommand(
|
|
97
114
|
[Bun.which("brew")!, "install", "fnm"],
|
|
98
|
-
{ inherit
|
|
115
|
+
{ inherit },
|
|
99
116
|
);
|
|
100
117
|
installed = brew.success;
|
|
101
118
|
}
|
|
@@ -103,7 +120,7 @@ async function installNodeViaFnm(): Promise<boolean> {
|
|
|
103
120
|
if (!installed && process.platform === "win32" && Bun.which("winget")) {
|
|
104
121
|
const winget = await runCommand(
|
|
105
122
|
[Bun.which("winget")!, "install", "Schniz.fnm"],
|
|
106
|
-
{ inherit
|
|
123
|
+
{ inherit },
|
|
107
124
|
);
|
|
108
125
|
if (winget.success) {
|
|
109
126
|
// Refresh PATH — winget installs to a location on the user PATH.
|
|
@@ -121,7 +138,7 @@ async function installNodeViaFnm(): Promise<boolean> {
|
|
|
121
138
|
|
|
122
139
|
const curl = await runCommand(
|
|
123
140
|
[shell, "-lc", "curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell"],
|
|
124
|
-
{ inherit
|
|
141
|
+
{ inherit },
|
|
125
142
|
);
|
|
126
143
|
if (!curl.success) return false;
|
|
127
144
|
|
|
@@ -140,7 +157,7 @@ async function installNodeViaFnm(): Promise<boolean> {
|
|
|
140
157
|
// Install LTS Node.js via fnm.
|
|
141
158
|
const fnmInstall = await runCommand(
|
|
142
159
|
[fnmPath, "install", "--lts"],
|
|
143
|
-
{ inherit
|
|
160
|
+
{ inherit },
|
|
144
161
|
);
|
|
145
162
|
if (!fnmInstall.success) return false;
|
|
146
163
|
|
|
@@ -172,40 +189,67 @@ async function installNodeViaFnm(): Promise<boolean> {
|
|
|
172
189
|
return !!Bun.which("node");
|
|
173
190
|
}
|
|
174
191
|
|
|
175
|
-
export async function ensureNpmInstalled(): Promise<void> {
|
|
192
|
+
export async function ensureNpmInstalled(options: EnsureOptions = {}): Promise<void> {
|
|
193
|
+
const quiet = options.quiet ?? false;
|
|
194
|
+
const inherit = !quiet;
|
|
195
|
+
|
|
176
196
|
if (Bun.which("npm")) {
|
|
177
197
|
return;
|
|
178
198
|
}
|
|
179
199
|
|
|
200
|
+
// Buffer captured failure output so a thrown error can surface the tail
|
|
201
|
+
// through the spinner summary. Only populated when `quiet` is set.
|
|
202
|
+
let capturedDetails = "";
|
|
203
|
+
const record = (result: SpawnResult) => {
|
|
204
|
+
if (quiet && !result.success && result.details) {
|
|
205
|
+
capturedDetails = result.details;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
180
209
|
// Preferred: install via fnm (no root required, works on all platforms).
|
|
181
|
-
if (await installNodeViaFnm()) {
|
|
210
|
+
if (await installNodeViaFnm(quiet)) {
|
|
182
211
|
return;
|
|
183
212
|
}
|
|
184
213
|
|
|
185
214
|
if (process.platform === "win32") {
|
|
186
215
|
// Fallback: direct Node.js installation via Windows package managers.
|
|
187
216
|
if (Bun.which("winget")) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
217
|
+
record(
|
|
218
|
+
await runCommand(
|
|
219
|
+
[
|
|
220
|
+
"winget",
|
|
221
|
+
"install",
|
|
222
|
+
"--id",
|
|
223
|
+
"OpenJS.NodeJS.LTS",
|
|
224
|
+
"-e",
|
|
225
|
+
"--silent",
|
|
226
|
+
"--accept-source-agreements",
|
|
227
|
+
"--accept-package-agreements",
|
|
228
|
+
],
|
|
229
|
+
{ inherit },
|
|
230
|
+
),
|
|
231
|
+
);
|
|
198
232
|
} else if (Bun.which("choco")) {
|
|
199
|
-
|
|
233
|
+
record(
|
|
234
|
+
await runCommand(
|
|
235
|
+
["choco", "install", "nodejs-lts", "-y", "--no-progress"],
|
|
236
|
+
{ inherit },
|
|
237
|
+
),
|
|
238
|
+
);
|
|
200
239
|
} else if (Bun.which("scoop")) {
|
|
201
|
-
|
|
240
|
+
record(
|
|
241
|
+
await runCommand(["scoop", "install", "nodejs-lts"], { inherit }),
|
|
242
|
+
);
|
|
202
243
|
}
|
|
203
244
|
|
|
204
245
|
const programFiles = process.env.ProgramFiles;
|
|
205
246
|
if (programFiles) {
|
|
206
247
|
prependPath(join(programFiles, "nodejs"));
|
|
207
248
|
}
|
|
208
|
-
return;
|
|
249
|
+
if (Bun.which("npm")) return;
|
|
250
|
+
throw new Error(
|
|
251
|
+
capturedDetails || "Could not install Node.js on Windows (no supported package manager found).",
|
|
252
|
+
);
|
|
209
253
|
}
|
|
210
254
|
|
|
211
255
|
const shell = Bun.which("bash") ?? Bun.which("sh");
|
|
@@ -228,11 +272,15 @@ export async function ensureNpmInstalled(): Promise<void> {
|
|
|
228
272
|
if (Bun.which("npm")) {
|
|
229
273
|
return;
|
|
230
274
|
}
|
|
231
|
-
await runCommand([shell, "-lc", script], { inherit
|
|
275
|
+
record(await runCommand([shell, "-lc", script], { inherit }));
|
|
232
276
|
if (Bun.which("npm")) {
|
|
233
277
|
return;
|
|
234
278
|
}
|
|
235
279
|
}
|
|
280
|
+
|
|
281
|
+
throw new Error(
|
|
282
|
+
capturedDetails || "Could not install Node.js — no supported package manager succeeded.",
|
|
283
|
+
);
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
/**
|
|
@@ -282,56 +330,79 @@ export async function upgradeLiteparse(): Promise<void> {
|
|
|
282
330
|
/**
|
|
283
331
|
* Ensure a terminal multiplexer (tmux on Unix, psmux on Windows) is installed.
|
|
284
332
|
* No-op when already present on PATH.
|
|
333
|
+
*
|
|
334
|
+
* When `quiet: true`, subprocess output is captured instead of inherited
|
|
335
|
+
* so an outer spinner UI owns the display. On failure the captured tail
|
|
336
|
+
* is re-thrown as the error message.
|
|
285
337
|
*/
|
|
286
|
-
export async function ensureTmuxInstalled(): Promise<void> {
|
|
338
|
+
export async function ensureTmuxInstalled(options: EnsureOptions = {}): Promise<void> {
|
|
339
|
+
const quiet = options.quiet ?? false;
|
|
340
|
+
const inherit = !quiet;
|
|
341
|
+
|
|
287
342
|
// Check for any multiplexer binary
|
|
288
343
|
if (Bun.which("tmux") || Bun.which("psmux") || Bun.which("pmux")) return;
|
|
289
344
|
|
|
345
|
+
let capturedDetails = "";
|
|
346
|
+
const record = (result: SpawnResult) => {
|
|
347
|
+
if (quiet && !result.success && result.details) {
|
|
348
|
+
capturedDetails = result.details;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
290
352
|
if (process.platform === "win32") {
|
|
291
353
|
// Windows: install psmux
|
|
292
354
|
const winget = Bun.which("winget");
|
|
293
355
|
if (winget) {
|
|
294
|
-
const result = await runCommand([winget, "install", "psmux", "--accept-source-agreements", "--accept-package-agreements"], { inherit
|
|
356
|
+
const result = await runCommand([winget, "install", "psmux", "--accept-source-agreements", "--accept-package-agreements"], { inherit });
|
|
357
|
+
record(result);
|
|
295
358
|
if (result.success && (Bun.which("psmux") || Bun.which("tmux"))) return;
|
|
296
359
|
}
|
|
297
360
|
|
|
298
361
|
const scoop = Bun.which("scoop");
|
|
299
362
|
if (scoop) {
|
|
300
|
-
await runCommand([scoop, "bucket", "add", "psmux", "https://github.com/psmux/scoop-psmux"], { inherit
|
|
301
|
-
const result = await runCommand([scoop, "install", "psmux"], { inherit
|
|
363
|
+
await runCommand([scoop, "bucket", "add", "psmux", "https://github.com/psmux/scoop-psmux"], { inherit });
|
|
364
|
+
const result = await runCommand([scoop, "install", "psmux"], { inherit });
|
|
365
|
+
record(result);
|
|
302
366
|
if (result.success && (Bun.which("psmux") || Bun.which("tmux"))) return;
|
|
303
367
|
}
|
|
304
368
|
|
|
305
369
|
const choco = Bun.which("choco");
|
|
306
370
|
if (choco) {
|
|
307
|
-
const result = await runCommand([choco, "install", "psmux", "-y", "--no-progress"], { inherit
|
|
371
|
+
const result = await runCommand([choco, "install", "psmux", "-y", "--no-progress"], { inherit });
|
|
372
|
+
record(result);
|
|
308
373
|
if (result.success && (Bun.which("psmux") || Bun.which("tmux"))) return;
|
|
309
374
|
}
|
|
310
375
|
|
|
311
376
|
const cargo = Bun.which("cargo");
|
|
312
377
|
if (cargo) {
|
|
313
|
-
const result = await runCommand([cargo, "install", "psmux"], { inherit
|
|
378
|
+
const result = await runCommand([cargo, "install", "psmux"], { inherit });
|
|
379
|
+
record(result);
|
|
314
380
|
if (result.success) {
|
|
315
381
|
const home = getHomeDir();
|
|
316
382
|
if (home) prependPath(join(home, ".cargo", "bin"));
|
|
317
383
|
if (Bun.which("psmux") || Bun.which("tmux")) return;
|
|
318
384
|
}
|
|
319
385
|
}
|
|
320
|
-
|
|
386
|
+
throw new Error(
|
|
387
|
+
capturedDetails || "Could not install psmux — no supported Windows package manager succeeded.",
|
|
388
|
+
);
|
|
321
389
|
}
|
|
322
390
|
|
|
323
391
|
// Unix / macOS
|
|
324
392
|
if (process.platform === "darwin") {
|
|
325
393
|
const brew = Bun.which("brew");
|
|
326
394
|
if (brew) {
|
|
327
|
-
const result = await runCommand([brew, "install", "tmux"], { inherit
|
|
395
|
+
const result = await runCommand([brew, "install", "tmux"], { inherit });
|
|
396
|
+
record(result);
|
|
328
397
|
if (result.success && Bun.which("tmux")) return;
|
|
329
398
|
}
|
|
330
399
|
}
|
|
331
400
|
|
|
332
401
|
// Linux package managers
|
|
333
402
|
const shell = Bun.which("bash") ?? Bun.which("sh");
|
|
334
|
-
if (!shell)
|
|
403
|
+
if (!shell) {
|
|
404
|
+
throw new Error("Neither bash nor sh is available to install tmux.");
|
|
405
|
+
}
|
|
335
406
|
|
|
336
407
|
const managers: string[] = [
|
|
337
408
|
"command -v apt-get >/dev/null 2>&1 && sudo apt-get update -qq && sudo apt-get install -y tmux",
|
|
@@ -343,9 +414,13 @@ export async function ensureTmuxInstalled(): Promise<void> {
|
|
|
343
414
|
];
|
|
344
415
|
|
|
345
416
|
for (const script of managers) {
|
|
346
|
-
await runCommand([shell, "-lc", script], { inherit
|
|
417
|
+
record(await runCommand([shell, "-lc", script], { inherit }));
|
|
347
418
|
if (Bun.which("tmux")) return;
|
|
348
419
|
}
|
|
420
|
+
|
|
421
|
+
throw new Error(
|
|
422
|
+
capturedDetails || "Could not install tmux — no supported package manager succeeded.",
|
|
423
|
+
);
|
|
349
424
|
}
|
|
350
425
|
|
|
351
426
|
/**
|
|
@@ -25,7 +25,7 @@ import { validateClaudeWorkflow } from "../providers/claude.ts";
|
|
|
25
25
|
// Absolute path to the currently-running atomic CLI's own SDK source tree
|
|
26
26
|
// (i.e. `<install_root>/src/sdk`). Computed from this file's URL so it always
|
|
27
27
|
// points at the actual installed atomic, regardless of how it was launched
|
|
28
|
-
// (dev checkout, global `bun install -g atomic`, `bunx atomic`, etc).
|
|
28
|
+
// (dev checkout, global `bun install -g @bastani/atomic`, `bunx atomic`, etc).
|
|
29
29
|
const ATOMIC_SDK_DIR = Bun.fileURLToPath(new URL("..", import.meta.url));
|
|
30
30
|
|
|
31
31
|
// Directory of this loader file. Used as the parent for `Bun.resolveSync`
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two installation modes:
|
|
5
5
|
* 1. Source/Development: Running from source with `bun run src/cli.ts`
|
|
6
|
-
* 2. npm/bun installed: Installed via `bun add -g atomic`
|
|
6
|
+
* 2. npm/bun installed: Installed via `bun add -g @bastani/atomic`
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from "path";
|
|
@@ -26,7 +26,6 @@ interface AtomicSettings {
|
|
|
26
26
|
scm?: "github" | "sapling";
|
|
27
27
|
version?: number;
|
|
28
28
|
lastUpdated?: string;
|
|
29
|
-
prerelease?: boolean;
|
|
30
29
|
trustedPaths?: TrustedPathEntry[];
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -97,14 +96,6 @@ function normalizeTrustedPaths(entries: TrustedPathEntry[] | undefined): Trusted
|
|
|
97
96
|
return Array.from(deduped.values());
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
/**
|
|
101
|
-
* Get the prerelease channel preference.
|
|
102
|
-
* Only checks global settings (~/.atomic/settings.json) since this is an install-level setting.
|
|
103
|
-
*/
|
|
104
|
-
export function getPrereleasePreference(): boolean {
|
|
105
|
-
return loadSettingsFileSync(globalSettingsPath()).prerelease === true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
99
|
export async function isTrustedWorkspacePath(
|
|
109
100
|
workspacePath: string,
|
|
110
101
|
provider: AgentKey,
|