@bastani/atomic 0.5.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +956 -0
- package/assets/settings.schema.json +52 -0
- package/package.json +68 -0
- package/src/cli.ts +197 -0
- package/src/commands/cli/chat/client.ts +18 -0
- package/src/commands/cli/chat/index.ts +247 -0
- package/src/commands/cli/chat.ts +8 -0
- package/src/commands/cli/config.ts +55 -0
- package/src/commands/cli/init/index.ts +452 -0
- package/src/commands/cli/init/onboarding.ts +45 -0
- package/src/commands/cli/init/scm.ts +190 -0
- package/src/commands/cli/init.ts +8 -0
- package/src/commands/cli/update.ts +46 -0
- package/src/commands/cli/workflow.ts +164 -0
- package/src/lib/merge.ts +65 -0
- package/src/lib/path-root-guard.ts +38 -0
- package/src/lib/spawn.ts +467 -0
- package/src/scripts/bump-version.ts +94 -0
- package/src/scripts/constants-base.ts +14 -0
- package/src/scripts/constants.ts +34 -0
- package/src/sdk/components/color-utils.ts +20 -0
- package/src/sdk/components/connectors.test.ts +661 -0
- package/src/sdk/components/connectors.ts +156 -0
- package/src/sdk/components/edge.tsx +11 -0
- package/src/sdk/components/error-boundary.tsx +38 -0
- package/src/sdk/components/graph-theme.ts +36 -0
- package/src/sdk/components/header.tsx +60 -0
- package/src/sdk/components/layout.test.ts +924 -0
- package/src/sdk/components/layout.ts +186 -0
- package/src/sdk/components/node-card.tsx +68 -0
- package/src/sdk/components/orchestrator-panel-contexts.ts +26 -0
- package/src/sdk/components/orchestrator-panel-store.test.ts +561 -0
- package/src/sdk/components/orchestrator-panel-store.ts +118 -0
- package/src/sdk/components/orchestrator-panel-types.ts +21 -0
- package/src/sdk/components/orchestrator-panel.tsx +143 -0
- package/src/sdk/components/session-graph-panel.tsx +364 -0
- package/src/sdk/components/status-helpers.ts +32 -0
- package/src/sdk/components/statusline.tsx +63 -0
- package/src/sdk/define-workflow.ts +98 -0
- package/src/sdk/errors.ts +39 -0
- package/src/sdk/index.ts +38 -0
- package/src/sdk/providers/claude.ts +316 -0
- package/src/sdk/providers/copilot.ts +43 -0
- package/src/sdk/providers/opencode.ts +43 -0
- package/src/sdk/runtime/discovery.ts +172 -0
- package/src/sdk/runtime/executor.test.ts +415 -0
- package/src/sdk/runtime/executor.ts +695 -0
- package/src/sdk/runtime/loader.ts +372 -0
- package/src/sdk/runtime/panel.tsx +9 -0
- package/src/sdk/runtime/theme.ts +76 -0
- package/src/sdk/runtime/tmux.ts +542 -0
- package/src/sdk/types.ts +114 -0
- package/src/sdk/workflows.ts +85 -0
- package/src/services/config/atomic-config.ts +124 -0
- package/src/services/config/atomic-global-config.ts +361 -0
- package/src/services/config/config-path.ts +19 -0
- package/src/services/config/definitions.ts +176 -0
- package/src/services/config/index.ts +7 -0
- package/src/services/config/settings-schema.ts +2 -0
- package/src/services/config/settings.ts +149 -0
- package/src/services/system/copy.ts +381 -0
- package/src/services/system/detect.ts +161 -0
- package/src/services/system/download.ts +325 -0
- package/src/services/system/file-lock.ts +289 -0
- package/src/services/system/skills.ts +67 -0
- package/src/theme/colors.ts +25 -0
- package/src/version.ts +7 -0
|
@@ -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
|
+
}
|