@bastani/atomic 0.5.14-0 → 0.5.15-0
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/.claude/settings.json +24 -0
- package/.opencode/opencode.json +10 -0
- package/README.md +10 -58
- package/assets/settings.schema.json +29 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +4 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +4 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +4 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/services/config/atomic-config.d.ts +44 -0
- package/dist/services/config/atomic-config.d.ts.map +1 -0
- package/dist/services/config/definitions.d.ts +18 -13
- package/dist/services/config/definitions.d.ts.map +1 -1
- package/dist/services/config/index.d.ts +7 -0
- package/dist/services/config/index.d.ts.map +1 -0
- package/dist/services/config/settings-schema.d.ts +2 -0
- package/dist/services/config/settings-schema.d.ts.map +1 -0
- package/dist/services/system/copy.d.ts +8 -1
- package/dist/services/system/copy.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/cli.ts +1 -30
- package/src/commands/cli/chat/index.ts +21 -6
- package/src/commands/cli/init/index.ts +78 -323
- package/src/commands/cli/init/onboarding.ts +4 -10
- package/src/commands/cli/init/scm.ts +3 -34
- package/src/lib/common-ignore.ts +46 -0
- package/src/lib/merge.ts +28 -1
- package/src/sdk/runtime/executor.ts +85 -52
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +9 -4
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +12 -7
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +12 -7
- package/src/services/config/atomic-config.ts +95 -1
- package/src/services/config/atomic-global-config.ts +8 -21
- package/src/services/config/definitions.ts +41 -44
- package/src/services/config/settings.ts +2 -1
- package/src/services/system/agents.ts +2 -1
- package/src/services/system/copy.ts +18 -7
- package/src/services/system/skills.ts +3 -1
|
@@ -1,351 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Automatic project setup — replaces the interactive `atomic init` command.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Detects the repo's SCM, applies onboarding files (MCP configs, settings),
|
|
5
|
+
* registers the workspace as trusted, and installs SCM-specific skills.
|
|
6
|
+
*
|
|
7
|
+
* Called transparently during `atomic chat` preflight so users never need
|
|
8
|
+
* to think about initialization.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import {
|
|
9
|
-
intro,
|
|
10
|
-
outro,
|
|
11
|
-
select,
|
|
12
|
-
confirm,
|
|
13
|
-
spinner,
|
|
14
|
-
isCancel,
|
|
15
|
-
cancel,
|
|
16
|
-
note,
|
|
17
|
-
log,
|
|
18
|
-
} from "@clack/prompts";
|
|
19
11
|
import { join, resolve } from "node:path";
|
|
20
|
-
|
|
21
12
|
import {
|
|
22
13
|
AGENT_CONFIG,
|
|
23
14
|
type AgentKey,
|
|
24
|
-
getAgentKeys,
|
|
25
|
-
isValidAgent,
|
|
26
|
-
SCM_CONFIG,
|
|
27
|
-
SCM_SKILLS_BY_TYPE,
|
|
28
15
|
type SourceControlType,
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
SCM_SKILLS_BY_TYPE,
|
|
17
|
+
detectScmType,
|
|
31
18
|
} from "../../../services/config/index.ts";
|
|
32
19
|
import { pathExists } from "../../../services/system/copy.ts";
|
|
33
20
|
import { getConfigRoot } from "../../../services/config/config-path.ts";
|
|
34
|
-
import {
|
|
35
|
-
import { saveAtomicConfig } from "../../../services/config/atomic-config.ts";
|
|
21
|
+
import { getTemplateAgentFolder } from "../../../services/config/atomic-global-config.ts";
|
|
36
22
|
import { upsertTrustedWorkspacePath } from "../../../services/config/settings.ts";
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
getTemplateAgentFolder,
|
|
40
|
-
} from "../../../services/config/atomic-global-config.ts";
|
|
41
|
-
import {
|
|
42
|
-
installLocalScmSkills,
|
|
43
|
-
reconcileScmVariants,
|
|
44
|
-
syncProjectScmSkills,
|
|
45
|
-
} from "./scm.ts";
|
|
46
|
-
import {
|
|
47
|
-
applyManagedOnboardingFiles,
|
|
48
|
-
hasProjectOnboardingFiles,
|
|
49
|
-
} from "./onboarding.ts";
|
|
50
|
-
import { displayBlockBanner } from "../../../theme/logo.ts";
|
|
51
|
-
import { createPainter } from "../../../theme/colors.ts";
|
|
23
|
+
import { applyManagedOnboardingFiles } from "./onboarding.ts";
|
|
24
|
+
import { installLocalScmSkills, syncProjectScmSkills } from "./scm.ts";
|
|
52
25
|
|
|
53
26
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* When `initCommand` is invoked from a caller that sets
|
|
57
|
-
* `callerHandlesExit: true` (e.g. the auto-init path inside
|
|
58
|
-
* `chatCommand`), cancellation throws this error instead of calling
|
|
59
|
-
* `process.exit(0)` so the caller can decide what to do.
|
|
27
|
+
* Check whether all expected SCM skills are already present on disk.
|
|
60
28
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
29
|
+
async function areScmSkillsInstalled(
|
|
30
|
+
agentKey: AgentKey,
|
|
31
|
+
projectRoot: string,
|
|
32
|
+
scmType: SourceControlType,
|
|
33
|
+
): Promise<boolean> {
|
|
34
|
+
const skillNames = SCM_SKILLS_BY_TYPE[scmType];
|
|
35
|
+
const skillsDir = join(projectRoot, AGENT_CONFIG[agentKey].folder, "skills");
|
|
36
|
+
|
|
37
|
+
for (const name of skillNames) {
|
|
38
|
+
if (!(await pathExists(join(skillsDir, name)))) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
65
41
|
}
|
|
42
|
+
return true;
|
|
66
43
|
}
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
preSelectedAgent?: AgentKey;
|
|
71
|
-
/** Pre-selected source control type (skip SCM selection prompt) */
|
|
72
|
-
preSelectedScm?: SourceControlType;
|
|
73
|
-
configNotFoundMessage?: string;
|
|
74
|
-
/** Auto-confirm all prompts (non-interactive mode for CI/testing) */
|
|
75
|
-
yes?: boolean;
|
|
76
|
-
/**
|
|
77
|
-
* When true, throw `InitCancelledError` instead of calling
|
|
78
|
-
* `process.exit()` on user cancellation. This allows callers like
|
|
79
|
-
* `chatCommand` auto-init to handle the cancellation gracefully.
|
|
80
|
-
*/
|
|
81
|
-
callerHandlesExit?: boolean;
|
|
45
|
+
function isInstalledPackage(): boolean {
|
|
46
|
+
return import.meta.dir.includes("node_modules");
|
|
82
47
|
}
|
|
83
48
|
|
|
84
|
-
export {
|
|
85
|
-
applyManagedOnboardingFiles,
|
|
86
|
-
hasProjectOnboardingFiles,
|
|
87
|
-
} from "./onboarding.ts";
|
|
88
|
-
export {
|
|
89
|
-
getScmPrefix,
|
|
90
|
-
reconcileScmVariants,
|
|
91
|
-
} from "./scm.ts";
|
|
92
|
-
|
|
93
49
|
/**
|
|
94
|
-
*
|
|
50
|
+
* Ensure the project is configured for the given agent.
|
|
51
|
+
*
|
|
52
|
+
* Idempotent — safe to call on every `atomic chat` invocation. Expensive
|
|
53
|
+
* operations (skill installation via `bunx skills add`) are skipped when
|
|
54
|
+
* the skills are already present on disk. Onboarding file merges are
|
|
55
|
+
* always applied since they are cheap and self-healing.
|
|
56
|
+
*
|
|
57
|
+
* Errors in skill installation are swallowed so the agent can still launch.
|
|
95
58
|
*/
|
|
96
|
-
export async function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
intro(paint("accent", "Configure agent skills & source control", { bold: true }));
|
|
116
|
-
|
|
117
|
-
if (configNotFoundMessage) {
|
|
118
|
-
log.info(configNotFoundMessage);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── Agent selection ────────────────────────────────────────────────
|
|
122
|
-
let agentKey: AgentKey;
|
|
123
|
-
|
|
124
|
-
if (options.preSelectedAgent) {
|
|
125
|
-
if (!isValidAgent(options.preSelectedAgent)) {
|
|
126
|
-
cancel(`Unknown agent: ${options.preSelectedAgent}`);
|
|
127
|
-
exitOrThrow(1, `Unknown agent: ${options.preSelectedAgent}`);
|
|
128
|
-
}
|
|
129
|
-
agentKey = options.preSelectedAgent;
|
|
130
|
-
log.info(`${paint("accent", "→")} Agent: ${paint("text", AGENT_CONFIG[agentKey].name, { bold: true })}`);
|
|
131
|
-
} else {
|
|
132
|
-
const agentKeys = getAgentKeys();
|
|
133
|
-
const agentOptions = agentKeys.map((key) => ({
|
|
134
|
-
value: key,
|
|
135
|
-
label: AGENT_CONFIG[key].name,
|
|
136
|
-
hint: AGENT_CONFIG[key].install_url.replace("https://", ""),
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
const selectedAgent = await select({
|
|
140
|
-
message: "Which coding agent?",
|
|
141
|
-
options: agentOptions,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
if (isCancel(selectedAgent)) {
|
|
145
|
-
cancel("Cancelled.");
|
|
146
|
-
exitOrThrow(0);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
agentKey = selectedAgent as AgentKey;
|
|
150
|
-
}
|
|
151
|
-
const agent = AGENT_CONFIG[agentKey];
|
|
152
|
-
const targetDir = process.cwd();
|
|
153
|
-
const autoConfirm = options.yes ?? false;
|
|
154
|
-
|
|
155
|
-
// ── SCM selection ──────────────────────────────────────────────────
|
|
156
|
-
let scmType: SourceControlType;
|
|
157
|
-
|
|
158
|
-
if (options.preSelectedScm) {
|
|
159
|
-
if (!isValidScm(options.preSelectedScm)) {
|
|
160
|
-
cancel(`Unknown source control: ${options.preSelectedScm}`);
|
|
161
|
-
exitOrThrow(1, `Unknown source control: ${options.preSelectedScm}`);
|
|
162
|
-
}
|
|
163
|
-
scmType = options.preSelectedScm;
|
|
164
|
-
log.info(`${paint("accent", "→")} SCM: ${paint("text", SCM_CONFIG[scmType].displayName, { bold: true })}`);
|
|
165
|
-
} else if (autoConfirm) {
|
|
166
|
-
scmType = "github";
|
|
167
|
-
log.info(`${paint("accent", "→")} SCM: ${paint("text", "GitHub / Git", { bold: true })} ${paint("dim", "(default)")}`);
|
|
168
|
-
} else {
|
|
169
|
-
const scmOptions = getScmKeys().map((key) => ({
|
|
170
|
-
value: key,
|
|
171
|
-
label: SCM_CONFIG[key].displayName,
|
|
172
|
-
hint: `${SCM_CONFIG[key].cliTool} + ${SCM_CONFIG[key].reviewSystem}`,
|
|
173
|
-
}));
|
|
174
|
-
|
|
175
|
-
const selectedScm = await select({
|
|
176
|
-
message: "Which source control?",
|
|
177
|
-
options: scmOptions,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (isCancel(selectedScm)) {
|
|
181
|
-
cancel("Cancelled.");
|
|
182
|
-
exitOrThrow(0);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
scmType = selectedScm as SourceControlType;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Sapling-specific warning
|
|
189
|
-
if (scmType === "sapling") {
|
|
190
|
-
const arcconfigPath = join(targetDir, ".arcconfig");
|
|
191
|
-
const hasArcconfig = await pathExists(arcconfigPath);
|
|
192
|
-
|
|
193
|
-
if (!hasArcconfig) {
|
|
194
|
-
log.warn(
|
|
195
|
-
`Sapling + Phabricator requires ${paint("text", ".arcconfig", { bold: true })} in your repo root.\n` +
|
|
196
|
-
`${paint("dim", "See: https://www.phacility.com/phabricator/")}`
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── Preflight summary ──────────────────────────────────────────────
|
|
202
|
-
const targetFolder = join(targetDir, agent.folder);
|
|
203
|
-
const folderExists = await pathExists(targetFolder);
|
|
204
|
-
const configAction = folderExists ? "update" : "create";
|
|
205
|
-
|
|
206
|
-
if (!autoConfirm) {
|
|
207
|
-
const summaryLines = [
|
|
208
|
-
`${paint("dim", "Agent")} ${paint("text", agent.name, { bold: true })}`,
|
|
209
|
-
`${paint("dim", "SCM")} ${paint("text", SCM_CONFIG[scmType].displayName, { bold: true })}`,
|
|
210
|
-
`${paint("dim", "Target")} ${paint("text", targetDir)}`,
|
|
211
|
-
`${paint("dim", "Action")} ${paint(folderExists ? "warning" : "success", configAction)}`,
|
|
212
|
-
];
|
|
213
|
-
note(summaryLines.join("\n"), paint("accent", "Setup", { bold: true }));
|
|
214
|
-
|
|
215
|
-
const shouldProceed = await confirm({
|
|
216
|
-
message: folderExists
|
|
217
|
-
? `${agent.folder} exists — update source control skills?`
|
|
218
|
-
: "Proceed with setup?",
|
|
219
|
-
initialValue: true,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
if (isCancel(shouldProceed) || !shouldProceed) {
|
|
223
|
-
cancel("Cancelled.");
|
|
224
|
-
exitOrThrow(0);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ── Configure ──────────────────────────────────────────────────────
|
|
229
|
-
const s = spinner();
|
|
230
|
-
s.start("Configuring skills…");
|
|
231
|
-
|
|
232
|
-
let skillsInstalled = false;
|
|
233
|
-
let skillsSkipReason = "";
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const configRoot = getConfigRoot();
|
|
237
|
-
|
|
238
|
-
await ensureAtomicGlobalAgentConfigs(configRoot);
|
|
239
|
-
|
|
240
|
-
const templateAgentFolder = getTemplateAgentFolder(agentKey);
|
|
241
|
-
const sourceSkillsDir = join(configRoot, templateAgentFolder, "skills");
|
|
242
|
-
const targetSkillsDir = join(targetFolder, "skills");
|
|
243
|
-
|
|
244
|
-
// Best-effort template copy: source checkouts still carry the bundled
|
|
245
|
-
// gh-*/sl-* skill templates, but binary and npm installs no longer do
|
|
246
|
-
// (they live in the skills CLI repo). `installLocalScmSkills` below
|
|
247
|
-
// handles the binary/npm case by invoking `bunx skills add` — so a zero
|
|
248
|
-
// copy here is not an error, just a signal that the template isn't
|
|
249
|
-
// bundled for this install type.
|
|
250
|
-
await syncProjectScmSkills({
|
|
251
|
-
scmType,
|
|
252
|
-
sourceSkillsDir,
|
|
253
|
-
targetSkillsDir,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Keep SCM-specific managed command/skill variants aligned with selected SCM
|
|
257
|
-
await reconcileScmVariants({
|
|
258
|
-
scmType,
|
|
259
|
-
agentFolder: agent.folder,
|
|
260
|
-
skillsSubfolder: "skills",
|
|
261
|
-
targetDir,
|
|
262
|
-
configRoot,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
await applyManagedOnboardingFiles(agentKey, targetDir, configRoot);
|
|
266
|
-
|
|
267
|
-
// Save SCM selection to .atomic/settings.json
|
|
268
|
-
await saveAtomicConfig(targetDir, {
|
|
269
|
-
scm: scmType,
|
|
270
|
-
});
|
|
271
|
-
await upsertTrustedWorkspacePath(resolve(targetDir), agentKey);
|
|
272
|
-
|
|
273
|
-
s.stop(paint("success", "✓", { bold: true }) + " Skills configured");
|
|
274
|
-
|
|
275
|
-
// Install SCM-specific skill variants locally for the active agent via
|
|
276
|
-
// `bunx skills add` (best-effort: a failure is surfaced as a warning).
|
|
277
|
-
//
|
|
278
|
-
// Source checkouts already have the bundled skills on disk and the
|
|
279
|
-
// template-copy above has placed the selected variants into `targetDir`;
|
|
280
|
-
// skip the network-backed skills CLI in that case to keep dev iteration
|
|
281
|
-
// fast and offline-friendly.
|
|
282
|
-
if (import.meta.dir.includes("node_modules")) {
|
|
283
|
-
const skillsToInstall = SCM_SKILLS_BY_TYPE[scmType];
|
|
284
|
-
const skillsLabel = skillsToInstall.join(", ");
|
|
285
|
-
const skillsSpinner = spinner();
|
|
286
|
-
skillsSpinner.start(
|
|
287
|
-
`Installing ${paint("text", skillsLabel, { bold: true })}…`,
|
|
288
|
-
);
|
|
289
|
-
const skillsResult = await installLocalScmSkills({
|
|
290
|
-
scmType,
|
|
59
|
+
export async function ensureProjectSetup(
|
|
60
|
+
agentKey: AgentKey,
|
|
61
|
+
projectRoot: string,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const configRoot = getConfigRoot();
|
|
64
|
+
const detectedScm = await detectScmType(projectRoot);
|
|
65
|
+
|
|
66
|
+
// Apply onboarding files (idempotent merge, SCM-gated entries handled internally)
|
|
67
|
+
await applyManagedOnboardingFiles(agentKey, projectRoot, configRoot);
|
|
68
|
+
|
|
69
|
+
// Register trusted workspace
|
|
70
|
+
await upsertTrustedWorkspacePath(resolve(projectRoot), agentKey);
|
|
71
|
+
|
|
72
|
+
// Install SCM skills if detected and not yet present (best-effort)
|
|
73
|
+
if (detectedScm) {
|
|
74
|
+
try {
|
|
75
|
+
const alreadyInstalled = await areScmSkillsInstalled(
|
|
291
76
|
agentKey,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (skillsResult.success) {
|
|
295
|
-
skillsInstalled = true;
|
|
296
|
-
skillsSpinner.stop(
|
|
297
|
-
paint("success", "✓", { bold: true }) + ` ${skillsLabel} installed`,
|
|
298
|
-
);
|
|
299
|
-
} else {
|
|
300
|
-
skillsSkipReason = skillsResult.details;
|
|
301
|
-
skillsSpinner.stop(
|
|
302
|
-
paint("warning", "○") + ` ${skillsLabel} skipped ${paint("dim", `(${skillsResult.details})`)}`,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
} catch (error) {
|
|
307
|
-
s.stop(paint("error", "✗", { bold: true }) + " Configuration failed");
|
|
308
|
-
console.error(
|
|
309
|
-
error instanceof Error ? error.message : "Unknown error occurred"
|
|
310
|
-
);
|
|
311
|
-
exitOrThrow(1, error instanceof Error ? error.message : "Unknown error occurred");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// ── WSL warning ────────────────────────────────────────────────────
|
|
315
|
-
if (isWindows() && !isWslInstalled()) {
|
|
316
|
-
log.warn(
|
|
317
|
-
`WSL not detected. Some scripts may require it.\n` +
|
|
318
|
-
`${paint("dim", WSL_INSTALL_URL)}`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ── Summary ────────────────────────────────────────────────────────
|
|
323
|
-
const resultLines: string[] = [];
|
|
324
|
-
resultLines.push(
|
|
325
|
-
`${paint("success", "✓")} ${agent.name} skills ${paint("dim", "→")} ${paint("text", agent.folder + "/skills")}`,
|
|
326
|
-
);
|
|
327
|
-
resultLines.push(
|
|
328
|
-
`${paint("success", "✓")} SCM workflow ${paint("dim", "→")} ${paint("text", SCM_CONFIG[scmType].displayName)}`,
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
if (import.meta.dir.includes("node_modules")) {
|
|
332
|
-
if (skillsInstalled) {
|
|
333
|
-
resultLines.push(
|
|
334
|
-
`${paint("success", "✓")} Local skills installed`,
|
|
335
|
-
);
|
|
336
|
-
} else {
|
|
337
|
-
resultLines.push(
|
|
338
|
-
`${paint("warning", "○")} Local skills skipped ${paint("dim", skillsSkipReason ? `(${skillsSkipReason})` : "")}`,
|
|
77
|
+
projectRoot,
|
|
78
|
+
detectedScm,
|
|
339
79
|
);
|
|
80
|
+
if (!alreadyInstalled) {
|
|
81
|
+
if (isInstalledPackage()) {
|
|
82
|
+
// npm/bunx install: fetch via the skills CLI
|
|
83
|
+
await installLocalScmSkills({
|
|
84
|
+
scmType: detectedScm,
|
|
85
|
+
agentKey,
|
|
86
|
+
cwd: projectRoot,
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
// Source checkout: copy from bundled templates
|
|
90
|
+
const templateFolder = getTemplateAgentFolder(agentKey);
|
|
91
|
+
await syncProjectScmSkills({
|
|
92
|
+
scmType: detectedScm,
|
|
93
|
+
sourceSkillsDir: join(configRoot, templateFolder, "skills"),
|
|
94
|
+
targetSkillsDir: join(
|
|
95
|
+
projectRoot,
|
|
96
|
+
AGENT_CONFIG[agentKey].folder,
|
|
97
|
+
"skills",
|
|
98
|
+
),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Skills installation is best-effort — don't block the agent launch
|
|
340
104
|
}
|
|
341
105
|
}
|
|
342
|
-
|
|
343
|
-
resultLines.push("");
|
|
344
|
-
resultLines.push(
|
|
345
|
-
`${paint("accent", "→")} Run ${paint("text", agent.cmd, { bold: true })} to start the agent`,
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
note(resultLines.join("\n"), paint("success", "Ready", { bold: true }));
|
|
349
|
-
|
|
350
|
-
outro(paint("dim", "Happy coding ⚛"));
|
|
351
106
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
import { AGENT_CONFIG, type AgentKey } from "../../../services/config/index.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { pathExists } from "../../../services/system/copy.ts";
|
|
4
|
+
import { syncJsonFile } from "../../../lib/merge.ts";
|
|
5
5
|
|
|
6
6
|
export async function applyManagedOnboardingFiles(
|
|
7
7
|
agentKey: AgentKey,
|
|
@@ -17,13 +17,7 @@ export async function applyManagedOnboardingFiles(
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const destinationPath = join(projectRoot, managedFile.destination);
|
|
20
|
-
await
|
|
21
|
-
|
|
22
|
-
if (managedFile.merge && (await pathExists(destinationPath))) {
|
|
23
|
-
await mergeJsonFile(sourcePath, destinationPath);
|
|
24
|
-
} else {
|
|
25
|
-
await copyFile(sourcePath, destinationPath);
|
|
26
|
-
}
|
|
20
|
+
await syncJsonFile(sourcePath, destinationPath, managedFile.merge);
|
|
27
21
|
}
|
|
28
22
|
}
|
|
29
23
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { copyDir, pathExists, ensureDir } from "../../../services/system/copy.ts";
|
|
4
|
+
import { createCommonIgnoreFilter } from "../../../lib/common-ignore.ts";
|
|
5
5
|
import {
|
|
6
6
|
SCM_SKILLS_BY_TYPE,
|
|
7
7
|
type AgentKey,
|
|
@@ -50,37 +50,6 @@ export async function reconcileScmVariants(options: ReconcileScmVariantsOptions)
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
interface CopyDirPreservingOptions {
|
|
54
|
-
exclude?: string[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function copyDirPreserving(
|
|
58
|
-
src: string,
|
|
59
|
-
dest: string,
|
|
60
|
-
options: CopyDirPreservingOptions = {}
|
|
61
|
-
): Promise<void> {
|
|
62
|
-
const { exclude = [] } = options;
|
|
63
|
-
|
|
64
|
-
await ensureDir(dest);
|
|
65
|
-
|
|
66
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
67
|
-
const oppositeExt = getOppositeScriptExtension();
|
|
68
|
-
|
|
69
|
-
for (const entry of entries) {
|
|
70
|
-
const srcPath = join(src, entry.name);
|
|
71
|
-
const destPath = join(dest, entry.name);
|
|
72
|
-
|
|
73
|
-
if (exclude.includes(entry.name)) continue;
|
|
74
|
-
if (entry.name.endsWith(oppositeExt)) continue;
|
|
75
|
-
|
|
76
|
-
if (entry.isDirectory()) {
|
|
77
|
-
await copyDirPreserving(srcPath, destPath, options);
|
|
78
|
-
} else {
|
|
79
|
-
await copyFile(srcPath, destPath);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
53
|
export interface SyncProjectScmSkillsOptions {
|
|
85
54
|
scmType: SourceControlType;
|
|
86
55
|
sourceSkillsDir: string;
|
|
@@ -106,7 +75,7 @@ export async function syncProjectScmSkills(options: SyncProjectScmSkillsOptions)
|
|
|
106
75
|
|
|
107
76
|
const srcPath = join(sourceSkillsDir, entry.name);
|
|
108
77
|
const destPath = join(targetSkillsDir, entry.name);
|
|
109
|
-
await
|
|
78
|
+
await copyDir(srcPath, destPath, { ignoreFilter: createCommonIgnoreFilter() });
|
|
110
79
|
copiedCount += 1;
|
|
111
80
|
}
|
|
112
81
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common gitignore-style filter for agent config copy operations.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `ignore` package (gitignore-compatible glob matching) so that
|
|
5
|
+
* per-agent `exclude` lists only need to contain meaningful,
|
|
6
|
+
* domain-specific entries — OS junk, dependency dirs, lockfiles, and
|
|
7
|
+
* similar noise are handled here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import ignore, { type Ignore } from "ignore";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Patterns that should never be copied during agent config operations.
|
|
14
|
+
*
|
|
15
|
+
* These mirror the most common entries found in a typical `.gitignore`
|
|
16
|
+
* and cover OS-generated files, dependency directories, lockfiles, and
|
|
17
|
+
* build artifacts.
|
|
18
|
+
*/
|
|
19
|
+
const COMMON_IGNORE_PATTERNS: readonly string[] = [
|
|
20
|
+
// macOS
|
|
21
|
+
".DS_Store",
|
|
22
|
+
"__MACOSX/",
|
|
23
|
+
"._*",
|
|
24
|
+
|
|
25
|
+
// Windows
|
|
26
|
+
"Thumbs.db",
|
|
27
|
+
|
|
28
|
+
// Dependencies
|
|
29
|
+
"node_modules/",
|
|
30
|
+
|
|
31
|
+
// Lockfiles
|
|
32
|
+
"bun.lock",
|
|
33
|
+
|
|
34
|
+
// Logs
|
|
35
|
+
"*.log",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create an {@link Ignore} filter pre-loaded with common gitignore
|
|
40
|
+
* patterns. Pass the returned instance as `ignoreFilter` in
|
|
41
|
+
* {@link CopyOptions} so agent-specific `exclude` lists stay focused
|
|
42
|
+
* on meaningful entries.
|
|
43
|
+
*/
|
|
44
|
+
export function createCommonIgnoreFilter(): Ignore {
|
|
45
|
+
return ignore().add(COMMON_IGNORE_PATTERNS);
|
|
46
|
+
}
|
package/src/lib/merge.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Utilities for merging JSON configuration files
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { resolve } from "node:path";
|
|
5
|
+
import { resolve, dirname } from "node:path";
|
|
6
|
+
import { ensureDir, pathExists, copyFile } from "../services/system/copy.ts";
|
|
6
7
|
|
|
7
8
|
type McpConfig = Record<string, unknown>;
|
|
8
9
|
|
|
@@ -49,3 +50,29 @@ export async function mergeJsonFile(
|
|
|
49
50
|
|
|
50
51
|
await Bun.write(destPath, JSON.stringify(mergedConfig, null, 2) + "\n");
|
|
51
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sync a JSON file from source to destination.
|
|
56
|
+
*
|
|
57
|
+
* - Creates the destination's parent directory if needed
|
|
58
|
+
* - When the destination exists and `merge` is true (the default),
|
|
59
|
+
* merges via {@link mergeJsonFile} (source keys win, server maps
|
|
60
|
+
* are merged individually)
|
|
61
|
+
* - Otherwise copies the source as-is
|
|
62
|
+
*
|
|
63
|
+
* This is the single entry-point for the merge-or-copy pattern used
|
|
64
|
+
* by both project-level onboarding and global config sync.
|
|
65
|
+
*/
|
|
66
|
+
export async function syncJsonFile(
|
|
67
|
+
srcPath: string,
|
|
68
|
+
destPath: string,
|
|
69
|
+
merge: boolean = true,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
await ensureDir(dirname(destPath));
|
|
72
|
+
|
|
73
|
+
if (merge && (await pathExists(destPath))) {
|
|
74
|
+
await mergeJsonFile(srcPath, destPath);
|
|
75
|
+
} else {
|
|
76
|
+
await copyFile(srcPath, destPath);
|
|
77
|
+
}
|
|
78
|
+
}
|