@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.
Files changed (68) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +956 -0
  3. package/assets/settings.schema.json +52 -0
  4. package/package.json +68 -0
  5. package/src/cli.ts +197 -0
  6. package/src/commands/cli/chat/client.ts +18 -0
  7. package/src/commands/cli/chat/index.ts +247 -0
  8. package/src/commands/cli/chat.ts +8 -0
  9. package/src/commands/cli/config.ts +55 -0
  10. package/src/commands/cli/init/index.ts +452 -0
  11. package/src/commands/cli/init/onboarding.ts +45 -0
  12. package/src/commands/cli/init/scm.ts +190 -0
  13. package/src/commands/cli/init.ts +8 -0
  14. package/src/commands/cli/update.ts +46 -0
  15. package/src/commands/cli/workflow.ts +164 -0
  16. package/src/lib/merge.ts +65 -0
  17. package/src/lib/path-root-guard.ts +38 -0
  18. package/src/lib/spawn.ts +467 -0
  19. package/src/scripts/bump-version.ts +94 -0
  20. package/src/scripts/constants-base.ts +14 -0
  21. package/src/scripts/constants.ts +34 -0
  22. package/src/sdk/components/color-utils.ts +20 -0
  23. package/src/sdk/components/connectors.test.ts +661 -0
  24. package/src/sdk/components/connectors.ts +156 -0
  25. package/src/sdk/components/edge.tsx +11 -0
  26. package/src/sdk/components/error-boundary.tsx +38 -0
  27. package/src/sdk/components/graph-theme.ts +36 -0
  28. package/src/sdk/components/header.tsx +60 -0
  29. package/src/sdk/components/layout.test.ts +924 -0
  30. package/src/sdk/components/layout.ts +186 -0
  31. package/src/sdk/components/node-card.tsx +68 -0
  32. package/src/sdk/components/orchestrator-panel-contexts.ts +26 -0
  33. package/src/sdk/components/orchestrator-panel-store.test.ts +561 -0
  34. package/src/sdk/components/orchestrator-panel-store.ts +118 -0
  35. package/src/sdk/components/orchestrator-panel-types.ts +21 -0
  36. package/src/sdk/components/orchestrator-panel.tsx +143 -0
  37. package/src/sdk/components/session-graph-panel.tsx +364 -0
  38. package/src/sdk/components/status-helpers.ts +32 -0
  39. package/src/sdk/components/statusline.tsx +63 -0
  40. package/src/sdk/define-workflow.ts +98 -0
  41. package/src/sdk/errors.ts +39 -0
  42. package/src/sdk/index.ts +38 -0
  43. package/src/sdk/providers/claude.ts +316 -0
  44. package/src/sdk/providers/copilot.ts +43 -0
  45. package/src/sdk/providers/opencode.ts +43 -0
  46. package/src/sdk/runtime/discovery.ts +172 -0
  47. package/src/sdk/runtime/executor.test.ts +415 -0
  48. package/src/sdk/runtime/executor.ts +695 -0
  49. package/src/sdk/runtime/loader.ts +372 -0
  50. package/src/sdk/runtime/panel.tsx +9 -0
  51. package/src/sdk/runtime/theme.ts +76 -0
  52. package/src/sdk/runtime/tmux.ts +542 -0
  53. package/src/sdk/types.ts +114 -0
  54. package/src/sdk/workflows.ts +85 -0
  55. package/src/services/config/atomic-config.ts +124 -0
  56. package/src/services/config/atomic-global-config.ts +361 -0
  57. package/src/services/config/config-path.ts +19 -0
  58. package/src/services/config/definitions.ts +176 -0
  59. package/src/services/config/index.ts +7 -0
  60. package/src/services/config/settings-schema.ts +2 -0
  61. package/src/services/config/settings.ts +149 -0
  62. package/src/services/system/copy.ts +381 -0
  63. package/src/services/system/detect.ts +161 -0
  64. package/src/services/system/download.ts +325 -0
  65. package/src/services/system/file-lock.ts +289 -0
  66. package/src/services/system/skills.ts +67 -0
  67. package/src/theme/colors.ts +25 -0
  68. package/src/version.ts +7 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * atomic/workflows
3
+ *
4
+ * Workflow SDK for defining multi-session agent workflows.
5
+ * Workflows are defined as a chain of .session() calls and compiled
6
+ * into a WorkflowDefinition consumed by the Atomic CLI runtime.
7
+ */
8
+
9
+ export { defineWorkflow, WorkflowBuilder } from "./define-workflow.ts";
10
+
11
+ export type {
12
+ AgentType,
13
+ Transcript,
14
+ SavedMessage,
15
+ SaveTranscript,
16
+ SessionContext,
17
+ SessionOptions,
18
+ WorkflowOptions,
19
+ WorkflowDefinition,
20
+ } from "./types.ts";
21
+
22
+ // Re-export native SDK types for convenience
23
+ export type { SessionEvent as CopilotSessionEvent } from "@github/copilot-sdk";
24
+ export type { SessionPromptResponse as OpenCodePromptResponse } from "@opencode-ai/sdk/v2";
25
+ export type { SessionMessage as ClaudeSessionMessage } from "@anthropic-ai/claude-agent-sdk";
26
+
27
+ // Providers
28
+ export { createClaudeSession, claudeQuery, clearClaudeSession, validateClaudeWorkflow } from "./providers/claude.ts";
29
+ export type { ClaudeSessionOptions, ClaudeQueryOptions, ClaudeQueryResult, ClaudeValidationWarning } from "./providers/claude.ts";
30
+
31
+ export { validateCopilotWorkflow } from "./providers/copilot.ts";
32
+ export type { CopilotValidationWarning } from "./providers/copilot.ts";
33
+
34
+ export { validateOpenCodeWorkflow } from "./providers/opencode.ts";
35
+ export type { OpenCodeValidationWarning } from "./providers/opencode.ts";
36
+
37
+ // Runtime — tmux utilities
38
+ export {
39
+ isTmuxInstalled,
40
+ getMuxBinary,
41
+ resetMuxBinaryCache,
42
+ isInsideTmux,
43
+ createSession,
44
+ createWindow,
45
+ createPane,
46
+ sendLiteralText,
47
+ sendSpecialKey,
48
+ sendKeysAndSubmit,
49
+ capturePane,
50
+ capturePaneVisible,
51
+ capturePaneScrollback,
52
+ killSession,
53
+ killWindow,
54
+ sessionExists,
55
+ attachSession,
56
+ switchClient,
57
+ getCurrentSession,
58
+ attachOrSwitch,
59
+ selectWindow,
60
+ waitForOutput,
61
+ tmuxRun,
62
+ normalizeTmuxCapture,
63
+ normalizeTmuxLines,
64
+ paneLooksReady,
65
+ paneHasActiveTask,
66
+ paneIsIdle,
67
+ waitForPaneReady,
68
+ attemptSubmitRounds,
69
+ } from "./runtime/tmux.ts";
70
+
71
+ // Runtime — workflow discovery
72
+ export {
73
+ AGENTS,
74
+ discoverWorkflows,
75
+ findWorkflow,
76
+ WORKFLOWS_GITIGNORE,
77
+ } from "./runtime/discovery.ts";
78
+ export type { DiscoveredWorkflow } from "./runtime/discovery.ts";
79
+
80
+ // Runtime — workflow loader pipeline
81
+ export { WorkflowLoader } from "./runtime/loader.ts";
82
+
83
+ // Runtime — workflow executor
84
+ export { executeWorkflow } from "./runtime/executor.ts";
85
+ export type { WorkflowRunOptions } from "./runtime/executor.ts";
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Atomic configuration file utilities for persisting project settings.
3
+ *
4
+ * Project/source-control selections are stored in `.atomic/settings.json`.
5
+ * Resolution order:
6
+ * 1) local `.atomic/settings.json` (project override)
7
+ * 2) global `~/.atomic/settings.json` (default fallback)
8
+ */
9
+
10
+ import { readFile, writeFile } from "fs/promises";
11
+ import { join, dirname } from "path";
12
+ import { homedir } from "os";
13
+ import { type SourceControlType } from "@/services/config/index.ts";
14
+ import { SETTINGS_SCHEMA_URL } from "@/services/config/settings-schema.ts";
15
+ import { ensureDir } from "@/services/system/copy.ts";
16
+
17
+ const SETTINGS_DIR = ".atomic";
18
+ const SETTINGS_FILENAME = "settings.json";
19
+
20
+ /**
21
+ * Atomic project configuration schema.
22
+ */
23
+ export interface AtomicConfig {
24
+ /** Version of config schema */
25
+ version?: number;
26
+ /** Selected source control type */
27
+ scm?: SourceControlType;
28
+ /** Timestamp of last init */
29
+ lastUpdated?: string;
30
+ }
31
+
32
+ type JsonRecord = Record<string, unknown>;
33
+
34
+ function getGlobalSettingsPath(): string {
35
+ const home = process.env.ATOMIC_SETTINGS_HOME ?? homedir();
36
+ return join(home, SETTINGS_DIR, SETTINGS_FILENAME);
37
+ }
38
+
39
+ function getLocalSettingsPath(projectDir: string): string {
40
+ return join(projectDir, SETTINGS_DIR, SETTINGS_FILENAME);
41
+ }
42
+
43
+ async function readJsonFile(path: string): Promise<JsonRecord | null> {
44
+ try {
45
+ return JSON.parse(await readFile(path, "utf-8")) as JsonRecord;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function pickAtomicConfig(record: JsonRecord | null): AtomicConfig | null {
52
+ if (!record) return null;
53
+
54
+ const config: AtomicConfig = {};
55
+ const version = record.version;
56
+ const scm = record.scm;
57
+ const lastUpdated = record.lastUpdated;
58
+
59
+ if (typeof version === "number") config.version = version;
60
+ if (typeof scm === "string") config.scm = scm as SourceControlType;
61
+ if (typeof lastUpdated === "string") config.lastUpdated = lastUpdated;
62
+
63
+ return Object.keys(config).length > 0 ? config : null;
64
+ }
65
+
66
+ function mergeConfigs(...configs: Array<AtomicConfig | null>): AtomicConfig | null {
67
+ const merged: AtomicConfig = {};
68
+ for (const config of configs) {
69
+ if (!config) continue;
70
+ if (config.version !== undefined) merged.version = config.version;
71
+ if (config.scm !== undefined) merged.scm = config.scm;
72
+ if (config.lastUpdated !== undefined) merged.lastUpdated = config.lastUpdated;
73
+ }
74
+ return Object.keys(merged).length > 0 ? merged : null;
75
+ }
76
+
77
+ /**
78
+ * Read atomic config with local override semantics.
79
+ */
80
+ export async function readAtomicConfig(projectDir: string): Promise<AtomicConfig | null> {
81
+ const localConfig = pickAtomicConfig(await readJsonFile(getLocalSettingsPath(projectDir)));
82
+ const globalConfig = pickAtomicConfig(await readJsonFile(getGlobalSettingsPath()));
83
+
84
+ // global < local settings
85
+ return mergeConfigs(globalConfig, localConfig);
86
+ }
87
+
88
+ /**
89
+ * Save project config to `.atomic/settings.json`.
90
+ */
91
+ export async function saveAtomicConfig(
92
+ projectDir: string,
93
+ updates: Partial<AtomicConfig>
94
+ ): Promise<void> {
95
+ const localPath = getLocalSettingsPath(projectDir);
96
+
97
+ const localSettings = (await readJsonFile(localPath)) ?? {};
98
+ const localExistingConfig = pickAtomicConfig(localSettings);
99
+ const currentConfig = localExistingConfig ?? {};
100
+
101
+ const newConfig: AtomicConfig = {
102
+ ...currentConfig,
103
+ ...updates,
104
+ version: 1,
105
+ lastUpdated: new Date().toISOString(),
106
+ };
107
+
108
+ const nextSettings: JsonRecord = {
109
+ ...localSettings,
110
+ ...newConfig,
111
+ $schema: SETTINGS_SCHEMA_URL,
112
+ };
113
+
114
+ await ensureDir(dirname(localPath));
115
+ await writeFile(localPath, JSON.stringify(nextSettings, null, 2) + "\n", "utf-8");
116
+ }
117
+
118
+ /**
119
+ * Get selected SCM using local override + global fallback.
120
+ */
121
+ export async function getSelectedScm(projectDir: string): Promise<SourceControlType | null> {
122
+ const config = await readAtomicConfig(projectDir);
123
+ return config?.scm ?? null;
124
+ }
@@ -0,0 +1,361 @@
1
+ import { copyFile, lstat, readdir, rm, rmdir } from "fs/promises";
2
+ import { join, resolve } from "path";
3
+ import { homedir } from "os";
4
+
5
+ import { AGENT_CONFIG, type AgentKey } from "@/services/config/index.ts";
6
+ import { mergeJsonFile } from "@/lib/merge.ts";
7
+ import { copyDir, ensureDir, pathExists } from "@/services/system/copy.ts";
8
+
9
+
10
+ const ATOMIC_HOME_DIR = join(homedir(), ".atomic");
11
+
12
+ const GLOBAL_AGENT_FOLDER_BY_KEY: Record<AgentKey, string> = {
13
+ claude: ".claude",
14
+ opencode: ".opencode",
15
+ copilot: ".copilot",
16
+ };
17
+
18
+ const TEMPLATE_AGENT_FOLDER_BY_KEY: Record<AgentKey, string> = {
19
+ claude: AGENT_CONFIG.claude.folder,
20
+ opencode: AGENT_CONFIG.opencode.folder,
21
+ copilot: AGENT_CONFIG.copilot.folder,
22
+ };
23
+
24
+ /**
25
+ * Per-agent subdirectories copied from the bundled template into the
26
+ * provider home. Only `agents/` now — skills ship via the skills CLI.
27
+ */
28
+ const GLOBAL_SYNC_SUBDIRECTORIES = ["agents"] as const;
29
+
30
+ /**
31
+ * Top-level files copied per agent. Copilot's lsp.json is renamed to
32
+ * lsp-config.json in the destination (see `GLOBAL_SYNC_DESTINATION_FILE_NAMES`).
33
+ */
34
+ const GLOBAL_SYNC_FILES: Partial<Record<AgentKey, readonly string[]>> = {
35
+ copilot: ["lsp.json"],
36
+ };
37
+
38
+ const GLOBAL_SYNC_DESTINATION_FILE_NAMES: Partial<Record<AgentKey, Partial<Record<string, string>>>> = {
39
+ copilot: {
40
+ "lsp.json": "lsp-config.json",
41
+ },
42
+ };
43
+
44
+ /**
45
+ * Return the Atomic home directory used for global workflows/tools/settings.
46
+ */
47
+ export function getAtomicHomeDir(): string {
48
+ return ATOMIC_HOME_DIR;
49
+ }
50
+
51
+ function resolveHomeDirFromAtomicHome(baseDir: string): string {
52
+ return resolve(baseDir, "..");
53
+ }
54
+
55
+ /**
56
+ * Get Atomic-managed provider config directories.
57
+ *
58
+ * Atomic now installs provider configs into the provider home roots while
59
+ * keeping Atomic-specific state under ~/.atomic.
60
+ */
61
+ export function getAtomicManagedConfigDirs(baseDir: string = ATOMIC_HOME_DIR): string[] {
62
+ const homeDir = resolveHomeDirFromAtomicHome(baseDir);
63
+ return [
64
+ join(homeDir, GLOBAL_AGENT_FOLDER_BY_KEY.claude),
65
+ join(homeDir, GLOBAL_AGENT_FOLDER_BY_KEY.opencode),
66
+ join(homeDir, GLOBAL_AGENT_FOLDER_BY_KEY.copilot),
67
+ ];
68
+ }
69
+
70
+ /**
71
+ * Get the provider home-folder suffix for the given agent.
72
+ */
73
+ export function getAtomicGlobalAgentFolder(agentKey: AgentKey): string {
74
+ return GLOBAL_AGENT_FOLDER_BY_KEY[agentKey];
75
+ }
76
+
77
+ /**
78
+ * Resolve the destination directory where Atomic installs provider configs.
79
+ */
80
+ export function getAtomicManagedAgentDir(
81
+ agentKey: AgentKey,
82
+ baseDir: string = ATOMIC_HOME_DIR,
83
+ ): string {
84
+ return join(resolveHomeDirFromAtomicHome(baseDir), getAtomicGlobalAgentFolder(agentKey));
85
+ }
86
+
87
+ /**
88
+ * Get the bundled template folder for the given agent.
89
+ */
90
+ export function getTemplateAgentFolder(agentKey: AgentKey): string {
91
+ return TEMPLATE_AGENT_FOLDER_BY_KEY[agentKey];
92
+ }
93
+
94
+ interface ManagedTreeEntries {
95
+ directories: string[];
96
+ files: string[];
97
+ }
98
+
99
+ async function collectManagedTreeEntries(
100
+ sourceDir: string,
101
+ exclude: readonly string[],
102
+ relativeDir: string = "",
103
+ ): Promise<ManagedTreeEntries> {
104
+ if (!(await pathExists(sourceDir))) {
105
+ return {
106
+ directories: [],
107
+ files: [],
108
+ };
109
+ }
110
+
111
+ const directories: string[] = [];
112
+ const files: string[] = [];
113
+ const entries = await readdir(sourceDir, { withFileTypes: true });
114
+
115
+ for (const entry of entries) {
116
+ const relativePath = relativeDir.length > 0
117
+ ? join(relativeDir, entry.name)
118
+ : entry.name;
119
+
120
+ if (exclude.includes(entry.name)) {
121
+ continue;
122
+ }
123
+
124
+ const normalizedRelativePath = relativePath.replace(/\\/g, "/");
125
+ if (exclude.some((excluded) =>
126
+ normalizedRelativePath === excluded.replace(/\\/g, "/") ||
127
+ normalizedRelativePath.startsWith(`${excluded.replace(/\\/g, "/")}/`)
128
+ )) {
129
+ continue;
130
+ }
131
+
132
+ const sourcePath = join(sourceDir, entry.name);
133
+ if (entry.isDirectory()) {
134
+ directories.push(relativePath);
135
+ const nestedEntries = await collectManagedTreeEntries(
136
+ sourcePath,
137
+ exclude,
138
+ relativePath,
139
+ );
140
+ directories.push(...nestedEntries.directories);
141
+ files.push(...nestedEntries.files);
142
+ continue;
143
+ }
144
+
145
+ if (entry.isFile() || entry.isSymbolicLink()) {
146
+ files.push(relativePath);
147
+ }
148
+ }
149
+
150
+ return { directories, files };
151
+ }
152
+
153
+ async function removeEmptyDirectoryIfPresent(pathToDirectory: string): Promise<void> {
154
+ try {
155
+ const stats = await lstat(pathToDirectory);
156
+ if (!stats.isDirectory()) {
157
+ return;
158
+ }
159
+ } catch {
160
+ return;
161
+ }
162
+
163
+ const entries = await readdir(pathToDirectory);
164
+ if (entries.length === 0) {
165
+ await rmdir(pathToDirectory);
166
+ }
167
+ }
168
+
169
+ function getGlobalSyncDestinationFileName(agentKey: AgentKey, sourceFileName: string): string {
170
+ return GLOBAL_SYNC_DESTINATION_FILE_NAMES[agentKey]?.[sourceFileName] ?? sourceFileName;
171
+ }
172
+
173
+ async function syncManagedGlobalFile(
174
+ sourcePath: string,
175
+ destinationPath: string,
176
+ ): Promise<void> {
177
+ await ensureDir(resolve(destinationPath, ".."));
178
+
179
+ if (await pathExists(destinationPath)) {
180
+ await mergeJsonFile(sourcePath, destinationPath);
181
+ return;
182
+ }
183
+
184
+ await copyFile(sourcePath, destinationPath);
185
+ }
186
+
187
+ /**
188
+ * Remove only the Atomic-managed entries from provider-native global roots.
189
+ *
190
+ * Mirrors `syncAtomicGlobalAgentConfigs`: walks the bundled template for
191
+ * each agent and removes every file/directory it would have installed. Any
192
+ * legacy skills or tools left behind from previous Atomic versions are
193
+ * intentionally untouched — those are owned by the skills CLI now.
194
+ */
195
+ export async function removeAtomicManagedGlobalAgentConfigs(
196
+ configRoot: string,
197
+ baseDir: string = ATOMIC_HOME_DIR,
198
+ ): Promise<void> {
199
+ const agentKeys = Object.keys(AGENT_CONFIG) as AgentKey[];
200
+
201
+ for (const agentKey of agentKeys) {
202
+ const sourceFolder = join(configRoot, getTemplateAgentFolder(agentKey));
203
+ const destinationFolder = getAtomicManagedAgentDir(agentKey, baseDir);
204
+ for (const subdirectory of GLOBAL_SYNC_SUBDIRECTORIES) {
205
+ const sourceSubdirectory = join(sourceFolder, subdirectory);
206
+ if (!(await pathExists(sourceSubdirectory))) {
207
+ continue;
208
+ }
209
+
210
+ const managedTree = await collectManagedTreeEntries(sourceSubdirectory, []);
211
+ const destinationSubdirectory = join(destinationFolder, subdirectory);
212
+
213
+ for (const relativeFile of managedTree.files) {
214
+ await rm(join(destinationSubdirectory, relativeFile), { force: true });
215
+ }
216
+
217
+ const managedDirectories = [...managedTree.directories].sort(
218
+ (left, right) => right.length - left.length,
219
+ );
220
+ for (const relativeDirectory of managedDirectories) {
221
+ await removeEmptyDirectoryIfPresent(join(destinationSubdirectory, relativeDirectory));
222
+ }
223
+ await removeEmptyDirectoryIfPresent(destinationSubdirectory);
224
+ }
225
+
226
+ const managedFiles = GLOBAL_SYNC_FILES[agentKey] ?? [];
227
+ for (const fileName of managedFiles) {
228
+ const sourceFilePath = join(sourceFolder, fileName);
229
+ if (!(await pathExists(sourceFilePath))) {
230
+ continue;
231
+ }
232
+
233
+ const destinationFilePath = join(
234
+ destinationFolder,
235
+ getGlobalSyncDestinationFileName(agentKey, fileName),
236
+ );
237
+ await rm(destinationFilePath, { force: true });
238
+ }
239
+
240
+ // Do NOT remove the top-level provider directory (e.g. ~/.claude, ~/.opencode,
241
+ // ~/.copilot) — Atomic does not own it and it may contain user-managed configs.
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Sync bundled agent templates into provider-native global roots.
247
+ *
248
+ * Copies each agent's `agents/` directory plus a small set of top-level
249
+ * files (currently just Copilot's `lsp.json` → `lsp-config.json`). Skills
250
+ * are NOT synced from here — they are installed globally at install time
251
+ * via `npx skills add` from the git repo.
252
+ */
253
+ export async function syncAtomicGlobalAgentConfigs(
254
+ configRoot: string,
255
+ baseDir: string = ATOMIC_HOME_DIR,
256
+ ): Promise<void> {
257
+ await ensureDir(baseDir);
258
+
259
+ const agentKeys = Object.keys(AGENT_CONFIG) as AgentKey[];
260
+ for (const agentKey of agentKeys) {
261
+ const sourceFolder = join(configRoot, getTemplateAgentFolder(agentKey));
262
+ if (!(await pathExists(sourceFolder))) continue;
263
+
264
+ const destinationFolder = getAtomicManagedAgentDir(agentKey, baseDir);
265
+ await ensureDir(destinationFolder);
266
+
267
+ for (const subdirectory of GLOBAL_SYNC_SUBDIRECTORIES) {
268
+ const sourceSubdir = join(sourceFolder, subdirectory);
269
+ if (await pathExists(sourceSubdir)) {
270
+ await copyDir(sourceSubdir, join(destinationFolder, subdirectory));
271
+ }
272
+ }
273
+
274
+ const managedFiles = GLOBAL_SYNC_FILES[agentKey] ?? [];
275
+ for (const fileName of managedFiles) {
276
+ const sourceFilePath = join(sourceFolder, fileName);
277
+ if (!(await pathExists(sourceFilePath))) continue;
278
+
279
+ const destinationFilePath = join(
280
+ destinationFolder,
281
+ getGlobalSyncDestinationFileName(agentKey, fileName),
282
+ );
283
+ await syncManagedGlobalFile(sourceFilePath, destinationFilePath);
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Return true when every Atomic-bundled agent file is present at its
290
+ * destination in the provider-native global roots.
291
+ *
292
+ * This walks the bundled template for each agent and checks that every
293
+ * file (and the top-level files in `GLOBAL_SYNC_FILES`) has a matching
294
+ * entry under `~/.<agent>/`. A single missing file returns false so the
295
+ * caller can run a merge re-sync. User-added files in the destination
296
+ * that don't exist in the template are ignored — they never trigger a
297
+ * false-negative and they are never removed.
298
+ */
299
+ export async function hasAtomicGlobalAgentConfigs(
300
+ configRoot: string,
301
+ baseDir: string = ATOMIC_HOME_DIR,
302
+ ): Promise<boolean> {
303
+ const agentKeys = Object.keys(AGENT_CONFIG) as AgentKey[];
304
+
305
+ for (const agentKey of agentKeys) {
306
+ const sourceFolder = join(configRoot, getTemplateAgentFolder(agentKey));
307
+ if (!(await pathExists(sourceFolder))) {
308
+ // No template for this agent in the config root — nothing to verify.
309
+ continue;
310
+ }
311
+
312
+ const destinationFolder = getAtomicManagedAgentDir(agentKey, baseDir);
313
+ if (!(await pathExists(destinationFolder))) return false;
314
+
315
+ for (const subdirectory of GLOBAL_SYNC_SUBDIRECTORIES) {
316
+ const sourceSubdir = join(sourceFolder, subdirectory);
317
+ if (!(await pathExists(sourceSubdir))) continue;
318
+
319
+ const managedTree = await collectManagedTreeEntries(sourceSubdir, []);
320
+ const destinationSubdir = join(destinationFolder, subdirectory);
321
+
322
+ for (const relativeFile of managedTree.files) {
323
+ if (!(await pathExists(join(destinationSubdir, relativeFile)))) {
324
+ return false;
325
+ }
326
+ }
327
+ }
328
+
329
+ const managedFiles = GLOBAL_SYNC_FILES[agentKey] ?? [];
330
+ for (const fileName of managedFiles) {
331
+ const sourceFilePath = join(sourceFolder, fileName);
332
+ if (!(await pathExists(sourceFilePath))) continue;
333
+
334
+ const destinationFilePath = join(
335
+ destinationFolder,
336
+ getGlobalSyncDestinationFileName(agentKey, fileName),
337
+ );
338
+ if (!(await pathExists(destinationFilePath))) return false;
339
+ }
340
+ }
341
+
342
+ return true;
343
+ }
344
+
345
+ /**
346
+ * Verify-and-repair entrypoint for user-facing commands (`atomic init`,
347
+ * `atomic chat`). If every bundled agent file is present at its
348
+ * destination, returns immediately without touching disk. Otherwise
349
+ * runs a merge re-sync, which fills the missing files from the local
350
+ * config data dir while leaving user-added files alone.
351
+ *
352
+ * This helper heals drift (e.g. a user deleted `~/.claude/agents/<foo>.md`).
353
+ */
354
+ export async function ensureAtomicGlobalAgentConfigs(
355
+ configRoot: string,
356
+ baseDir: string = ATOMIC_HOME_DIR,
357
+ ): Promise<void> {
358
+ if (await hasAtomicGlobalAgentConfigs(configRoot, baseDir)) return;
359
+ await syncAtomicGlobalAgentConfigs(configRoot, baseDir);
360
+ }
361
+
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Config path resolution.
3
+ *
4
+ * Two installation modes:
5
+ * 1. Source/Development: Running from source with `bun run src/cli.ts`
6
+ * 2. npm/bun installed: Installed via `bun add -g atomic`
7
+ */
8
+
9
+ import { join } from "path";
10
+
11
+ /**
12
+ * Get the root directory where config folders (.claude, .opencode, .github) are stored.
13
+ *
14
+ * Navigates up from the current file to the package/repo root:
15
+ * src/services/config/config-path.ts -> ../../.. -> root
16
+ */
17
+ export function getConfigRoot(): string {
18
+ return join(import.meta.dir, "..", "..", "..");
19
+ }