@bastani/atomic 0.5.18 → 0.5.19
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/.agents/skills/workflow-creator/SKILL.md +110 -1
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +10 -0
- package/.mcp.json +9 -0
- package/.opencode/opencode.json +5 -2
- package/README.md +394 -645
- package/assets/settings.schema.json +0 -20
- package/dist/sdk/components/attached-statusline.d.ts +13 -0
- package/dist/sdk/components/attached-statusline.d.ts.map +1 -0
- package/dist/sdk/components/header.d.ts.map +1 -1
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
- package/dist/sdk/components/statusline.d.ts +1 -3
- package/dist/sdk/components/statusline.d.ts.map +1 -1
- package/dist/sdk/providers/claude.d.ts +16 -5
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +63 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/tmux.d.ts +0 -9
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/services/config/atomic-config.d.ts +1 -7
- package/dist/services/config/atomic-config.d.ts.map +1 -1
- package/dist/services/config/definitions.d.ts +0 -45
- package/dist/services/config/definitions.d.ts.map +1 -1
- package/dist/services/config/index.d.ts +1 -1
- package/dist/theme/colors.d.ts +33 -0
- package/dist/theme/colors.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/cli.ts +16 -1
- package/src/commands/cli/chat/index.ts +1 -1
- package/src/commands/cli/footer.tsx +118 -0
- package/src/commands/cli/init/index.ts +6 -89
- package/src/commands/cli/workflow-command.test.ts +146 -0
- package/src/commands/cli/workflow.ts +43 -7
- package/src/completions/bash.ts +3 -8
- package/src/completions/fish.ts +1 -3
- package/src/completions/powershell.ts +1 -17
- package/src/completions/zsh.ts +0 -2
- package/src/scripts/bundle-configs.ts +0 -12
- package/src/sdk/components/attached-statusline.tsx +33 -0
- package/src/sdk/components/header.tsx +16 -2
- package/src/sdk/components/session-graph-panel.tsx +10 -51
- package/src/sdk/components/statusline.tsx +0 -17
- package/src/sdk/providers/claude.ts +179 -177
- package/src/sdk/runtime/executor-entry.ts +3 -1
- package/src/sdk/runtime/executor.test.ts +292 -1
- package/src/sdk/runtime/executor.ts +222 -1
- package/src/sdk/runtime/tmux.conf +35 -4
- package/src/sdk/runtime/tmux.ts +0 -22
- package/src/services/config/atomic-config.ts +1 -14
- package/src/services/config/definitions.ts +1 -102
- package/src/services/config/index.ts +1 -1
- package/src/services/config/settings.ts +2 -65
- package/src/services/system/skills.ts +2 -19
- package/src/commands/cli/init/scm.ts +0 -175
package/src/sdk/runtime/tmux.ts
CHANGED
|
@@ -30,28 +30,6 @@ export type TmuxResult =
|
|
|
30
30
|
// Core tmux primitives
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Default status-bar values — must match tmux.conf.
|
|
35
|
-
// Centralised here so restore logic in session-graph-panel stays in sync.
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
export const TMUX_DEFAULT_STATUS_LEFT = " ";
|
|
39
|
-
export const TMUX_DEFAULT_STATUS_LEFT_LENGTH = "10";
|
|
40
|
-
export const TMUX_DEFAULT_STATUS_RIGHT = " #{session_name} | %H:%M ";
|
|
41
|
-
export const TMUX_DEFAULT_STATUS_RIGHT_LENGTH = "60";
|
|
42
|
-
|
|
43
|
-
// Attached-mode status bar — agent list via tmux window list + shortcut hints.
|
|
44
|
-
// The window-status formats hide window 0 (orchestrator) and style agent names.
|
|
45
|
-
// tmux natively highlights the current window, so no React state sync is needed
|
|
46
|
-
// for agent cycling via Ctrl+\.
|
|
47
|
-
export const TMUX_ATTACHED_STATUS_RIGHT =
|
|
48
|
-
"#[fg=#cdd6f4]ctrl+g #[fg=#6c7086]graph #[fg=#585b70]\u00b7 #[fg=#cdd6f4]ctrl+\\ #[fg=#6c7086]next ";
|
|
49
|
-
export const TMUX_ATTACHED_STATUS_RIGHT_LENGTH = "40";
|
|
50
|
-
export const TMUX_ATTACHED_WINDOW_FMT =
|
|
51
|
-
"#{?#{==:#{window_index},0},, #W }";
|
|
52
|
-
export const TMUX_ATTACHED_WINDOW_STYLE = "fg=#6c7086";
|
|
53
|
-
export const TMUX_ATTACHED_WINDOW_CURRENT_STYLE = "fg=#cdd6f4,bold";
|
|
54
|
-
|
|
55
33
|
// ---------------------------------------------------------------------------
|
|
56
34
|
// Core tmux primitives
|
|
57
35
|
// ---------------------------------------------------------------------------
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { join, dirname } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { type
|
|
12
|
+
import { type AgentKey, type ProviderOverrides } from "./index.ts";
|
|
13
13
|
import { SETTINGS_SCHEMA_URL } from "./settings-schema.ts";
|
|
14
14
|
import { ensureDir } from "../system/copy.ts";
|
|
15
15
|
|
|
@@ -22,8 +22,6 @@ const SETTINGS_FILENAME = "settings.json";
|
|
|
22
22
|
export interface AtomicConfig {
|
|
23
23
|
/** Version of config schema */
|
|
24
24
|
version?: number;
|
|
25
|
-
/** Selected source control type */
|
|
26
|
-
scm?: SourceControlType;
|
|
27
25
|
/** Timestamp of last init */
|
|
28
26
|
lastUpdated?: string;
|
|
29
27
|
/** Per-provider overrides for chatFlags and envVars */
|
|
@@ -89,11 +87,9 @@ function pickAtomicConfig(record: JsonRecord | null): AtomicConfig | null {
|
|
|
89
87
|
|
|
90
88
|
const config: AtomicConfig = {};
|
|
91
89
|
const version = record.version;
|
|
92
|
-
const scm = record.scm;
|
|
93
90
|
const lastUpdated = record.lastUpdated;
|
|
94
91
|
|
|
95
92
|
if (typeof version === "number") config.version = version;
|
|
96
|
-
if (typeof scm === "string") config.scm = scm as SourceControlType;
|
|
97
93
|
if (typeof lastUpdated === "string") config.lastUpdated = lastUpdated;
|
|
98
94
|
|
|
99
95
|
const providers = pickProviders(record.providers);
|
|
@@ -137,7 +133,6 @@ function mergeConfigs(...configs: Array<AtomicConfig | null>): AtomicConfig | nu
|
|
|
137
133
|
for (const config of configs) {
|
|
138
134
|
if (!config) continue;
|
|
139
135
|
if (config.version !== undefined) merged.version = config.version;
|
|
140
|
-
if (config.scm !== undefined) merged.scm = config.scm;
|
|
141
136
|
if (config.lastUpdated !== undefined) merged.lastUpdated = config.lastUpdated;
|
|
142
137
|
|
|
143
138
|
if (config.providers) {
|
|
@@ -192,14 +187,6 @@ export async function saveAtomicConfig(
|
|
|
192
187
|
await Bun.write(localPath, JSON.stringify(nextSettings, null, 2) + "\n");
|
|
193
188
|
}
|
|
194
189
|
|
|
195
|
-
/**
|
|
196
|
-
* Get selected SCM using local override + global fallback.
|
|
197
|
-
*/
|
|
198
|
-
export async function getSelectedScm(projectDir: string): Promise<SourceControlType | null> {
|
|
199
|
-
const config = await readAtomicConfig(projectDir);
|
|
200
|
-
return config?.scm ?? null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
190
|
/**
|
|
204
191
|
* Resolve provider overrides from global + local settings (local wins).
|
|
205
192
|
*
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
* Agent configuration definitions for atomic CLI
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { stat } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
5
|
export interface AgentConfig {
|
|
9
6
|
/** Display name for the agent */
|
|
10
7
|
name: string;
|
|
@@ -88,13 +85,7 @@ export const AGENT_CONFIG: Record<AgentKey, AgentConfig> = {
|
|
|
88
85
|
install_url:
|
|
89
86
|
"https://github.com/github/copilot-cli?tab=readme-ov-file#installation",
|
|
90
87
|
exclude: ["workflows", "dependabot.yml"],
|
|
91
|
-
onboarding_files: [
|
|
92
|
-
{
|
|
93
|
-
source: ".mcp.json",
|
|
94
|
-
destination: ".mcp.json",
|
|
95
|
-
merge: true,
|
|
96
|
-
},
|
|
97
|
-
],
|
|
88
|
+
onboarding_files: [],
|
|
98
89
|
},
|
|
99
90
|
};
|
|
100
91
|
|
|
@@ -122,95 +113,3 @@ export function getAgentConfig(key: AgentKey): AgentConfig {
|
|
|
122
113
|
export function getAgentKeys(): AgentKey[] {
|
|
123
114
|
return [...AGENT_KEYS];
|
|
124
115
|
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Source Control Management (SCM) configuration definitions
|
|
128
|
-
*/
|
|
129
|
-
|
|
130
|
-
/** SCM keys for iteration */
|
|
131
|
-
const SCM_KEYS = ["github", "sapling"] as const;
|
|
132
|
-
|
|
133
|
-
/** Supported source control types — derived from SCM_KEYS tuple. */
|
|
134
|
-
export type SourceControlType = (typeof SCM_KEYS)[number];
|
|
135
|
-
|
|
136
|
-
export interface ScmConfig {
|
|
137
|
-
/** Display name for prompts */
|
|
138
|
-
displayName: string;
|
|
139
|
-
/** Primary CLI tool (git or sl) */
|
|
140
|
-
cliTool: string;
|
|
141
|
-
/** Code review system (github, phabricator) */
|
|
142
|
-
reviewSystem: string;
|
|
143
|
-
/** Directory marker used to detect this SCM in a repo (e.g. `.git`, `.sl`) */
|
|
144
|
-
detectDir: string;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export const SCM_CONFIG: Record<SourceControlType, ScmConfig> = {
|
|
148
|
-
github: {
|
|
149
|
-
displayName: "GitHub / Git",
|
|
150
|
-
cliTool: "git",
|
|
151
|
-
reviewSystem: "github",
|
|
152
|
-
detectDir: ".git",
|
|
153
|
-
},
|
|
154
|
-
sapling: {
|
|
155
|
-
displayName: "Sapling + Phabricator",
|
|
156
|
-
cliTool: "sl",
|
|
157
|
-
reviewSystem: "phabricator",
|
|
158
|
-
detectDir: ".sl",
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* SCM-variant skill names, grouped by source control type.
|
|
164
|
-
*
|
|
165
|
-
* These are the skills that `installGlobalSkills` removes from the global
|
|
166
|
-
* scope after the initial install, and that `installLocalScmSkills`
|
|
167
|
-
* re-installs per-project based on the user's selected SCM. Passed to
|
|
168
|
-
* `npx skills add --skill <name>` as explicit names (the skills CLI does
|
|
169
|
-
* not support glob patterns like `gh-*`).
|
|
170
|
-
*/
|
|
171
|
-
export const SCM_SKILLS_BY_TYPE: Record<SourceControlType, readonly string[]> =
|
|
172
|
-
{
|
|
173
|
-
github: ["gh-commit", "gh-create-pr"],
|
|
174
|
-
sapling: ["sl-commit", "sl-submit-diff"],
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/** Flat list of every SCM-variant skill across all source control types. */
|
|
178
|
-
export const ALL_SCM_SKILLS: readonly string[] = [
|
|
179
|
-
...SCM_SKILLS_BY_TYPE.github,
|
|
180
|
-
...SCM_SKILLS_BY_TYPE.sapling,
|
|
181
|
-
];
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get all SCM keys for iteration
|
|
185
|
-
*/
|
|
186
|
-
export function getScmKeys(): SourceControlType[] {
|
|
187
|
-
return [...SCM_KEYS];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Check if a string is a valid SCM type
|
|
192
|
-
*/
|
|
193
|
-
export function isValidScm(key: string): key is SourceControlType {
|
|
194
|
-
return key in SCM_CONFIG;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Detect the SCM type by looking for marker directories in `projectRoot`.
|
|
199
|
-
*
|
|
200
|
-
* Checks each {@link ScmConfig.detectDir} (e.g. `.git`, `.sl`) and returns
|
|
201
|
-
* the first match. Returns `null` when no known marker is found.
|
|
202
|
-
*/
|
|
203
|
-
export async function detectScmType(
|
|
204
|
-
projectRoot: string,
|
|
205
|
-
): Promise<SourceControlType | null> {
|
|
206
|
-
for (const key of getScmKeys()) {
|
|
207
|
-
const markerPath = join(projectRoot, SCM_CONFIG[key].detectDir);
|
|
208
|
-
try {
|
|
209
|
-
await stat(markerPath);
|
|
210
|
-
return key;
|
|
211
|
-
} catch {
|
|
212
|
-
// marker not found — try next
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
@@ -9,24 +9,17 @@
|
|
|
9
9
|
* The --model CLI flag takes precedence over both (handled at call site).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { join, dirname
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { SETTINGS_SCHEMA_URL } from "./settings-schema.ts";
|
|
15
15
|
import { ensureDir } from "../system/copy.ts";
|
|
16
16
|
import { errorMessage } from "../../sdk/errors.ts";
|
|
17
|
-
import type { AgentKey, ProviderOverrides
|
|
18
|
-
|
|
19
|
-
export interface TrustedPathEntry {
|
|
20
|
-
workspacePath: string;
|
|
21
|
-
provider: AgentKey;
|
|
22
|
-
}
|
|
17
|
+
import type { AgentKey, ProviderOverrides } from "./definitions.ts";
|
|
23
18
|
|
|
24
19
|
interface AtomicSettings {
|
|
25
20
|
$schema?: string;
|
|
26
|
-
scm?: SourceControlType;
|
|
27
21
|
version?: number;
|
|
28
22
|
lastUpdated?: string;
|
|
29
|
-
trustedPaths?: TrustedPathEntry[];
|
|
30
23
|
telemetryEnabled?: boolean;
|
|
31
24
|
providers?: Partial<Record<AgentKey, ProviderOverrides>>;
|
|
32
25
|
}
|
|
@@ -59,62 +52,6 @@ async function writeGlobalSettings(settings: AtomicSettings): Promise<void> {
|
|
|
59
52
|
await Bun.write(path, JSON.stringify(settings, null, 2));
|
|
60
53
|
}
|
|
61
54
|
|
|
62
|
-
function normalizeTrustedPathEntry(entry: TrustedPathEntry): TrustedPathEntry {
|
|
63
|
-
return {
|
|
64
|
-
workspacePath: resolve(entry.workspacePath),
|
|
65
|
-
provider: entry.provider,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeTrustedPaths(entries: TrustedPathEntry[] | undefined): TrustedPathEntry[] {
|
|
70
|
-
const deduped = new Map<string, TrustedPathEntry>();
|
|
71
|
-
|
|
72
|
-
for (const entry of entries ?? []) {
|
|
73
|
-
if (
|
|
74
|
-
typeof entry.workspacePath !== "string" ||
|
|
75
|
-
typeof entry.provider !== "string"
|
|
76
|
-
) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const normalizedEntry = normalizeTrustedPathEntry(entry);
|
|
81
|
-
deduped.set(
|
|
82
|
-
`${normalizedEntry.provider}:${normalizedEntry.workspacePath}`,
|
|
83
|
-
normalizedEntry,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return Array.from(deduped.values());
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function isTrustedWorkspacePath(
|
|
91
|
-
workspacePath: string,
|
|
92
|
-
provider: AgentKey,
|
|
93
|
-
): Promise<boolean> {
|
|
94
|
-
const settings = await loadSettingsFile(globalSettingsPath());
|
|
95
|
-
const normalizedWorkspacePath = resolve(workspacePath);
|
|
96
|
-
|
|
97
|
-
return normalizeTrustedPaths(settings.trustedPaths).some((entry) =>
|
|
98
|
-
entry.provider === provider && entry.workspacePath === normalizedWorkspacePath
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function upsertTrustedWorkspacePath(
|
|
103
|
-
workspacePath: string,
|
|
104
|
-
provider: AgentKey,
|
|
105
|
-
): Promise<void> {
|
|
106
|
-
try {
|
|
107
|
-
const settings = await loadSettingsFile(globalSettingsPath());
|
|
108
|
-
settings.trustedPaths = normalizeTrustedPaths([
|
|
109
|
-
...(settings.trustedPaths ?? []),
|
|
110
|
-
{ workspacePath, provider },
|
|
111
|
-
]);
|
|
112
|
-
await writeGlobalSettings(settings);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.warn(`[settings] failed to upsert trusted path: ${errorMessage(e)}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
55
|
/**
|
|
119
56
|
* Set telemetry enabled/disabled in global settings.
|
|
120
57
|
*/
|
|
@@ -4,20 +4,10 @@
|
|
|
4
4
|
* Copies bundled agent skills from the installed package into the
|
|
5
5
|
* provider-native global skill roots, mirroring the merge-copy approach
|
|
6
6
|
* used by {@link installGlobalAgents} for agent configs.
|
|
7
|
-
*
|
|
8
|
-
* Previously this ran `npx skills add <repo>` at runtime, which cloned
|
|
9
|
-
* the entire git repo on every version bump. Now the skills ship inside
|
|
10
|
-
* the npm package (`.agents/skills/`) and are copied locally — no network
|
|
11
|
-
* required, no `npx`/`bunx` dependency.
|
|
12
|
-
*
|
|
13
|
-
* SCM-variant skills (gh-commit, gh-create-pr, sl-commit, sl-submit-diff)
|
|
14
|
-
* are excluded from the global install; `atomic init` installs them
|
|
15
|
-
* per-project based on the user's selected SCM + active agent.
|
|
16
7
|
*/
|
|
17
8
|
|
|
18
9
|
import { join } from "node:path";
|
|
19
10
|
import { homedir } from "node:os";
|
|
20
|
-
import { ALL_SCM_SKILLS } from "../config/index.ts";
|
|
21
11
|
import { createCommonIgnoreFilter } from "../../lib/common-ignore.ts";
|
|
22
12
|
import { copyDir, pathExists } from "./copy.ts";
|
|
23
13
|
|
|
@@ -47,12 +37,8 @@ const SKILL_DEST_DIRS = [
|
|
|
47
37
|
".claude/skills",
|
|
48
38
|
] as const;
|
|
49
39
|
|
|
50
|
-
/** The set of SCM skill names to exclude from global installation. */
|
|
51
|
-
const SCM_SKILL_SET = new Set<string>(ALL_SCM_SKILLS);
|
|
52
|
-
|
|
53
40
|
/**
|
|
54
|
-
* Copy bundled skills to the global skill directories
|
|
55
|
-
* SCM-variant skills that are installed per-project by `atomic init`.
|
|
41
|
+
* Copy bundled skills to the global skill directories.
|
|
56
42
|
*/
|
|
57
43
|
export async function installGlobalSkills(): Promise<void> {
|
|
58
44
|
const src = join(packageRoot(), ".agents", "skills");
|
|
@@ -62,14 +48,11 @@ export async function installGlobalSkills(): Promise<void> {
|
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
const home = homeRoot();
|
|
65
|
-
|
|
66
|
-
// Build the exclusion list from SCM skill names
|
|
67
|
-
const exclude = [...SCM_SKILL_SET];
|
|
68
51
|
const ignoreFilter = createCommonIgnoreFilter();
|
|
69
52
|
|
|
70
53
|
await Promise.all(
|
|
71
54
|
SKILL_DEST_DIRS.map((rel) =>
|
|
72
|
-
copyDir(src, join(home, rel), {
|
|
55
|
+
copyDir(src, join(home, rel), { ignoreFilter }),
|
|
73
56
|
),
|
|
74
57
|
);
|
|
75
58
|
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { readdir } from "node:fs/promises";
|
|
3
|
-
import { copyDir, pathExists, ensureDir } from "../../../services/system/copy.ts";
|
|
4
|
-
import { createCommonIgnoreFilter } from "../../../lib/common-ignore.ts";
|
|
5
|
-
import {
|
|
6
|
-
SCM_SKILLS_BY_TYPE,
|
|
7
|
-
type AgentKey,
|
|
8
|
-
type SourceControlType,
|
|
9
|
-
} from "../../../services/config/index.ts";
|
|
10
|
-
|
|
11
|
-
export const SCM_PREFIX_BY_TYPE: Record<SourceControlType, "gh-" | "sl-"> = {
|
|
12
|
-
github: "gh-",
|
|
13
|
-
sapling: "sl-",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export function getScmPrefix(scmType: SourceControlType): "gh-" | "sl-" {
|
|
17
|
-
return SCM_PREFIX_BY_TYPE[scmType];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isManagedScmEntry(name: string): boolean {
|
|
21
|
-
return name.startsWith("gh-") || name.startsWith("sl-");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ReconcileScmVariantsOptions {
|
|
25
|
-
scmType: SourceControlType;
|
|
26
|
-
agentFolder: string;
|
|
27
|
-
skillsSubfolder: string;
|
|
28
|
-
targetDir: string;
|
|
29
|
-
configRoot: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function reconcileScmVariants(options: ReconcileScmVariantsOptions): Promise<void> {
|
|
33
|
-
const { agentFolder, skillsSubfolder, targetDir, configRoot } = options;
|
|
34
|
-
const srcDir = join(configRoot, agentFolder, skillsSubfolder);
|
|
35
|
-
const destDir = join(targetDir, agentFolder, skillsSubfolder);
|
|
36
|
-
|
|
37
|
-
if (!(await pathExists(srcDir)) || !(await pathExists(destDir))) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const sourceEntries = await readdir(srcDir, { withFileTypes: true });
|
|
42
|
-
const managedEntries = sourceEntries.filter((entry) => isManagedScmEntry(entry.name));
|
|
43
|
-
|
|
44
|
-
if (process.env.DEBUG === "1" && managedEntries.length > 0) {
|
|
45
|
-
console.log(
|
|
46
|
-
`[DEBUG] Preserving existing managed SCM variants in ${destDir}: ${managedEntries
|
|
47
|
-
.map((entry) => entry.name)
|
|
48
|
-
.join(", ")}`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SyncProjectScmSkillsOptions {
|
|
54
|
-
scmType: SourceControlType;
|
|
55
|
-
sourceSkillsDir: string;
|
|
56
|
-
targetSkillsDir: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function syncProjectScmSkills(options: SyncProjectScmSkillsOptions): Promise<number> {
|
|
60
|
-
const { scmType, sourceSkillsDir, targetSkillsDir } = options;
|
|
61
|
-
const selectedPrefix = getScmPrefix(scmType);
|
|
62
|
-
|
|
63
|
-
if (!(await pathExists(sourceSkillsDir))) {
|
|
64
|
-
return 0;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
await ensureDir(targetSkillsDir);
|
|
68
|
-
|
|
69
|
-
const entries = await readdir(sourceSkillsDir, { withFileTypes: true });
|
|
70
|
-
let copiedCount = 0;
|
|
71
|
-
|
|
72
|
-
for (const entry of entries) {
|
|
73
|
-
if (!entry.isDirectory()) continue;
|
|
74
|
-
if (!entry.name.startsWith(selectedPrefix)) continue;
|
|
75
|
-
|
|
76
|
-
const srcPath = join(sourceSkillsDir, entry.name);
|
|
77
|
-
const destPath = join(targetSkillsDir, entry.name);
|
|
78
|
-
await copyDir(srcPath, destPath, { ignoreFilter: createCommonIgnoreFilter() });
|
|
79
|
-
copiedCount += 1;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return copiedCount;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Skills-CLI agent identifiers (match `bunx skills -a <value>`). */
|
|
86
|
-
const SKILLS_AGENT_BY_KEY: Record<AgentKey, string> = {
|
|
87
|
-
claude: "claude-code",
|
|
88
|
-
opencode: "opencode",
|
|
89
|
-
copilot: "github-copilot",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const SKILLS_REPO = "https://github.com/flora131/atomic.git";
|
|
93
|
-
|
|
94
|
-
export interface InstallLocalScmSkillsOptions {
|
|
95
|
-
scmType: SourceControlType;
|
|
96
|
-
agentKey: AgentKey;
|
|
97
|
-
/** The directory to run `bunx skills add` in (the project root). */
|
|
98
|
-
cwd: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface InstallLocalScmSkillsResult {
|
|
102
|
-
success: boolean;
|
|
103
|
-
/** The explicit skill names that were requested (e.g. `["gh-commit", "gh-create-pr"]`). */
|
|
104
|
-
skills: readonly string[];
|
|
105
|
-
/** Non-empty when `success` is false. */
|
|
106
|
-
details: string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Install the SCM skill variants (e.g. `gh-commit`, `gh-create-pr` for
|
|
111
|
-
* GitHub) locally into the current project via `bunx skills add`. The `-g`
|
|
112
|
-
* flag is intentionally omitted so the skills are installed per-project
|
|
113
|
-
* (in the given `cwd`).
|
|
114
|
-
*
|
|
115
|
-
* Each skill is passed explicitly with `--skill <name>` — the skills CLI
|
|
116
|
-
* does not support glob patterns like `gh-*`, which would either fail or
|
|
117
|
-
* fall back to installing the entire skill set.
|
|
118
|
-
*
|
|
119
|
-
* This is best-effort: callers should treat a failed result as a warning,
|
|
120
|
-
* not as a fatal error.
|
|
121
|
-
*/
|
|
122
|
-
export async function installLocalScmSkills(
|
|
123
|
-
options: InstallLocalScmSkillsOptions,
|
|
124
|
-
): Promise<InstallLocalScmSkillsResult> {
|
|
125
|
-
const { scmType, agentKey, cwd } = options;
|
|
126
|
-
|
|
127
|
-
const skills = SCM_SKILLS_BY_TYPE[scmType];
|
|
128
|
-
|
|
129
|
-
const bunxPath = Bun.which("bunx");
|
|
130
|
-
if (!bunxPath) {
|
|
131
|
-
return { success: false, skills, details: "bunx not found on PATH" };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const agentFlag = SKILLS_AGENT_BY_KEY[agentKey];
|
|
135
|
-
const skillFlags = skills.flatMap((skill) => ["--skill", skill]);
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const proc = Bun.spawn({
|
|
139
|
-
cmd: [
|
|
140
|
-
bunxPath,
|
|
141
|
-
"skills",
|
|
142
|
-
"add",
|
|
143
|
-
SKILLS_REPO,
|
|
144
|
-
...skillFlags,
|
|
145
|
-
"-a",
|
|
146
|
-
agentFlag,
|
|
147
|
-
"-y",
|
|
148
|
-
],
|
|
149
|
-
cwd,
|
|
150
|
-
stdout: "pipe",
|
|
151
|
-
stderr: "pipe",
|
|
152
|
-
env: process.env,
|
|
153
|
-
});
|
|
154
|
-
const [stderr, stdout, exitCode] = await Promise.all([
|
|
155
|
-
new Response(proc.stderr).text(),
|
|
156
|
-
new Response(proc.stdout).text(),
|
|
157
|
-
proc.exited,
|
|
158
|
-
]);
|
|
159
|
-
if (exitCode === 0) {
|
|
160
|
-
return { success: true, skills, details: "" };
|
|
161
|
-
}
|
|
162
|
-
const details = stderr.trim().length > 0 ? stderr.trim() : stdout.trim();
|
|
163
|
-
return {
|
|
164
|
-
success: false,
|
|
165
|
-
skills,
|
|
166
|
-
details: details || `exit code ${exitCode}`,
|
|
167
|
-
};
|
|
168
|
-
} catch (error) {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
skills,
|
|
172
|
-
details: error instanceof Error ? error.message : String(error),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
}
|