@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge-copy bundled Atomic agents from the installed package into the
|
|
3
|
+
* provider-native global roots.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `install_global_agents()` from the production install.sh /
|
|
6
|
+
* install.ps1 bootstrap installers. The bundled agent definitions ship
|
|
7
|
+
* with the npm package (see the `files` array in package.json) at:
|
|
8
|
+
*
|
|
9
|
+
* <pkg-root>/.claude/agents → ~/.claude/agents
|
|
10
|
+
* <pkg-root>/.opencode/agents → ~/.opencode/agents
|
|
11
|
+
* <pkg-root>/.github/agents → ~/.copilot/agents (rename: github → copilot)
|
|
12
|
+
* <pkg-root>/.github/lsp.json → ~/.copilot/lsp-config.json (rename per atomic-global-config.ts)
|
|
13
|
+
*
|
|
14
|
+
* Copy semantics: files sharing a name with a bundled file are overwritten;
|
|
15
|
+
* unrelated user-added files in those directories are preserved (the
|
|
16
|
+
* `copyDir()` helper iterates source entries, so anything not in the
|
|
17
|
+
* source is left untouched).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { join, dirname } from "path";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import {
|
|
23
|
+
copyDir,
|
|
24
|
+
copyFile,
|
|
25
|
+
ensureDir,
|
|
26
|
+
pathExists,
|
|
27
|
+
} from "@/services/system/copy.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Locate the package root by walking up from this module. Both in installed
|
|
31
|
+
* (`<pkg>/src/services/system/agents.ts`) and dev checkout layouts the
|
|
32
|
+
* package root is three directories up.
|
|
33
|
+
*/
|
|
34
|
+
function packageRoot(): string {
|
|
35
|
+
return join(import.meta.dir, "..", "..", "..");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Honors ATOMIC_SETTINGS_HOME so tests can point at a temp dir. */
|
|
39
|
+
function homeRoot(): string {
|
|
40
|
+
return process.env.ATOMIC_SETTINGS_HOME ?? homedir();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface AgentSyncPair {
|
|
44
|
+
/** Source path relative to package root. */
|
|
45
|
+
src: string;
|
|
46
|
+
/** Destination path relative to home root. */
|
|
47
|
+
dest: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const AGENT_DIR_PAIRS: AgentSyncPair[] = [
|
|
51
|
+
{ src: ".claude/agents", dest: ".claude/agents" },
|
|
52
|
+
{ src: ".opencode/agents", dest: ".opencode/agents" },
|
|
53
|
+
{ src: ".github/agents", dest: ".copilot/agents" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sync bundled agents and the copilot lsp-config to the provider global
|
|
58
|
+
* roots. Throws on hard failures (a single sync failing); missing source
|
|
59
|
+
* directories are warned about and skipped, not thrown.
|
|
60
|
+
*/
|
|
61
|
+
export async function installGlobalAgents(): Promise<void> {
|
|
62
|
+
const pkg = packageRoot();
|
|
63
|
+
const home = homeRoot();
|
|
64
|
+
|
|
65
|
+
const warnings: string[] = [];
|
|
66
|
+
for (const pair of AGENT_DIR_PAIRS) {
|
|
67
|
+
const src = join(pkg, pair.src);
|
|
68
|
+
const dest = join(home, pair.dest);
|
|
69
|
+
|
|
70
|
+
if (!(await pathExists(src))) {
|
|
71
|
+
warnings.push(`bundled agents missing at ${src} — skipping ${dest}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await copyDir(src, dest);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Surface skipped sources via a non-fatal thrown error only if ALL sources
|
|
79
|
+
// were missing. A partial miss (e.g. one provider folder absent in a dev
|
|
80
|
+
// checkout) is normal and shouldn't mark the step as failed. The spinner
|
|
81
|
+
// UI shows the thrown message beneath the failing row.
|
|
82
|
+
if (warnings.length === AGENT_DIR_PAIRS.length) {
|
|
83
|
+
throw new Error(warnings.join("\n"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Copilot's lsp.json is renamed to ~/.copilot/lsp-config.json on disk
|
|
87
|
+
// (see atomic-global-config.ts for the in-binary rename rationale).
|
|
88
|
+
const lspSrc = join(pkg, ".github", "lsp.json");
|
|
89
|
+
const lspDest = join(home, ".copilot", "lsp-config.json");
|
|
90
|
+
if (await pathExists(lspSrc)) {
|
|
91
|
+
await ensureDir(dirname(lspDest));
|
|
92
|
+
await copyFile(lspSrc, lspDest);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy first-run sync of tooling deps, bundled agents, and global skills.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: bun's package manager does NOT execute the top-level
|
|
5
|
+
* package's `postinstall` script on `bun add -g` / `bun update -g` — see
|
|
6
|
+
* `src/install/PackageManager/install_with_manager.zig` (the
|
|
7
|
+
* `!manager.options.global` guard around root lifecycle scripts). So
|
|
8
|
+
* there's no install-time hook we can register from `package.json`.
|
|
9
|
+
*
|
|
10
|
+
* Instead, we detect a fresh install or upgrade lazily on CLI startup by
|
|
11
|
+
* comparing the bundled `VERSION` constant against a marker file at
|
|
12
|
+
* `~/.atomic/.synced-version`. On a mismatch we run the same setup the
|
|
13
|
+
* production bootstrap installers (`install.sh` / `install.ps1`) provide:
|
|
14
|
+
*
|
|
15
|
+
* 1. Node.js / npm (gates everything that needs npm/npx)
|
|
16
|
+
* 2. tmux / psmux (terminal multiplexer for `chat` and `workflow`)
|
|
17
|
+
* 3. @playwright/cli (global npm package used by skills)
|
|
18
|
+
* 4. @llamaindex/liteparse (global npm package used by skills)
|
|
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 ...)
|
|
22
|
+
*
|
|
23
|
+
* Each step runs sequentially. Failures are collected and reported as a
|
|
24
|
+
* summary at the end, but never abort the run — partial setup matches the
|
|
25
|
+
* production installer's "best-effort" semantics. The marker is written
|
|
26
|
+
* after every run (success or partial) so we don't re-attempt the whole
|
|
27
|
+
* setup on every subsequent launch.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
import { homedir } from "os";
|
|
32
|
+
import { VERSION } from "@/version.ts";
|
|
33
|
+
import { COLORS } from "@/theme/colors.ts";
|
|
34
|
+
import {
|
|
35
|
+
ensureNpmInstalled,
|
|
36
|
+
ensureTmuxInstalled,
|
|
37
|
+
upgradePlaywrightCli,
|
|
38
|
+
upgradeLiteparse,
|
|
39
|
+
} from "@/lib/spawn.ts";
|
|
40
|
+
import { installGlobalAgents } from "@/services/system/agents.ts";
|
|
41
|
+
import { installGlobalWorkflows } from "@/services/system/workflows.ts";
|
|
42
|
+
import { installGlobalSkills } from "@/services/system/skills.ts";
|
|
43
|
+
import { runSteps, printSummary } from "@/services/system/install-ui.ts";
|
|
44
|
+
|
|
45
|
+
/** Path to the version marker. Honors ATOMIC_SETTINGS_HOME for tests. */
|
|
46
|
+
function syncMarkerPath(): string {
|
|
47
|
+
const home = process.env.ATOMIC_SETTINGS_HOME ?? homedir();
|
|
48
|
+
return join(home, ".atomic", ".synced-version");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when running from an installed package (under `node_modules/`),
|
|
53
|
+
* false on a dev checkout. Avoids triggering a full global setup on every
|
|
54
|
+
* `bun run dev` in the repo.
|
|
55
|
+
*/
|
|
56
|
+
function isInstalledPackage(): boolean {
|
|
57
|
+
return import.meta.dir.includes("node_modules");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write the version marker. Best-effort: a failed write just means the
|
|
62
|
+
* next launch will re-sync, which is wasteful but not broken.
|
|
63
|
+
*/
|
|
64
|
+
export async function markSynced(): Promise<void> {
|
|
65
|
+
try {
|
|
66
|
+
await Bun.write(syncMarkerPath(), VERSION);
|
|
67
|
+
} catch {
|
|
68
|
+
// Swallow — see docstring.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sync tooling deps, bundled agents, and global skills if the marker
|
|
74
|
+
* doesn't match the bundled VERSION. No-op in dev checkouts and when the
|
|
75
|
+
* marker already matches the current version.
|
|
76
|
+
*/
|
|
77
|
+
export async function autoSyncIfStale(): Promise<void> {
|
|
78
|
+
if (!isInstalledPackage()) return;
|
|
79
|
+
|
|
80
|
+
let stored = "";
|
|
81
|
+
const marker = Bun.file(syncMarkerPath());
|
|
82
|
+
if (await marker.exists()) {
|
|
83
|
+
stored = (await marker.text()).trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (stored === VERSION) return;
|
|
87
|
+
|
|
88
|
+
console.log(
|
|
89
|
+
`\n ${COLORS.dim}Setting up atomic ${COLORS.reset}${COLORS.bold}v${VERSION}${COLORS.reset}${COLORS.dim}…${COLORS.reset}\n`,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Ordering notes:
|
|
93
|
+
// - Phase 1 (npm, tmux): core tools. npm comes first because
|
|
94
|
+
// @playwright/cli, @llamaindex/liteparse, and `npx skills` all need it.
|
|
95
|
+
// tmux/psmux is independent but installed sequentially to avoid
|
|
96
|
+
// contention with system package-manager locks (apt-get, dnf, etc).
|
|
97
|
+
// - Phase 2 (global npm packages): will fail loudly if Phase 1's npm
|
|
98
|
+
// install failed, which is fine — the spinner summary records their
|
|
99
|
+
// failure too.
|
|
100
|
+
// - Phase 3 (bundled atomic content): file copies + npx skills install.
|
|
101
|
+
//
|
|
102
|
+
// Each step's failure is caught inside `runSteps` (not thrown), so
|
|
103
|
+
// subsequent steps still run even if one fails — matches install.sh's
|
|
104
|
+
// best-effort contract.
|
|
105
|
+
const results = await runSteps([
|
|
106
|
+
{ label: "Node.js / npm", fn: () => ensureNpmInstalled({ quiet: true }) },
|
|
107
|
+
{ label: "tmux / psmux", fn: () => ensureTmuxInstalled({ quiet: true }) },
|
|
108
|
+
{ label: "@playwright/cli", fn: upgradePlaywrightCli },
|
|
109
|
+
{ label: "@llamaindex/liteparse", fn: upgradeLiteparse },
|
|
110
|
+
{ label: "global agent configs", fn: installGlobalAgents },
|
|
111
|
+
{ label: "global workflows", fn: installGlobalWorkflows },
|
|
112
|
+
{ label: "global skills", fn: installGlobalSkills },
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// Always write the marker — partial setup is the production-installer
|
|
116
|
+
// contract. Re-running the bootstrap installer or `bun update -g
|
|
117
|
+
// @bastani/atomic` is the recovery path for a failed setup.
|
|
118
|
+
await markSynced();
|
|
119
|
+
|
|
120
|
+
console.log();
|
|
121
|
+
printSummary(results);
|
|
122
|
+
|
|
123
|
+
const failures = results.filter((r) => !r.ok);
|
|
124
|
+
if (failures.length > 0) {
|
|
125
|
+
console.log(
|
|
126
|
+
`\n ${COLORS.dim}Re-run \`bun install -g @bastani/atomic\` after resolving the issues to retry.${COLORS.reset}\n`,
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
console.log();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress UI primitives for the first-run install flow (auto-sync).
|
|
3
|
+
*
|
|
4
|
+
* Renders a single persistent line:
|
|
5
|
+
*
|
|
6
|
+
* ⠋ [██████▒▒▒▒▒▒] 3/7 tmux / psmux
|
|
7
|
+
*
|
|
8
|
+
* where the braille spinner animation is provided by `@clack/prompts`
|
|
9
|
+
* and the bracketed bar tracks overall step progress (completed / total).
|
|
10
|
+
* A final summary (✓/✗ per step) is printed after all steps finish, and
|
|
11
|
+
* any captured stderr/stdout from a failed step is shown beneath it.
|
|
12
|
+
*
|
|
13
|
+
* Kept intentionally small — this is not a general-purpose progress
|
|
14
|
+
* library, just what auto-sync needs to stop being visually noisy.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spinner } from "@clack/prompts";
|
|
18
|
+
import { COLORS } from "@/theme/colors.ts";
|
|
19
|
+
|
|
20
|
+
const BAR_WIDTH = 18;
|
|
21
|
+
const BAR_FILLED = "█";
|
|
22
|
+
const BAR_EMPTY = "░";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Semantic bar states:
|
|
26
|
+
* progress → blue (Catppuccin accent; "in flight")
|
|
27
|
+
* success → green (universal "completed")
|
|
28
|
+
* error → red (universal "failed")
|
|
29
|
+
*
|
|
30
|
+
* The empty track stays dim regardless — only the filled portion carries
|
|
31
|
+
* the status signal, which keeps the bar legible while still telegraphing
|
|
32
|
+
* the outcome at a glance.
|
|
33
|
+
*/
|
|
34
|
+
type BarState = "progress" | "success" | "error";
|
|
35
|
+
|
|
36
|
+
function fillColor(state: BarState): string {
|
|
37
|
+
switch (state) {
|
|
38
|
+
case "success":
|
|
39
|
+
return COLORS.green;
|
|
40
|
+
case "error":
|
|
41
|
+
return COLORS.red;
|
|
42
|
+
case "progress":
|
|
43
|
+
default:
|
|
44
|
+
return COLORS.blue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Render a bracketed step-progress bar. `completed` is capped at `total`. */
|
|
49
|
+
function renderBar(
|
|
50
|
+
completed: number,
|
|
51
|
+
total: number,
|
|
52
|
+
state: BarState,
|
|
53
|
+
): string {
|
|
54
|
+
const safeTotal = Math.max(1, total);
|
|
55
|
+
const ratio = Math.max(0, Math.min(1, completed / safeTotal));
|
|
56
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
57
|
+
const empty = BAR_WIDTH - filled;
|
|
58
|
+
return (
|
|
59
|
+
COLORS.bold +
|
|
60
|
+
fillColor(state) +
|
|
61
|
+
BAR_FILLED.repeat(filled) +
|
|
62
|
+
COLORS.reset +
|
|
63
|
+
COLORS.dim +
|
|
64
|
+
BAR_EMPTY.repeat(empty) +
|
|
65
|
+
COLORS.reset
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatLine(
|
|
70
|
+
completed: number,
|
|
71
|
+
total: number,
|
|
72
|
+
label: string,
|
|
73
|
+
state: BarState = "progress",
|
|
74
|
+
): string {
|
|
75
|
+
const bar = renderBar(completed, total, state);
|
|
76
|
+
const counter = `${COLORS.dim}${completed}/${total}${COLORS.reset}`;
|
|
77
|
+
return `${bar} ${counter} ${label}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface StepResult {
|
|
81
|
+
label: string;
|
|
82
|
+
ok: boolean;
|
|
83
|
+
/** Error message (if any) surfaced in the final summary. */
|
|
84
|
+
error?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runs a sequence of async steps with a single persistent spinner line
|
|
89
|
+
* showing stepped progress. Each step's failure is collected rather than
|
|
90
|
+
* thrown, mirroring auto-sync's "best-effort" contract.
|
|
91
|
+
*
|
|
92
|
+
* Returns the per-step results in submission order so the caller can
|
|
93
|
+
* render a summary.
|
|
94
|
+
*/
|
|
95
|
+
export async function runSteps(
|
|
96
|
+
steps: Array<{ label: string; fn: () => Promise<unknown> }>,
|
|
97
|
+
): Promise<StepResult[]> {
|
|
98
|
+
const total = steps.length;
|
|
99
|
+
const results: StepResult[] = [];
|
|
100
|
+
const s = spinner();
|
|
101
|
+
|
|
102
|
+
// Start with 0/total so the user sees the bar immediately.
|
|
103
|
+
s.start(formatLine(0, total, steps[0]?.label ?? ""));
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < steps.length; i++) {
|
|
106
|
+
const step = steps[i]!;
|
|
107
|
+
s.message(formatLine(i, total, step.label));
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await step.fn();
|
|
111
|
+
results.push({ label: step.label, ok: true });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
results.push({ label: step.label, ok: false, error: message });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Stop with a filled bar + final label so the user sees we reached the
|
|
119
|
+
// end rather than leaving the last in-flight step pinned. Bar color
|
|
120
|
+
// flips to the universal status colors: green when every step
|
|
121
|
+
// succeeded, red when any step failed. The per-step ✓/✗ rows printed
|
|
122
|
+
// afterwards provide the detailed breakdown.
|
|
123
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
124
|
+
const allOk = okCount === total;
|
|
125
|
+
const finalLabel = allOk
|
|
126
|
+
? `${COLORS.green}Setup complete${COLORS.reset}`
|
|
127
|
+
: `${COLORS.red}Setup finished with errors${COLORS.reset}`;
|
|
128
|
+
s.stop(formatLine(total, total, finalLabel, allOk ? "success" : "error"));
|
|
129
|
+
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Print a compact per-step summary after `runSteps`. Successes render as
|
|
135
|
+
* a single dim line; failures render with a red cross and an indented
|
|
136
|
+
* excerpt of the captured error.
|
|
137
|
+
*/
|
|
138
|
+
export function printSummary(results: StepResult[]): void {
|
|
139
|
+
for (const result of results) {
|
|
140
|
+
if (result.ok) {
|
|
141
|
+
console.log(
|
|
142
|
+
` ${COLORS.green}✓${COLORS.reset} ${COLORS.dim}${result.label}${COLORS.reset}`,
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
console.log(
|
|
146
|
+
` ${COLORS.red}✗${COLORS.reset} ${result.label}`,
|
|
147
|
+
);
|
|
148
|
+
if (result.error) {
|
|
149
|
+
// Indent the first ~4 lines of the error so it reads as a nested
|
|
150
|
+
// block rather than wall-of-text.
|
|
151
|
+
const lines = result.error.split("\n").slice(0, 4);
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
console.log(` ${COLORS.dim}${line}${COLORS.reset}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -15,25 +15,38 @@ const SCM_SKILLS_TO_REMOVE_GLOBALLY = [
|
|
|
15
15
|
"sl-submit-diff",
|
|
16
16
|
] as const;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
interface NpxSkillsResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Tail of captured stderr/stdout — surfaced by the spinner on failure. */
|
|
21
|
+
details: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runNpxSkills(args: string[]): Promise<NpxSkillsResult> {
|
|
19
25
|
const npxPath = Bun.which("npx");
|
|
20
26
|
if (!npxPath) {
|
|
21
|
-
|
|
22
|
-
return false;
|
|
27
|
+
return { ok: false, details: "npx not found on PATH" };
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
// Capture stdout/stderr so the outer spinner UI owns terminal output and
|
|
31
|
+
// can surface the tail of any failure. `npx skills add` is otherwise
|
|
32
|
+
// very chatty.
|
|
25
33
|
const proc = Bun.spawn([npxPath, "--yes", "skills", ...args], {
|
|
26
|
-
|
|
34
|
+
stdout: "pipe",
|
|
35
|
+
stderr: "pipe",
|
|
27
36
|
});
|
|
28
|
-
const exitCode = await
|
|
29
|
-
|
|
37
|
+
const [stderr, stdout, exitCode] = await Promise.all([
|
|
38
|
+
new Response(proc.stderr).text(),
|
|
39
|
+
new Response(proc.stdout).text(),
|
|
40
|
+
proc.exited,
|
|
41
|
+
]);
|
|
42
|
+
const details = (stderr.trim() || stdout.trim()).slice(-800);
|
|
43
|
+
return { ok: exitCode === 0, details };
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
export async function installGlobalSkills(): Promise<void> {
|
|
33
47
|
const agentFlags = SKILLS_AGENTS.flatMap((agent) => ["-a", agent]);
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
const addOk = await runNpxSkills([
|
|
49
|
+
const addResult = await runNpxSkills([
|
|
37
50
|
"add",
|
|
38
51
|
SKILLS_REPO,
|
|
39
52
|
"--skill",
|
|
@@ -42,26 +55,22 @@ export async function installGlobalSkills(): Promise<void> {
|
|
|
42
55
|
...agentFlags,
|
|
43
56
|
"-y",
|
|
44
57
|
]);
|
|
45
|
-
if (!
|
|
46
|
-
|
|
47
|
-
return;
|
|
58
|
+
if (!addResult.ok) {
|
|
59
|
+
throw new Error(`npx skills add failed: ${addResult.details}`);
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
const removeSkillFlags = SCM_SKILLS_TO_REMOVE_GLOBALLY.flatMap((skill) => [
|
|
51
63
|
"--skill",
|
|
52
64
|
skill,
|
|
53
65
|
]);
|
|
54
|
-
|
|
55
|
-
"Removing source-control skill variants globally (added per-project by `atomic init`)...",
|
|
56
|
-
);
|
|
57
|
-
const removeOk = await runNpxSkills([
|
|
66
|
+
const removeResult = await runNpxSkills([
|
|
58
67
|
"remove",
|
|
59
68
|
...removeSkillFlags,
|
|
60
69
|
"-g",
|
|
61
70
|
...agentFlags,
|
|
62
71
|
"-y",
|
|
63
72
|
]);
|
|
64
|
-
if (!
|
|
65
|
-
|
|
73
|
+
if (!removeResult.ok) {
|
|
74
|
+
throw new Error(`npx skills remove failed: ${removeResult.details}`);
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync bundled Atomic workflow templates from the installed package into
|
|
3
|
+
* the user's global `~/.atomic/workflows/` directory.
|
|
4
|
+
*
|
|
5
|
+
* Each bundled workflow directory (`hello/`, `hello-parallel/`, `ralph/`,
|
|
6
|
+
* etc.) is a full overwrite of its destination — `rm -rf` followed by a
|
|
7
|
+
* fresh copy — so files removed upstream don't linger after an upgrade.
|
|
8
|
+
* User-created workflows whose names don't collide with bundled names are
|
|
9
|
+
* left untouched (we only iterate the bundled source, never the user's
|
|
10
|
+
* destination).
|
|
11
|
+
*
|
|
12
|
+
* Root-level files (`tsconfig.json`, etc.) are also overwritten on each
|
|
13
|
+
* sync.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { join, sep } from "path";
|
|
17
|
+
import { readdir, rm } from "fs/promises";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import {
|
|
20
|
+
copyDir,
|
|
21
|
+
copyFile,
|
|
22
|
+
ensureDir,
|
|
23
|
+
pathExists,
|
|
24
|
+
} from "@/services/system/copy.ts";
|
|
25
|
+
import { assertPathWithinRoot } from "@/lib/path-root-guard.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reject any entry name that could redirect a write outside destRoot.
|
|
29
|
+
* `readdir` should never return `.`, `..`, or names with separators, but
|
|
30
|
+
* this is a cheap belt-and-braces check in case the installed package has
|
|
31
|
+
* been tampered with or sits on an exotic filesystem.
|
|
32
|
+
*/
|
|
33
|
+
function isSafeEntryName(name: string): boolean {
|
|
34
|
+
if (name === "" || name === "." || name === "..") return false;
|
|
35
|
+
if (name.includes("/") || name.includes("\\")) return false;
|
|
36
|
+
if (sep !== "/" && name.includes(sep)) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Locate the package root by walking up from this module. Both in installed
|
|
42
|
+
* (`<pkg>/src/services/system/workflows.ts`) and dev checkout layouts the
|
|
43
|
+
* package root is three directories up.
|
|
44
|
+
*/
|
|
45
|
+
function packageRoot(): string {
|
|
46
|
+
return join(import.meta.dir, "..", "..", "..");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Honors ATOMIC_SETTINGS_HOME so tests can point at a temp dir. */
|
|
50
|
+
function homeRoot(): string {
|
|
51
|
+
return process.env.ATOMIC_SETTINGS_HOME ?? homedir();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sync bundled workflow templates to `~/.atomic/workflows/`. Returns the
|
|
56
|
+
* number of bundled workflows installed (for logging). Best-effort on
|
|
57
|
+
* individual entries — readdir failures throw, per-entry copy failures
|
|
58
|
+
* propagate up to the caller (auto-sync's runStep).
|
|
59
|
+
*/
|
|
60
|
+
export async function installGlobalWorkflows(): Promise<void> {
|
|
61
|
+
const srcRoot = join(packageRoot(), ".atomic", "workflows");
|
|
62
|
+
const destRoot = join(homeRoot(), ".atomic", "workflows");
|
|
63
|
+
|
|
64
|
+
if (!(await pathExists(srcRoot))) {
|
|
65
|
+
// Treat a missing bundled source as a non-fatal skip: dev checkouts
|
|
66
|
+
// and partial installs legitimately hit this path. Surfaced via a
|
|
67
|
+
// thrown error so the spinner UI marks the step red with context.
|
|
68
|
+
throw new Error(`bundled workflows missing at ${srcRoot} — skipping ${destRoot}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await ensureDir(destRoot);
|
|
72
|
+
|
|
73
|
+
// Safety invariant: we enumerate the BUNDLED source, never the user's
|
|
74
|
+
// destination. This guarantees that `rm(dest)` can only ever target a
|
|
75
|
+
// path whose basename exists in the bundled workflows — user-created
|
|
76
|
+
// workflows with different names are structurally invisible to this loop.
|
|
77
|
+
const entries = await readdir(srcRoot, { withFileTypes: true });
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!isSafeEntryName(entry.name)) {
|
|
81
|
+
console.warn(
|
|
82
|
+
` skipping unsafe bundled workflow entry name: ${JSON.stringify(entry.name)}`,
|
|
83
|
+
);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const src = join(srcRoot, entry.name);
|
|
88
|
+
const dest = join(destRoot, entry.name);
|
|
89
|
+
|
|
90
|
+
// Belt-and-braces: confirm the computed destination actually lives
|
|
91
|
+
// inside destRoot. Throws if something conspired to produce an escape.
|
|
92
|
+
assertPathWithinRoot(destRoot, dest, "Workflow destination");
|
|
93
|
+
|
|
94
|
+
if (entry.isFile()) {
|
|
95
|
+
// Root files (tsconfig.json, etc.) — overwrite in place.
|
|
96
|
+
await copyFile(src, dest);
|
|
97
|
+
} else if (entry.isDirectory()) {
|
|
98
|
+
// Bundled workflow — full overwrite of the destination directory so
|
|
99
|
+
// files removed upstream don't linger across upgrades. User-created
|
|
100
|
+
// workflows under names that don't collide are untouched.
|
|
101
|
+
await rm(dest, { recursive: true, force: true });
|
|
102
|
+
await copyDir(src, dest);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/theme/colors.ts
CHANGED
|
@@ -11,6 +11,7 @@ const ANSI_CODES = {
|
|
|
11
11
|
red: "\x1b[31m",
|
|
12
12
|
green: "\x1b[32m",
|
|
13
13
|
yellow: "\x1b[33m",
|
|
14
|
+
blue: "\x1b[34m",
|
|
14
15
|
} as const;
|
|
15
16
|
|
|
16
17
|
const NO_COLORS = {
|
|
@@ -20,6 +21,7 @@ const NO_COLORS = {
|
|
|
20
21
|
red: "",
|
|
21
22
|
green: "",
|
|
22
23
|
yellow: "",
|
|
24
|
+
blue: "",
|
|
23
25
|
} as const;
|
|
24
26
|
|
|
25
27
|
export const COLORS = supportsColor() ? ANSI_CODES : NO_COLORS;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Update command — upgrades atomic to the latest version via bun and
|
|
3
|
-
* reinstalls global skills.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* atomic update
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { COLORS } from "@/theme/colors.ts";
|
|
10
|
-
import { VERSION } from "@/version.ts";
|
|
11
|
-
import { installGlobalSkills } from "@/services/system/skills.ts";
|
|
12
|
-
|
|
13
|
-
export async function updateCommand(): Promise<number> {
|
|
14
|
-
const bunPath = Bun.which("bun");
|
|
15
|
-
if (!bunPath) {
|
|
16
|
-
console.error(`${COLORS.red}Error: bun is not installed.${COLORS.reset}`);
|
|
17
|
-
console.error("Install bun: https://bun.sh");
|
|
18
|
-
return 1;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
console.log(`Current version: ${VERSION}`);
|
|
22
|
-
console.log("Updating atomic...\n");
|
|
23
|
-
|
|
24
|
-
// Upgrade the package
|
|
25
|
-
const proc = Bun.spawn([bunPath, "add", "-g", "atomic@latest"], {
|
|
26
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
27
|
-
});
|
|
28
|
-
const exitCode = await proc.exited;
|
|
29
|
-
|
|
30
|
-
if (exitCode !== 0) {
|
|
31
|
-
console.error(`\n${COLORS.red}Failed to update atomic (exit ${exitCode}).${COLORS.reset}`);
|
|
32
|
-
return 1;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Reinstall global skills
|
|
36
|
-
console.log("\nUpdating skills...");
|
|
37
|
-
try {
|
|
38
|
-
await installGlobalSkills();
|
|
39
|
-
} catch (error) {
|
|
40
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
-
console.warn(`\n${COLORS.yellow}Warning: failed to install skills: ${message}${COLORS.reset}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
console.log(`\n${COLORS.green}Update complete.${COLORS.reset}`);
|
|
45
|
-
return 0;
|
|
46
|
-
}
|