@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.
Files changed (67) hide show
  1. package/.atomic/workflows/hello/claude/index.ts +44 -0
  2. package/.atomic/workflows/hello/copilot/index.ts +58 -0
  3. package/.atomic/workflows/hello/opencode/index.ts +58 -0
  4. package/.atomic/workflows/hello-parallel/claude/index.ts +76 -0
  5. package/.atomic/workflows/hello-parallel/copilot/index.ts +105 -0
  6. package/.atomic/workflows/hello-parallel/opencode/index.ts +115 -0
  7. package/.atomic/workflows/ralph/claude/index.ts +149 -0
  8. package/.atomic/workflows/ralph/copilot/index.ts +162 -0
  9. package/.atomic/workflows/ralph/helpers/git.ts +34 -0
  10. package/.atomic/workflows/ralph/helpers/prompts.ts +538 -0
  11. package/.atomic/workflows/ralph/helpers/review.ts +32 -0
  12. package/.atomic/workflows/ralph/opencode/index.ts +164 -0
  13. package/.atomic/workflows/tsconfig.json +22 -0
  14. package/.claude/agents/code-simplifier.md +52 -0
  15. package/.claude/agents/codebase-analyzer.md +166 -0
  16. package/.claude/agents/codebase-locator.md +122 -0
  17. package/.claude/agents/codebase-online-researcher.md +148 -0
  18. package/.claude/agents/codebase-pattern-finder.md +247 -0
  19. package/.claude/agents/codebase-research-analyzer.md +179 -0
  20. package/.claude/agents/codebase-research-locator.md +145 -0
  21. package/.claude/agents/debugger.md +91 -0
  22. package/.claude/agents/orchestrator.md +19 -0
  23. package/.claude/agents/planner.md +106 -0
  24. package/.claude/agents/reviewer.md +97 -0
  25. package/.claude/agents/worker.md +165 -0
  26. package/.github/agents/code-simplifier.md +52 -0
  27. package/.github/agents/codebase-analyzer.md +166 -0
  28. package/.github/agents/codebase-locator.md +122 -0
  29. package/.github/agents/codebase-online-researcher.md +146 -0
  30. package/.github/agents/codebase-pattern-finder.md +247 -0
  31. package/.github/agents/codebase-research-analyzer.md +179 -0
  32. package/.github/agents/codebase-research-locator.md +145 -0
  33. package/.github/agents/debugger.md +98 -0
  34. package/.github/agents/orchestrator.md +27 -0
  35. package/.github/agents/planner.md +131 -0
  36. package/.github/agents/reviewer.md +94 -0
  37. package/.github/agents/worker.md +237 -0
  38. package/.github/lsp.json +93 -0
  39. package/.opencode/agents/code-simplifier.md +62 -0
  40. package/.opencode/agents/codebase-analyzer.md +171 -0
  41. package/.opencode/agents/codebase-locator.md +127 -0
  42. package/.opencode/agents/codebase-online-researcher.md +152 -0
  43. package/.opencode/agents/codebase-pattern-finder.md +252 -0
  44. package/.opencode/agents/codebase-research-analyzer.md +183 -0
  45. package/.opencode/agents/codebase-research-locator.md +149 -0
  46. package/.opencode/agents/debugger.md +99 -0
  47. package/.opencode/agents/orchestrator.md +27 -0
  48. package/.opencode/agents/planner.md +146 -0
  49. package/.opencode/agents/reviewer.md +102 -0
  50. package/.opencode/agents/worker.md +165 -0
  51. package/README.md +355 -299
  52. package/assets/settings.schema.json +0 -5
  53. package/package.json +7 -2
  54. package/src/cli.ts +16 -8
  55. package/src/commands/cli/workflow.ts +209 -15
  56. package/src/lib/spawn.ts +106 -31
  57. package/src/sdk/runtime/loader.ts +1 -1
  58. package/src/services/config/config-path.ts +1 -1
  59. package/src/services/config/settings.ts +0 -9
  60. package/src/services/system/agents.ts +94 -0
  61. package/src/services/system/auto-sync.ts +131 -0
  62. package/src/services/system/install-ui.ts +158 -0
  63. package/src/services/system/skills.ts +26 -17
  64. package/src/services/system/workflows.ts +105 -0
  65. package/src/theme/colors.ts +2 -0
  66. package/src/commands/cli/update.ts +0 -46
  67. 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-1",
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: true },
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: true },
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: true },
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: true },
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
- 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 });
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
- await runCommand(["choco", "install", "nodejs-lts", "-y", "--no-progress"], { inherit: true });
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
- await runCommand(["scoop", "install", "nodejs-lts"], { inherit: true });
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: true });
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: true });
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: true });
301
- const result = await runCommand([scoop, "install", "psmux"], { inherit: true });
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: true });
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: true });
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
- return;
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: true });
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) return;
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: true });
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,