@tofrankie/agents-sync 0.0.1 → 0.0.3
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/CHANGELOG.md +9 -0
- package/dist/cli.mjs +90 -55
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## agents-sync@0.0.3 (2026-04-22)
|
|
4
|
+
|
|
5
|
+
- Update CLI interaction
|
|
6
|
+
|
|
7
|
+
## agents-sync@0.0.2 (2026-04-22)
|
|
8
|
+
|
|
9
|
+
- Fix config loader and sync engine bugs
|
|
10
|
+
- User home config file name is `.agents-sync.config.*`
|
|
11
|
+
|
|
3
12
|
## agents-sync@0.0.1 (2026-04-22)
|
|
4
13
|
|
|
5
14
|
- Initial release
|
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import process
|
|
2
|
+
import process from "node:process";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import c from "ansis";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { loadConfig } from "c12";
|
|
9
|
-
import jiti from "jiti";
|
|
10
8
|
import os from "node:os";
|
|
9
|
+
import { loadConfig } from "c12";
|
|
10
|
+
import { createJiti } from "jiti";
|
|
11
11
|
//#region src/core/agent-profiles.ts
|
|
12
12
|
const AGENT_PROFILES = {
|
|
13
13
|
cursor: {
|
|
@@ -93,22 +93,20 @@ const DEFAULTS = {
|
|
|
93
93
|
source: "~/.agents",
|
|
94
94
|
targetAgent: "cursor"
|
|
95
95
|
};
|
|
96
|
-
function pickFirst(value) {
|
|
97
|
-
if (Array.isArray(value)) return value[0];
|
|
98
|
-
return value;
|
|
99
|
-
}
|
|
100
96
|
async function loadUserConfig(cwd, configPath) {
|
|
101
97
|
if (configPath) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
return loaded
|
|
98
|
+
const resolvedConfig = path.resolve(cwd, configPath);
|
|
99
|
+
const loaded = await createJiti(resolvedConfig, { interopDefault: true }).import(resolvedConfig, { default: true });
|
|
100
|
+
return loaded && typeof loaded === "object" ? loaded : {};
|
|
105
101
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
102
|
+
const projectDir = path.resolve(cwd);
|
|
103
|
+
const homeDir = path.resolve(os.homedir());
|
|
104
|
+
if (projectDir === homeDir) return loadHomeAgentsSyncConfig(homeDir);
|
|
105
|
+
const projectLayer = await loadAgentsSyncConfigFromDir(projectDir);
|
|
106
|
+
return {
|
|
107
|
+
...await loadHomeAgentsSyncConfig(homeDir),
|
|
108
|
+
...projectLayer
|
|
109
|
+
};
|
|
112
110
|
}
|
|
113
111
|
function resolveOptions(config, cli) {
|
|
114
112
|
const sourceFromConfig = pickFirst(config.sourceDir);
|
|
@@ -122,26 +120,37 @@ function resolveOptions(config, cli) {
|
|
|
122
120
|
yes: Boolean(cli.yes)
|
|
123
121
|
};
|
|
124
122
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
await fs.access(filePath);
|
|
130
|
-
return true;
|
|
131
|
-
} catch {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
123
|
+
function pickFirst(value) {
|
|
124
|
+
if (Array.isArray(value)) return value[0];
|
|
125
|
+
return value;
|
|
134
126
|
}
|
|
135
|
-
async function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
127
|
+
async function loadAgentsSyncConfigFromDir(dir) {
|
|
128
|
+
return (await loadC12AgentsSync(dir)).config ?? {};
|
|
129
|
+
}
|
|
130
|
+
/** Standard `agents-sync.config.*` (+ c12 rc / layers for that name). */
|
|
131
|
+
async function loadC12AgentsSync(dir) {
|
|
132
|
+
return loadConfig({
|
|
133
|
+
cwd: dir,
|
|
134
|
+
name: "agents-sync",
|
|
135
|
+
defaults: {},
|
|
136
|
+
jitiOptions: { interopDefault: true }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* User home only: hidden `.agents-sync.config.*` (not `agents-sync.config.*`).
|
|
141
|
+
* Keeps `name: 'agents-sync'` so `~/.agents-syncrc` still merges when present.
|
|
142
|
+
*/
|
|
143
|
+
async function loadHomeAgentsSyncConfig(homeDir) {
|
|
144
|
+
return (await loadConfig({
|
|
145
|
+
cwd: homeDir,
|
|
146
|
+
name: "agents-sync",
|
|
147
|
+
configFile: ".agents-sync.config",
|
|
148
|
+
defaults: {},
|
|
149
|
+
jitiOptions: { interopDefault: true }
|
|
150
|
+
})).config ?? {};
|
|
144
151
|
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/core/sync-engine.ts
|
|
145
154
|
async function discoverSkills(source) {
|
|
146
155
|
const candidates = [path.join(source, "skills"), source];
|
|
147
156
|
const found = [];
|
|
@@ -232,6 +241,24 @@ function renderMcpConfigByAgent(model, targetAgent) {
|
|
|
232
241
|
if (targetAgent !== "cursor") throw new Error(`unsupported mcp target: ${targetAgent}`);
|
|
233
242
|
return `${JSON.stringify(model, null, 2)}\n`;
|
|
234
243
|
}
|
|
244
|
+
async function exists(filePath) {
|
|
245
|
+
try {
|
|
246
|
+
await fs.access(filePath);
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function listFilesRecursively(root) {
|
|
253
|
+
const out = [];
|
|
254
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
const abs = path.join(root, entry.name);
|
|
257
|
+
if (entry.isDirectory()) out.push(...await listFilesRecursively(abs));
|
|
258
|
+
else out.push(abs);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
235
262
|
//#endregion
|
|
236
263
|
//#region src/cli/commands/mcp.ts
|
|
237
264
|
async function runMcpCommand(cwd, options) {
|
|
@@ -242,18 +269,18 @@ async function runMcpCommand(cwd, options) {
|
|
|
242
269
|
}
|
|
243
270
|
const sourceFile = await resolveMcpSourceFile(runtime.source);
|
|
244
271
|
if (!sourceFile) {
|
|
245
|
-
printSkip(`No MCP config file found in ${runtime.source}.`);
|
|
272
|
+
printSkip(`No MCP config file found in ${c.dim(runtime.source)}.`);
|
|
246
273
|
return;
|
|
247
274
|
}
|
|
248
275
|
const rendered = renderMcpConfigByAgent(await normalizeMcpConfig(sourceFile), runtime.targetAgent);
|
|
249
276
|
const targetFile = resolveMcpTargetFile(cwd);
|
|
250
277
|
if (runtime.dryRun) {
|
|
251
|
-
process.stdout.write(`dry-run: MCP sync target => ${targetFile}\n`);
|
|
278
|
+
process.stdout.write(`dry-run: MCP sync target => ${c.dim(targetFile)}\n`);
|
|
252
279
|
return;
|
|
253
280
|
}
|
|
254
281
|
await fs.mkdir(path.dirname(targetFile), { recursive: true });
|
|
255
282
|
await fs.writeFile(targetFile, rendered, "utf8");
|
|
256
|
-
process.stdout.write(`done: MCP synced to ${targetFile}\n`);
|
|
283
|
+
process.stdout.write(`done: MCP synced to ${c.dim(targetFile)}\n`);
|
|
257
284
|
}
|
|
258
285
|
//#endregion
|
|
259
286
|
//#region src/cli/commands/rule.ts
|
|
@@ -265,7 +292,7 @@ async function runRuleCommand(cwd, options) {
|
|
|
265
292
|
}
|
|
266
293
|
const files = await discoverRuleFiles(runtime.source);
|
|
267
294
|
if (files.length === 0) {
|
|
268
|
-
printSkip(`No rule files found in ${runtime.source}.`);
|
|
295
|
+
printSkip(`No rule files found in ${c.dim(runtime.source)}.`);
|
|
269
296
|
return;
|
|
270
297
|
}
|
|
271
298
|
printSummary("rules", await copyRuleFiles(files, resolveRulesTargetDir(cwd), {
|
|
@@ -275,28 +302,36 @@ async function runRuleCommand(cwd, options) {
|
|
|
275
302
|
}
|
|
276
303
|
//#endregion
|
|
277
304
|
//#region src/cli/commands/skill.ts
|
|
305
|
+
/** Multiselect sentinel: not a filesystem path. When selected, every skill is synced. */
|
|
306
|
+
const MULTISELECT_ALL_SKILLS = "__agents-sync:all-skills__";
|
|
278
307
|
async function runSkillCommand(cwd, options) {
|
|
279
308
|
const runtime = resolveOptions(await loadUserConfig(cwd, options.config), options);
|
|
280
309
|
const skillDirs = await discoverSkills(runtime.source);
|
|
281
310
|
if (skillDirs.length === 0) {
|
|
282
|
-
printSkip(`No syncable skills found in ${runtime.source}.`);
|
|
311
|
+
printSkip(`No syncable skills found in ${c.dim(runtime.source)}.`);
|
|
283
312
|
return;
|
|
284
313
|
}
|
|
285
314
|
let selected = skillDirs;
|
|
286
315
|
if (!runtime.yes) {
|
|
316
|
+
const skillOptions = skillDirs.map((dir) => ({
|
|
317
|
+
label: path.basename(dir),
|
|
318
|
+
value: dir
|
|
319
|
+
}));
|
|
320
|
+
const options = skillDirs.length > 1 ? [{
|
|
321
|
+
label: "All skills",
|
|
322
|
+
value: MULTISELECT_ALL_SKILLS
|
|
323
|
+
}, ...skillOptions] : skillOptions;
|
|
287
324
|
const picked = await p.multiselect({
|
|
288
|
-
message: `Select ${c.green("skills")} to sync`,
|
|
289
|
-
options
|
|
290
|
-
label: path.basename(dir),
|
|
291
|
-
value: dir
|
|
292
|
-
})),
|
|
325
|
+
message: `Select one or more ${c.green("skills")} to sync from ${c.dim(runtime.source)}`,
|
|
326
|
+
options,
|
|
293
327
|
required: false
|
|
294
328
|
});
|
|
295
329
|
if (p.isCancel(picked)) {
|
|
296
330
|
p.cancel("Operation canceled.");
|
|
297
331
|
return;
|
|
298
332
|
}
|
|
299
|
-
|
|
333
|
+
const values = picked;
|
|
334
|
+
selected = values.includes(MULTISELECT_ALL_SKILLS) ? skillDirs : values;
|
|
300
335
|
}
|
|
301
336
|
if (selected.length === 0) {
|
|
302
337
|
printSkip("No skills selected.");
|
|
@@ -328,31 +363,31 @@ async function runSkillCommand(cwd, options) {
|
|
|
328
363
|
//#endregion
|
|
329
364
|
//#region package.json
|
|
330
365
|
var name = "@tofrankie/agents-sync";
|
|
331
|
-
var version = "0.0.
|
|
366
|
+
var version = "0.0.3";
|
|
332
367
|
//#endregion
|
|
333
368
|
//#region src/cli/index.ts
|
|
334
369
|
main().catch((error) => {
|
|
335
|
-
process
|
|
336
|
-
process
|
|
337
|
-
process
|
|
338
|
-
process
|
|
370
|
+
process.stdout.write("\n");
|
|
371
|
+
process.stderr.write(`${String(error)}\n`);
|
|
372
|
+
process.stdout.write("\n");
|
|
373
|
+
process.exitCode = 1;
|
|
339
374
|
});
|
|
340
375
|
async function main() {
|
|
341
376
|
const program = new Command();
|
|
342
377
|
program.name("agents-sync").description("Sync skills, cursor rules, and cursor mcp config from a source folder.").version(version, "-v, --version", "output the version number").hook("preAction", () => {
|
|
343
|
-
process
|
|
378
|
+
process.stdout.write("\n");
|
|
344
379
|
p.intro(`${name} ${c.dim(`v${version}`)}`);
|
|
345
380
|
});
|
|
346
381
|
applySharedOptions(program.command("skill").description("sync skills")).action(async (options) => {
|
|
347
|
-
await runSkillCommand(process
|
|
382
|
+
await runSkillCommand(process.cwd(), options);
|
|
348
383
|
});
|
|
349
384
|
applySharedOptions(program.command("rule").description("sync rules (cursor only)")).action(async (options) => {
|
|
350
|
-
await runRuleCommand(process
|
|
385
|
+
await runRuleCommand(process.cwd(), options);
|
|
351
386
|
});
|
|
352
387
|
applySharedOptions(program.command("mcp").description("sync mcp config (cursor only)")).action(async (options) => {
|
|
353
|
-
await runMcpCommand(process
|
|
388
|
+
await runMcpCommand(process.cwd(), options);
|
|
354
389
|
});
|
|
355
|
-
await program.parseAsync(process
|
|
390
|
+
await program.parseAsync(process.argv);
|
|
356
391
|
}
|
|
357
392
|
function applySharedOptions(cmd) {
|
|
358
393
|
return cmd.option("-s, --source <path>", "source path, default ~/.agents").option("-t, --target-agent <agent>", "target agent, default cursor").option("-c, --config <path>", "config file path").option("--dry-run", "show planned operations without writing files").option("-y, --yes", "skip prompts and overwrite conflicts");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tofrankie/agents-sync",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Sync skills, rules, and MCP from a configurable source into your project",
|
|
6
6
|
"author": "Frankie <1426203851@qq.com> (https://github.com/tofrankie)",
|
|
7
7
|
"license": "MIT",
|
|
@@ -83,6 +83,6 @@
|
|
|
83
83
|
"typecheck": "tsc --noEmit",
|
|
84
84
|
"test": "vitest run",
|
|
85
85
|
"publint": "publint",
|
|
86
|
-
"github:release": "tfr"
|
|
86
|
+
"github:release": "tfr --package @tofrankie/agents-sync"
|
|
87
87
|
}
|
|
88
88
|
}
|