@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,46 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow CLI command
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* atomic workflow -n <name> -a <agent> <prompt>
|
|
6
|
+
* atomic workflow --list
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AGENT_CONFIG, type AgentKey } from "@/services/config/index.ts";
|
|
10
|
+
import { COLORS } from "@/theme/colors.ts";
|
|
11
|
+
import { isCommandInstalled } from "@/services/system/detect.ts";
|
|
12
|
+
import { ensureTmuxInstalled, ensureBunInstalled } from "../../lib/spawn.ts";
|
|
13
|
+
import {
|
|
14
|
+
isTmuxInstalled,
|
|
15
|
+
discoverWorkflows,
|
|
16
|
+
findWorkflow,
|
|
17
|
+
executeWorkflow,
|
|
18
|
+
WorkflowLoader,
|
|
19
|
+
resetMuxBinaryCache,
|
|
20
|
+
} from "@/sdk/workflows.ts";
|
|
21
|
+
import type { AgentType } from "@/sdk/workflows.ts";
|
|
22
|
+
|
|
23
|
+
export async function workflowCommand(options: {
|
|
24
|
+
name?: string;
|
|
25
|
+
agent?: string;
|
|
26
|
+
prompt?: string;
|
|
27
|
+
list?: boolean;
|
|
28
|
+
}): Promise<number> {
|
|
29
|
+
// List mode
|
|
30
|
+
if (options.list) {
|
|
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
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Run mode — validate inputs
|
|
49
|
+
if (!options.name) {
|
|
50
|
+
console.error(`${COLORS.red}Error: Missing workflow name. Use -n <name>.${COLORS.reset}`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!options.agent) {
|
|
55
|
+
console.error(`${COLORS.red}Error: Missing agent. Use -a <agent>.${COLORS.reset}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const validAgents = Object.keys(AGENT_CONFIG);
|
|
60
|
+
if (!validAgents.includes(options.agent)) {
|
|
61
|
+
console.error(`${COLORS.red}Error: Unknown agent '${options.agent}'.${COLORS.reset}`);
|
|
62
|
+
console.error(`Valid agents: ${validAgents.join(", ")}`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const agent = options.agent as AgentKey;
|
|
67
|
+
|
|
68
|
+
// Check agent CLI is installed
|
|
69
|
+
if (!isCommandInstalled(AGENT_CONFIG[agent].cmd)) {
|
|
70
|
+
console.error(`${COLORS.red}Error: '${AGENT_CONFIG[agent].cmd}' is not installed.${COLORS.reset}`);
|
|
71
|
+
console.error(`Install it from: ${AGENT_CONFIG[agent].install_url}`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure tmux/psmux is installed
|
|
76
|
+
if (!isTmuxInstalled()) {
|
|
77
|
+
console.log("Terminal multiplexer not found. Installing...");
|
|
78
|
+
try {
|
|
79
|
+
await ensureTmuxInstalled();
|
|
80
|
+
resetMuxBinaryCache();
|
|
81
|
+
} catch {
|
|
82
|
+
// Installation attempt failed — fall through to check below
|
|
83
|
+
}
|
|
84
|
+
if (!isTmuxInstalled()) {
|
|
85
|
+
const isWin = process.platform === "win32";
|
|
86
|
+
console.error(`${COLORS.red}Error: ${isWin ? "psmux" : "tmux"} is not installed.${COLORS.reset}`);
|
|
87
|
+
console.error(
|
|
88
|
+
isWin
|
|
89
|
+
? "Install psmux: https://github.com/psmux/psmux#installation"
|
|
90
|
+
: "Install tmux: https://github.com/tmux/tmux/wiki/Installing",
|
|
91
|
+
);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Ensure bun is installed (required for workflow execution)
|
|
97
|
+
if (!Bun.which("bun")) {
|
|
98
|
+
console.log("Bun runtime not found. Installing...");
|
|
99
|
+
try {
|
|
100
|
+
await ensureBunInstalled();
|
|
101
|
+
} catch {
|
|
102
|
+
// Installation attempt failed — fall through to check below
|
|
103
|
+
}
|
|
104
|
+
if (!Bun.which("bun")) {
|
|
105
|
+
console.error(`${COLORS.red}Error: bun is not installed.${COLORS.reset}`);
|
|
106
|
+
console.error("Install bun: https://bun.sh");
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find the workflow
|
|
112
|
+
const discovered = await findWorkflow(options.name, agent);
|
|
113
|
+
|
|
114
|
+
if (!discovered) {
|
|
115
|
+
console.error(`${COLORS.red}Error: Workflow '${options.name}' not found for agent '${agent}'.${COLORS.reset}`);
|
|
116
|
+
console.error(`\nExpected location:`);
|
|
117
|
+
console.error(` .atomic/workflows/${options.name}/${agent}/index.ts ${COLORS.dim}(local)${COLORS.reset}`);
|
|
118
|
+
console.error(` ~/.atomic/workflows/${options.name}/${agent}/index.ts ${COLORS.dim}(global)${COLORS.reset}`);
|
|
119
|
+
|
|
120
|
+
const available = await discoverWorkflows(undefined, agent);
|
|
121
|
+
if (available.length > 0) {
|
|
122
|
+
console.error(`\nAvailable ${agent} workflows:`);
|
|
123
|
+
for (const wf of available) {
|
|
124
|
+
console.error(` ${COLORS.dim}•${COLORS.reset} ${wf.name} ${COLORS.dim}(${wf.source})${COLORS.reset}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Load workflow through the pipeline: resolve → validate → load.
|
|
132
|
+
// The loader registers a Bun resolver plugin that maps `atomic/*` and
|
|
133
|
+
// atomic's installed deps onto the running CLI's own module graph, so
|
|
134
|
+
// workflow files don't need their own `package.json` / `node_modules`.
|
|
135
|
+
const result = await WorkflowLoader.loadWorkflow(discovered, {
|
|
136
|
+
warn(warnings) {
|
|
137
|
+
for (const w of warnings) {
|
|
138
|
+
console.warn(`⚠ [${w.rule}] ${w.message}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
error(stage, _error, message) {
|
|
142
|
+
console.error(`${COLORS.red}Error (${stage}): ${message}${COLORS.reset}`);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Execute
|
|
151
|
+
try {
|
|
152
|
+
await executeWorkflow({
|
|
153
|
+
definition: result.value.definition,
|
|
154
|
+
agent,
|
|
155
|
+
prompt: options.prompt ?? "",
|
|
156
|
+
workflowFile: discovered.path,
|
|
157
|
+
});
|
|
158
|
+
return 0;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
console.error(`${COLORS.red}Workflow failed: ${message}${COLORS.reset}`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/lib/merge.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for merging JSON configuration files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile } from "fs/promises";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
interface McpConfig {
|
|
9
|
+
mcpServers?: Record<string, unknown>;
|
|
10
|
+
servers?: Record<string, unknown>;
|
|
11
|
+
lspServers?: Record<string, unknown>;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mergeNamedObjectMap(
|
|
16
|
+
destination: Record<string, unknown> | undefined,
|
|
17
|
+
source: Record<string, unknown> | undefined,
|
|
18
|
+
): Record<string, unknown> | undefined {
|
|
19
|
+
if (!destination && !source) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...destination,
|
|
25
|
+
...source,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Merge source JSON file into destination JSON file
|
|
31
|
+
* - Preserves all existing keys in destination
|
|
32
|
+
* - Adds/updates keys from source
|
|
33
|
+
* - For MCP server maps: preserves user's servers, adds/updates CLI-managed servers
|
|
34
|
+
*
|
|
35
|
+
* @param srcPath Path to source JSON file
|
|
36
|
+
* @param destPath Path to destination JSON file (will be modified in place)
|
|
37
|
+
*/
|
|
38
|
+
export async function mergeJsonFile(
|
|
39
|
+
srcPath: string,
|
|
40
|
+
destPath: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if (resolve(srcPath) === resolve(destPath)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const [srcContent, destContent] = await Promise.all([
|
|
47
|
+
readFile(srcPath, "utf-8"),
|
|
48
|
+
readFile(destPath, "utf-8"),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const srcConfig: McpConfig = JSON.parse(srcContent);
|
|
52
|
+
const destConfig: McpConfig = JSON.parse(destContent);
|
|
53
|
+
|
|
54
|
+
// Merge top-level config - preserve destination's other keys
|
|
55
|
+
const mergedConfig: McpConfig = {
|
|
56
|
+
...destConfig,
|
|
57
|
+
...srcConfig,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mergedConfig.mcpServers = mergeNamedObjectMap(destConfig.mcpServers, srcConfig.mcpServers);
|
|
61
|
+
mergedConfig.servers = mergeNamedObjectMap(destConfig.servers, srcConfig.servers);
|
|
62
|
+
mergedConfig.lspServers = mergeNamedObjectMap(destConfig.lspServers, srcConfig.lspServers);
|
|
63
|
+
|
|
64
|
+
await writeFile(destPath, JSON.stringify(mergedConfig, null, 2) + "\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { realpath } from "fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
function isSubPath(rootPath: string, targetPath: string): boolean {
|
|
5
|
+
const rel = relative(rootPath, targetPath);
|
|
6
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isPathWithinRoot(rootPath: string, candidatePath: string): boolean {
|
|
10
|
+
return isSubPath(resolve(rootPath), resolve(candidatePath));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertPathWithinRoot(
|
|
14
|
+
rootPath: string,
|
|
15
|
+
candidatePath: string,
|
|
16
|
+
label: string,
|
|
17
|
+
): void {
|
|
18
|
+
if (!isPathWithinRoot(rootPath, candidatePath)) {
|
|
19
|
+
throw new Error(`${label} escapes allowed root: ${candidatePath}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function assertRealPathWithinRoot(
|
|
24
|
+
rootPath: string,
|
|
25
|
+
candidatePath: string,
|
|
26
|
+
label: string,
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
const [resolvedRootPath, resolvedCandidatePath] = await Promise.all([
|
|
29
|
+
realpath(rootPath),
|
|
30
|
+
realpath(candidatePath),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
if (!isSubPath(resolvedRootPath, resolvedCandidatePath)) {
|
|
34
|
+
throw new Error(`${label} resolves outside allowed root: ${candidatePath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return resolvedCandidatePath;
|
|
38
|
+
}
|