@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,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Atomic Settings",
|
|
4
|
+
"description": "Configuration file for the Atomic CLI (@bastani/atomic). Stored at ~/.atomic/settings.json (global) or .atomic/settings.json (project-local).",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"$schema": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "JSON Schema URL for editor intellisense."
|
|
10
|
+
},
|
|
11
|
+
"version": {
|
|
12
|
+
"type": "number",
|
|
13
|
+
"description": "Config schema version."
|
|
14
|
+
},
|
|
15
|
+
"scm": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["github", "sapling"],
|
|
18
|
+
"description": "Selected source control management system."
|
|
19
|
+
},
|
|
20
|
+
"lastUpdated": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"format": "date-time",
|
|
23
|
+
"description": "ISO 8601 timestamp of the last configuration update."
|
|
24
|
+
},
|
|
25
|
+
"prerelease": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "When true, 'atomic update' fetches the latest prerelease instead of the latest stable release. Set automatically by the installer when using the --prerelease flag.",
|
|
28
|
+
"default": false
|
|
29
|
+
},
|
|
30
|
+
"trustedPaths": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"description": "Globally trusted workspaces that have completed provider onboarding through 'atomic init'.",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["workspacePath", "provider"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"workspacePath": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Absolute path to the initialized workspace."
|
|
40
|
+
},
|
|
41
|
+
"provider": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["claude", "opencode", "copilot"],
|
|
44
|
+
"description": "Provider initialized for the trusted workspace."
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bastani/atomic",
|
|
3
|
+
"version": "0.5.0-1",
|
|
4
|
+
"description": "Configuration management CLI and SDK for coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/flora131/atomic.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"cli",
|
|
13
|
+
"sdk",
|
|
14
|
+
"coding-agents",
|
|
15
|
+
"agents",
|
|
16
|
+
"ai-engineering",
|
|
17
|
+
"harness-engineering",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"copilot-cli",
|
|
20
|
+
"opencode"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"atomic": "src/cli.ts"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/sdk/index.ts",
|
|
30
|
+
"./workflows": "./src/sdk/workflows.ts"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"assets/settings.schema.json"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "bun run src/cli.ts",
|
|
38
|
+
"build": "bun run build.ts",
|
|
39
|
+
"test": "bun test ./src ./tests",
|
|
40
|
+
"test:coverage": "bun test --coverage ./src ./tests",
|
|
41
|
+
"typecheck": "bunx tsc --noEmit",
|
|
42
|
+
"lint": "oxlint --config=oxlint.json src",
|
|
43
|
+
"lint:fix": "oxlint --config=oxlint.json --fix src",
|
|
44
|
+
"prepare": "lefthook install || true"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.3.11",
|
|
48
|
+
"@types/react": "^19.2.14",
|
|
49
|
+
"lefthook": "^2.1.4",
|
|
50
|
+
"oxlint": "^1.59.0",
|
|
51
|
+
"typescript": "^6.0.2",
|
|
52
|
+
"typescript-language-server": "^5.1.3"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
56
|
+
"@clack/prompts": "^1.2.0",
|
|
57
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
58
|
+
"@github/copilot-sdk": "^0.2.1",
|
|
59
|
+
"@opencode-ai/sdk": "^1.3.17",
|
|
60
|
+
"@opentui/core": "^0.1.97",
|
|
61
|
+
"@opentui/react": "^0.1.97",
|
|
62
|
+
"commander": "^14.0.3",
|
|
63
|
+
"ignore": "^7.0.5",
|
|
64
|
+
"react": "19",
|
|
65
|
+
"yaml": "^2.8.3",
|
|
66
|
+
"zod": "^4.3.6"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Atomic CLI - Configuration management for coding agents
|
|
4
|
+
*
|
|
5
|
+
* Built with Commander.js for robust argument parsing and type-safe options.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* atomic Interactive setup (same as 'atomic init')
|
|
9
|
+
* atomic init Interactive setup with agent selection
|
|
10
|
+
* atomic init -a <agent> Setup specific agent (skip selection)
|
|
11
|
+
* atomic init -s <scm> Setup specific SCM (github, sapling)
|
|
12
|
+
* atomic chat -a <agent> Start interactive chat with an agent
|
|
13
|
+
* atomic config set <key> <value> Set configuration value
|
|
14
|
+
* atomic --version Show version
|
|
15
|
+
* atomic --help Show help
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Command } from "@commander-js/extra-typings";
|
|
19
|
+
import { VERSION } from "@/version.ts";
|
|
20
|
+
import { COLORS } from "@/theme/colors.ts";
|
|
21
|
+
import { AGENT_CONFIG, type AgentKey, SCM_CONFIG, type SourceControlType } from "@/services/config/index.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create and configure the main CLI program
|
|
25
|
+
*/
|
|
26
|
+
export function createProgram() {
|
|
27
|
+
const program = new Command()
|
|
28
|
+
.name("atomic")
|
|
29
|
+
.description("Configuration management CLI for coding agents")
|
|
30
|
+
.version(VERSION, "-v, --version", "Show version number")
|
|
31
|
+
|
|
32
|
+
// Global options available to all commands
|
|
33
|
+
.option("-f, --force", "Overwrite all config files")
|
|
34
|
+
.option("-y, --yes", "Auto-confirm all prompts (non-interactive mode)")
|
|
35
|
+
.option("--no-banner", "Skip ASCII banner display")
|
|
36
|
+
|
|
37
|
+
// Configure error output with colors
|
|
38
|
+
.configureOutput({
|
|
39
|
+
writeErr: (str) => {
|
|
40
|
+
process.stderr.write(`${COLORS.red}${str}${COLORS.reset}`);
|
|
41
|
+
},
|
|
42
|
+
outputError: (str, write) => {
|
|
43
|
+
write(`${COLORS.red}${str}${COLORS.reset}`);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Build agent choices string for help text
|
|
48
|
+
const agentChoices = Object.keys(AGENT_CONFIG).join(", ");
|
|
49
|
+
const scmChoices = Object.keys(SCM_CONFIG).join(", ");
|
|
50
|
+
|
|
51
|
+
// Add init command
|
|
52
|
+
program
|
|
53
|
+
.command("init")
|
|
54
|
+
.description("Interactive setup with agent selection")
|
|
55
|
+
.option(
|
|
56
|
+
"-a, --agent <name>",
|
|
57
|
+
`Pre-select agent to configure (${agentChoices})`,
|
|
58
|
+
)
|
|
59
|
+
.option(
|
|
60
|
+
"-s, --scm <name>",
|
|
61
|
+
`Pre-select source control system (${scmChoices})`,
|
|
62
|
+
)
|
|
63
|
+
.action(async (localOpts) => {
|
|
64
|
+
const globalOpts = program.opts();
|
|
65
|
+
const { initCommand } = await import("@/commands/cli/init.ts");
|
|
66
|
+
|
|
67
|
+
await initCommand({
|
|
68
|
+
showBanner: globalOpts.banner !== false,
|
|
69
|
+
preSelectedAgent: localOpts.agent as AgentKey | undefined,
|
|
70
|
+
preSelectedScm: localOpts.scm as SourceControlType | undefined,
|
|
71
|
+
force: globalOpts.force,
|
|
72
|
+
yes: globalOpts.yes,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Add chat command (default command when no subcommand is provided)
|
|
77
|
+
program
|
|
78
|
+
.command("chat", { isDefault: true })
|
|
79
|
+
.description("Start an interactive chat session with a coding agent")
|
|
80
|
+
.option("-a, --agent <name>", `Agent to chat with (${agentChoices})`)
|
|
81
|
+
.allowUnknownOption()
|
|
82
|
+
.allowExcessArguments(true)
|
|
83
|
+
.addHelpText(
|
|
84
|
+
"after",
|
|
85
|
+
`
|
|
86
|
+
All arguments after -a <agent> are forwarded to the native agent CLI.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
$ atomic chat -a claude Start Claude interactively
|
|
90
|
+
$ atomic chat -a copilot Start Copilot interactively
|
|
91
|
+
$ atomic chat -a opencode Start OpenCode interactively
|
|
92
|
+
$ atomic chat -a claude "fix the bug" Claude with initial prompt
|
|
93
|
+
$ atomic chat -a copilot --model gpt-4o Copilot with custom model
|
|
94
|
+
$ atomic chat -a claude --verbose Forward --verbose to claude`,
|
|
95
|
+
)
|
|
96
|
+
.action(async (localOpts, cmd) => {
|
|
97
|
+
const validAgents = Object.keys(AGENT_CONFIG);
|
|
98
|
+
const agentType = localOpts.agent;
|
|
99
|
+
|
|
100
|
+
if (!agentType) {
|
|
101
|
+
console.error(
|
|
102
|
+
`${COLORS.red}Error: Missing agent.${COLORS.reset}`,
|
|
103
|
+
);
|
|
104
|
+
console.error(
|
|
105
|
+
"Start chat with an explicit provider, for example: atomic chat -a claude",
|
|
106
|
+
);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate agent choice
|
|
111
|
+
if (!validAgents.includes(agentType)) {
|
|
112
|
+
console.error(
|
|
113
|
+
`${COLORS.red}Error: Unknown agent '${agentType}'${COLORS.reset}`,
|
|
114
|
+
);
|
|
115
|
+
console.error(`Valid agents: ${agentChoices}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Collect extra args/options to forward to the native CLI
|
|
120
|
+
const passthroughArgs = cmd.args;
|
|
121
|
+
|
|
122
|
+
const { chatCommand } = await import("@/commands/cli/chat.ts");
|
|
123
|
+
const exitCode = await chatCommand({
|
|
124
|
+
agentType: agentType as "claude" | "opencode" | "copilot",
|
|
125
|
+
passthroughArgs,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
process.exit(exitCode);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Add workflow command
|
|
132
|
+
program
|
|
133
|
+
.command("workflow")
|
|
134
|
+
.description("Run a multi-session agent workflow")
|
|
135
|
+
.option("-n, --name <name>", "Workflow name (matches directory under .atomic/workflows/<name>/)")
|
|
136
|
+
.option("-a, --agent <name>", `Agent to use (${agentChoices})`)
|
|
137
|
+
.option("-l, --list", "List available workflows")
|
|
138
|
+
.argument("[prompt...]", "Prompt for the workflow")
|
|
139
|
+
.action(async (promptParts, localOpts) => {
|
|
140
|
+
const { workflowCommand } = await import("@/commands/cli/workflow.ts");
|
|
141
|
+
const exitCode = await workflowCommand({
|
|
142
|
+
name: localOpts.name,
|
|
143
|
+
agent: localOpts.agent,
|
|
144
|
+
prompt: promptParts.length > 0 ? promptParts.join(" ") : undefined,
|
|
145
|
+
list: localOpts.list,
|
|
146
|
+
});
|
|
147
|
+
process.exit(exitCode);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
program
|
|
151
|
+
.command("update")
|
|
152
|
+
.description("Update atomic to the latest version and reinstall skills")
|
|
153
|
+
.action(async () => {
|
|
154
|
+
const { updateCommand } = await import("@/commands/cli/update.ts");
|
|
155
|
+
process.exit(await updateCommand());
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Add config command for managing CLI settings
|
|
159
|
+
const configCmd = program
|
|
160
|
+
.command("config")
|
|
161
|
+
.description("Manage atomic configuration");
|
|
162
|
+
|
|
163
|
+
// Add 'set' subcommand to config
|
|
164
|
+
configCmd
|
|
165
|
+
.command("set")
|
|
166
|
+
.description("Set a configuration value")
|
|
167
|
+
.argument("<key>", "Configuration key (e.g., telemetry)")
|
|
168
|
+
.argument("<value>", "Value to set (e.g., true, false)")
|
|
169
|
+
.action(async (key: string, value: string) => {
|
|
170
|
+
const { configCommand } = await import("@/commands/cli/config.ts");
|
|
171
|
+
await configCommand("set", key, value);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return program;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create the program instance for use by main() and tests
|
|
178
|
+
export const program = createProgram();
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Main entry point for the CLI
|
|
182
|
+
*/
|
|
183
|
+
async function main(): Promise<void> {
|
|
184
|
+
try {
|
|
185
|
+
// Parse and execute the command
|
|
186
|
+
await program.parseAsync();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
console.error(`${COLORS.red}Error: ${message}${COLORS.reset}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Run the CLI
|
|
195
|
+
if (import.meta.main) {
|
|
196
|
+
await main();
|
|
197
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat client utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides agent display name resolution for the chat command.
|
|
5
|
+
* SDK client creation has been removed — the chat command now spawns
|
|
6
|
+
* native agent CLIs directly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentKey } from "@/services/config/index.ts";
|
|
10
|
+
|
|
11
|
+
export function getAgentDisplayName(agentType: AgentKey): string {
|
|
12
|
+
const names: Record<AgentKey, string> = {
|
|
13
|
+
claude: "Claude",
|
|
14
|
+
opencode: "OpenCode",
|
|
15
|
+
copilot: "Copilot",
|
|
16
|
+
};
|
|
17
|
+
return names[agentType] ?? agentType;
|
|
18
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Chat CLI command for atomic
|
|
4
|
+
*
|
|
5
|
+
* Spawns the native agent CLI as an interactive subprocess.
|
|
6
|
+
* When already inside a tmux/psmux session, the agent spawns inline
|
|
7
|
+
* in the current pane. When outside tmux, it creates a new tmux
|
|
8
|
+
* session and attaches to it.
|
|
9
|
+
*
|
|
10
|
+
* All extra arguments after `-a <agent>` are forwarded to the native CLI.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* atomic chat -a <agent> [native-args...]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { mkdir, writeFile, rm } from "fs/promises";
|
|
19
|
+
import { AGENT_CONFIG, type AgentKey } from "@/services/config/index.ts";
|
|
20
|
+
import { COLORS } from "@/theme/colors.ts";
|
|
21
|
+
import { isCommandInstalled } from "@/services/system/detect.ts";
|
|
22
|
+
import {
|
|
23
|
+
ensureAtomicGlobalAgentConfigs,
|
|
24
|
+
} from "@/services/config/atomic-global-config.ts";
|
|
25
|
+
import { getConfigRoot } from "@/services/config/config-path.ts";
|
|
26
|
+
import {
|
|
27
|
+
isInsideTmux,
|
|
28
|
+
isTmuxInstalled,
|
|
29
|
+
resetMuxBinaryCache,
|
|
30
|
+
} from "@/sdk/workflows.ts";
|
|
31
|
+
import {
|
|
32
|
+
createSession,
|
|
33
|
+
killSession,
|
|
34
|
+
getMuxBinary,
|
|
35
|
+
} from "@/sdk/workflows.ts";
|
|
36
|
+
import { ensureTmuxInstalled } from "@/lib/spawn.ts";
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Types
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
export type AgentType = AgentKey;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options for the chat command.
|
|
46
|
+
*/
|
|
47
|
+
export interface ChatCommandOptions {
|
|
48
|
+
/** Agent type to use (claude, opencode, copilot) */
|
|
49
|
+
agentType?: AgentType;
|
|
50
|
+
/** Extra args/options forwarded verbatim to the native agent CLI */
|
|
51
|
+
passthroughArgs?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Helpers
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export function getAgentDisplayName(agentType: AgentType): string {
|
|
59
|
+
return AGENT_CONFIG[agentType].name;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the argv array for spawning the agent CLI.
|
|
64
|
+
*
|
|
65
|
+
* Starts with the agent's default chat_flags, then appends any
|
|
66
|
+
* extra args the user passed after `-a <agent>`.
|
|
67
|
+
*/
|
|
68
|
+
export function buildAgentArgs(agentType: AgentType, passthroughArgs: string[] = []): string[] {
|
|
69
|
+
const config = AGENT_CONFIG[agentType];
|
|
70
|
+
return [...config.chat_flags, ...passthroughArgs];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateChatId(): string {
|
|
74
|
+
return crypto.randomUUID().slice(0, 8);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Escape a string for safe interpolation inside a bash double-quoted string. */
|
|
78
|
+
function escBash(s: string): string {
|
|
79
|
+
return s.replace(/[\\"$`!]/g, "\\$&");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Escape a string for safe interpolation inside a PowerShell double-quoted string. */
|
|
83
|
+
function escPwsh(s: string): string {
|
|
84
|
+
return s.replace(/[`"$]/g, "`$&");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build a launcher script that preserves cwd and properly quotes args.
|
|
89
|
+
* This avoids shell-injection risks from passthrough args.
|
|
90
|
+
*/
|
|
91
|
+
function buildLauncherScript(
|
|
92
|
+
cmd: string,
|
|
93
|
+
args: string[],
|
|
94
|
+
projectRoot: string,
|
|
95
|
+
): { script: string; ext: string } {
|
|
96
|
+
const isWin = process.platform === "win32";
|
|
97
|
+
|
|
98
|
+
if (isWin) {
|
|
99
|
+
// PowerShell: use array splatting for safe arg passing
|
|
100
|
+
const argList = args.map((a) => `"${escPwsh(a)}"`).join(", ");
|
|
101
|
+
const script = [
|
|
102
|
+
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
103
|
+
argList.length > 0
|
|
104
|
+
? `& "${escPwsh(cmd)}" @(${argList})`
|
|
105
|
+
: `& "${escPwsh(cmd)}"`,
|
|
106
|
+
].join("\n");
|
|
107
|
+
return { script, ext: "ps1" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Bash: use proper quoting for each arg
|
|
111
|
+
const quotedArgs = args
|
|
112
|
+
.map((a) => `"${escBash(a)}"`)
|
|
113
|
+
.join(" ");
|
|
114
|
+
const script = [
|
|
115
|
+
"#!/bin/bash",
|
|
116
|
+
`cd "${escBash(projectRoot)}"`,
|
|
117
|
+
`exec "${escBash(cmd)}" ${quotedArgs}`,
|
|
118
|
+
].join("\n");
|
|
119
|
+
return { script, ext: "sh" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Chat Command Implementation
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spawn the native agent CLI as an interactive subprocess.
|
|
128
|
+
*
|
|
129
|
+
* When running inside a tmux/psmux session, the agent spawns inline
|
|
130
|
+
* in the current pane with inherited stdio.
|
|
131
|
+
*
|
|
132
|
+
* When running outside tmux, a new tmux session is created and
|
|
133
|
+
* attached so the agent benefits from multiplexer features.
|
|
134
|
+
*
|
|
135
|
+
* @param options - Chat command configuration options
|
|
136
|
+
* @returns Exit code from the agent process
|
|
137
|
+
*/
|
|
138
|
+
export async function chatCommand(options: ChatCommandOptions = {}): Promise<number> {
|
|
139
|
+
const { agentType, passthroughArgs } = options;
|
|
140
|
+
|
|
141
|
+
if (!agentType) {
|
|
142
|
+
throw new Error("agentType is required. Start chat with `atomic chat -a <agent>`.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const config = AGENT_CONFIG[agentType];
|
|
146
|
+
|
|
147
|
+
// Check the agent CLI is installed
|
|
148
|
+
if (!isCommandInstalled(config.cmd)) {
|
|
149
|
+
console.error(
|
|
150
|
+
`${COLORS.red}Error: '${config.cmd}' is not installed or not in PATH.${COLORS.reset}`
|
|
151
|
+
);
|
|
152
|
+
console.error(`Install it from: ${config.install_url}`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Preflight: global config sync ──
|
|
157
|
+
const projectRoot = process.cwd();
|
|
158
|
+
const configRoot = getConfigRoot();
|
|
159
|
+
|
|
160
|
+
await ensureAtomicGlobalAgentConfigs(configRoot);
|
|
161
|
+
|
|
162
|
+
// ── Build argv ──
|
|
163
|
+
const args = buildAgentArgs(agentType, passthroughArgs);
|
|
164
|
+
const cmd = [config.cmd, ...args];
|
|
165
|
+
|
|
166
|
+
// ── Inside tmux: spawn inline in the current pane ──
|
|
167
|
+
if (isInsideTmux()) {
|
|
168
|
+
return spawnDirect(cmd, projectRoot);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── No TTY: tmux attach requires a real terminal ──
|
|
172
|
+
if (!process.stdin.isTTY) {
|
|
173
|
+
return spawnDirect(cmd, projectRoot);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Ensure tmux is available ──
|
|
177
|
+
if (!isTmuxInstalled()) {
|
|
178
|
+
console.log("Terminal multiplexer not found. Installing...");
|
|
179
|
+
try {
|
|
180
|
+
await ensureTmuxInstalled();
|
|
181
|
+
resetMuxBinaryCache();
|
|
182
|
+
} catch {
|
|
183
|
+
// Fall through to check below
|
|
184
|
+
}
|
|
185
|
+
if (!isTmuxInstalled()) {
|
|
186
|
+
// No tmux available — fall back to direct spawn
|
|
187
|
+
return spawnDirect(cmd, projectRoot);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Build launcher script for safe arg/cwd handling ──
|
|
192
|
+
const chatId = generateChatId();
|
|
193
|
+
const windowName = `atomic-chat-${chatId}`;
|
|
194
|
+
|
|
195
|
+
const sessionsDir = join(homedir(), ".atomic", "sessions", "chat");
|
|
196
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
197
|
+
const { script, ext } = buildLauncherScript(config.cmd, args, projectRoot);
|
|
198
|
+
const launcherPath = join(sessionsDir, `${windowName}.${ext}`);
|
|
199
|
+
await writeFile(launcherPath, script, { mode: 0o755 });
|
|
200
|
+
|
|
201
|
+
const shellCmd = process.platform === "win32"
|
|
202
|
+
? `pwsh -NoProfile -File "${launcherPath}"`
|
|
203
|
+
: `bash "${launcherPath}"`;
|
|
204
|
+
|
|
205
|
+
// ── Outside tmux: create a new session and attach ──
|
|
206
|
+
try {
|
|
207
|
+
createSession(windowName, shellCmd, undefined, projectRoot);
|
|
208
|
+
|
|
209
|
+
const muxBinary = getMuxBinary() ?? "tmux";
|
|
210
|
+
const attachProc = Bun.spawn([muxBinary, "attach-session", "-t", windowName], {
|
|
211
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
212
|
+
});
|
|
213
|
+
const exitCode = await attachProc.exited;
|
|
214
|
+
|
|
215
|
+
// Clean up launcher
|
|
216
|
+
try { await rm(launcherPath, { force: true }); } catch {}
|
|
217
|
+
|
|
218
|
+
// If tmux attach itself failed (e.g. lost TTY), clean up and fall back
|
|
219
|
+
if (exitCode !== 0) {
|
|
220
|
+
try { killSession(windowName); } catch {}
|
|
221
|
+
return spawnDirect(cmd, projectRoot);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return exitCode;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
try { await rm(launcherPath, { force: true }); } catch {}
|
|
227
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
228
|
+
console.error(
|
|
229
|
+
`${COLORS.yellow}Warning: Failed to create tmux session (${message}). Falling back to direct spawn.${COLORS.reset}`
|
|
230
|
+
);
|
|
231
|
+
return spawnDirect(cmd, projectRoot);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Spawn the agent CLI directly with inherited stdio.
|
|
237
|
+
* Used when not inside tmux.
|
|
238
|
+
*/
|
|
239
|
+
async function spawnDirect(cmd: string[], projectRoot: string): Promise<number> {
|
|
240
|
+
const proc = Bun.spawn(cmd, {
|
|
241
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
242
|
+
cwd: projectRoot,
|
|
243
|
+
env: { ...process.env },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return await proc.exited;
|
|
247
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config command - Manage Atomic CLI configuration
|
|
3
|
+
*
|
|
4
|
+
* Usage: atomic config set <key> <value>
|
|
5
|
+
*
|
|
6
|
+
* Currently supported:
|
|
7
|
+
* atomic config set telemetry true|false
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { log } from "@clack/prompts";
|
|
11
|
+
import { setTelemetryEnabled } from "@/services/config/settings.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute the config command
|
|
15
|
+
*/
|
|
16
|
+
export async function configCommand(
|
|
17
|
+
subcommand: string | undefined,
|
|
18
|
+
key: string | undefined,
|
|
19
|
+
value: string | undefined
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
if (!subcommand) {
|
|
22
|
+
log.error("Missing subcommand. Usage: atomic config set <key> <value>");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (subcommand !== "set") {
|
|
27
|
+
log.error(`Unknown subcommand: ${subcommand}. Only 'set' is supported.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!key) {
|
|
32
|
+
log.error("Missing key. Usage: atomic config set <key> <value>");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (key !== "telemetry") {
|
|
37
|
+
log.error(`Unknown config key: ${key}. Only 'telemetry' is supported.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!value) {
|
|
42
|
+
log.error("Missing value. Usage: atomic config set telemetry <true|false>");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value !== "true" && value !== "false") {
|
|
47
|
+
log.error(`Invalid value: ${value}. Must be 'true' or 'false'.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const enabled = value === "true";
|
|
52
|
+
setTelemetryEnabled(enabled);
|
|
53
|
+
|
|
54
|
+
log.success(`Telemetry has been ${enabled ? "enabled" : "disabled"}.`);
|
|
55
|
+
}
|