agents-sync 0.1.0 → 0.2.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/dist/cli.js CHANGED
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import "./index.js";
2
+ const args = process.argv.slice(2);
3
+ if (args.includes("init")) {
4
+ const { runInit } = await import("./init.js");
5
+ await runInit({ cwd: process.cwd() });
6
+ }
7
+ else {
8
+ await import("./index.js");
9
+ }
10
+ export {};
package/dist/index.js CHANGED
@@ -1,129 +1,35 @@
1
1
  import chokidar from "chokidar";
2
- import path from "node:path";
3
- import fs from "node:fs/promises";
4
2
  import { existsSync } from "node:fs";
5
- import { loadConfig, resolveTargetAgentsDir } from "./config.js";
6
- import { parseAgentsMarkdown, inferKindFromPath } from "./schema.js";
7
- import { translators } from "./translation/index.js";
8
- const CWD = process.cwd();
9
- const CONFIG_PATH = path.join(CWD, "agents.config.json");
10
- const ensureDir = async (dirPath) => {
11
- await fs.mkdir(dirPath, { recursive: true });
12
- };
13
- const writeIfChanged = async (filePath, content) => {
14
- let existing = null;
15
- try {
16
- existing = await fs.readFile(filePath, "utf8");
17
- }
18
- catch {
19
- existing = null;
20
- }
21
- if (existing === content)
22
- return;
23
- await ensureDir(path.dirname(filePath));
24
- await fs.writeFile(filePath, content, "utf8");
25
- };
26
- const walkFiles = async (dirPath) => {
27
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
28
- const files = [];
29
- for (const entry of entries) {
30
- const full = path.join(dirPath, entry.name);
31
- if (entry.isDirectory()) {
32
- files.push(...(await walkFiles(full)));
33
- }
34
- else if (entry.isFile()) {
35
- files.push(full);
36
- }
37
- }
38
- return files;
39
- };
3
+ import { createSyncContext, ensureTargetDirs, walkFiles, syncSourceFile, removeSourceFile, syncRootDoc } from "./sync.js";
40
4
  const main = async () => {
41
- const config = await loadConfig(CONFIG_PATH);
42
- const sourceDir = path.resolve(CWD, config.sourceDir);
43
- const rootDoc = path.resolve(CWD, config.rootDoc);
44
- const targetEntries = Object.entries(config.targets);
45
- for (const [, target] of targetEntries) {
46
- await ensureDir(resolveTargetAgentsDir(CWD, target, config.agentsSubdir));
47
- }
48
- const syncSourceFile = async (filePath) => {
49
- const rel = path.relative(sourceDir, filePath);
50
- const kind = inferKindFromPath(rel);
51
- if (!kind)
52
- return;
53
- const id = path.basename(rel, path.extname(rel));
54
- const content = await fs.readFile(filePath, "utf8");
55
- const doc = parseAgentsMarkdown(content, { kind, id });
56
- await Promise.all(targetEntries.map(async ([targetKey, target]) => {
57
- const translator = translators[targetKey];
58
- if (!translator)
59
- return;
60
- const baseDir = resolveTargetAgentsDir(CWD, target, config.agentsSubdir);
61
- const subdir = translator.subdir(doc, target);
62
- const ext = translator.fileExtension(doc, target);
63
- const outPath = path.join(baseDir, subdir, `${doc.id}${ext}`);
64
- const translated = translator.renderDoc(doc, target);
65
- await writeIfChanged(outPath, translated);
66
- }));
67
- };
68
- const removeSourceFile = async (filePath) => {
69
- const rel = path.relative(sourceDir, filePath);
70
- const kind = inferKindFromPath(rel);
71
- if (!kind)
72
- return;
73
- const id = path.basename(rel, path.extname(rel));
74
- const doc = parseAgentsMarkdown("", { kind, id });
75
- await Promise.all(targetEntries.map(async ([targetKey, target]) => {
76
- const translator = translators[targetKey];
77
- if (!translator)
78
- return;
79
- const baseDir = resolveTargetAgentsDir(CWD, target, config.agentsSubdir);
80
- const subdir = translator.subdir(doc, target);
81
- const ext = translator.fileExtension(doc, target);
82
- const outPath = path.join(baseDir, subdir, `${id}${ext}`);
83
- await fs.rm(outPath, { force: true });
84
- }));
85
- };
86
- const syncRootDoc = async () => {
87
- if (!existsSync(rootDoc))
88
- return;
89
- const content = await fs.readFile(rootDoc, "utf8");
90
- const doc = parseAgentsMarkdown(content, { kind: "config", id: "root" });
91
- const rootTargets = config.rootTargets;
92
- await Promise.all(Object.entries(rootTargets).map(async ([targetKey, outName]) => {
93
- const translator = translators[targetKey];
94
- if (!translator)
95
- return;
96
- const outPath = path.resolve(CWD, outName);
97
- const translated = translator.renderDoc(doc, config.targets[targetKey]);
98
- await writeIfChanged(outPath, translated);
99
- }));
100
- };
101
- if (existsSync(sourceDir)) {
102
- const files = await walkFiles(sourceDir);
5
+ const ctx = await createSyncContext(process.cwd());
6
+ await ensureTargetDirs(ctx);
7
+ if (existsSync(ctx.sourceDir)) {
8
+ const files = await walkFiles(ctx.sourceDir);
103
9
  for (const filePath of files) {
104
- await syncSourceFile(filePath);
10
+ await syncSourceFile(ctx, filePath);
105
11
  }
106
12
  }
107
- await syncRootDoc();
108
- const agentsWatcher = chokidar.watch(sourceDir, {
13
+ await syncRootDoc(ctx);
14
+ const agentsWatcher = chokidar.watch(ctx.sourceDir, {
109
15
  ignoreInitial: true,
110
16
  awaitWriteFinish: {
111
17
  stabilityThreshold: 150,
112
18
  pollInterval: 50
113
19
  }
114
20
  });
115
- agentsWatcher.on("add", syncSourceFile);
116
- agentsWatcher.on("change", syncSourceFile);
117
- agentsWatcher.on("unlink", removeSourceFile);
118
- const rootWatcher = chokidar.watch(rootDoc, {
21
+ agentsWatcher.on("add", (filePath) => syncSourceFile(ctx, filePath));
22
+ agentsWatcher.on("change", (filePath) => syncSourceFile(ctx, filePath));
23
+ agentsWatcher.on("unlink", (filePath) => removeSourceFile(ctx, filePath));
24
+ const rootWatcher = chokidar.watch(ctx.rootDoc, {
119
25
  ignoreInitial: true,
120
26
  awaitWriteFinish: {
121
27
  stabilityThreshold: 150,
122
28
  pollInterval: 50
123
29
  }
124
30
  });
125
- rootWatcher.on("add", syncRootDoc);
126
- rootWatcher.on("change", syncRootDoc);
31
+ rootWatcher.on("add", () => syncRootDoc(ctx));
32
+ rootWatcher.on("change", () => syncRootDoc(ctx));
127
33
  process.stdout.write("agents-sync watching .agents and AGENTS.MD\n");
128
34
  };
129
35
  main().catch((err) => {
package/dist/init.js ADDED
@@ -0,0 +1,62 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ensureDir, syncOnce } from "./sync.js";
6
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const DEFAULT_CONFIG_PATH = path.join(PACKAGE_ROOT, "agents.config.json");
8
+ const DEFAULT_SCHEMA_PATH = path.join(PACKAGE_ROOT, "SCHEMA.MD");
9
+ const DEFAULT_SCHEMA_COMMAND = `\`\`\`agents
10
+ id: schema
11
+ type: command
12
+ title: Schema Guide
13
+ description: How to write and update the canonical agents schema.
14
+ aliases: agents-schema
15
+ \`\`\`
16
+
17
+ Use \`.agents/SCHEMA.MD\` as the source of truth for the canonical schema.
18
+
19
+ When you create or update commands, hooks, or skills, follow the format
20
+ defined in \`.agents/SCHEMA.MD\`. If you need to extend or change the schema,
21
+ update \`.agents/SCHEMA.MD\` and keep examples consistent.`;
22
+ const writeIfMissing = async (filePath, content) => {
23
+ if (existsSync(filePath))
24
+ return false;
25
+ await ensureDir(path.dirname(filePath));
26
+ await fs.writeFile(filePath, content, "utf8");
27
+ return true;
28
+ };
29
+ const copyIfMissing = async (fromPath, toPath) => {
30
+ if (existsSync(toPath))
31
+ return false;
32
+ const content = await fs.readFile(fromPath, "utf8");
33
+ await ensureDir(path.dirname(toPath));
34
+ await fs.writeFile(toPath, content, "utf8");
35
+ return true;
36
+ };
37
+ export const runInit = async ({ cwd, quiet }) => {
38
+ const created = [];
39
+ if (await copyIfMissing(DEFAULT_CONFIG_PATH, path.join(cwd, "agents.config.json"))) {
40
+ created.push("agents.config.json");
41
+ }
42
+ const agentsDir = path.join(cwd, ".agents");
43
+ await ensureDir(agentsDir);
44
+ await ensureDir(path.join(agentsDir, "commands"));
45
+ await ensureDir(path.join(agentsDir, "hooks"));
46
+ await ensureDir(path.join(agentsDir, "skills"));
47
+ if (await copyIfMissing(DEFAULT_SCHEMA_PATH, path.join(agentsDir, "SCHEMA.MD"))) {
48
+ created.push(".agents/SCHEMA.MD");
49
+ }
50
+ if (await writeIfMissing(path.join(agentsDir, "commands", "schema.md"), DEFAULT_SCHEMA_COMMAND)) {
51
+ created.push(".agents/commands/schema.md");
52
+ }
53
+ await syncOnce(cwd);
54
+ if (!quiet) {
55
+ if (created.length) {
56
+ console.log(`agents-sync init created: ${created.join(", ")}`);
57
+ }
58
+ else {
59
+ console.log("agents-sync init: nothing to do");
60
+ }
61
+ }
62
+ };
@@ -0,0 +1,36 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { runInit } from "./init.js";
5
+ const shouldSkip = async () => {
6
+ if (process.env.AGENTS_SYNC_SKIP_INIT === "1")
7
+ return true;
8
+ if (process.env.npm_config_global === "true")
9
+ return true;
10
+ const initCwd = process.env.INIT_CWD;
11
+ if (!initCwd)
12
+ return true;
13
+ const pkgPath = path.join(initCwd, "package.json");
14
+ if (!existsSync(pkgPath))
15
+ return true;
16
+ try {
17
+ const raw = await fs.readFile(pkgPath, "utf8");
18
+ const pkg = JSON.parse(raw);
19
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
20
+ if (!deps["agents-sync"])
21
+ return true;
22
+ }
23
+ catch {
24
+ return true;
25
+ }
26
+ return false;
27
+ };
28
+ const main = async () => {
29
+ if (await shouldSkip())
30
+ return;
31
+ const cwd = process.env.INIT_CWD ?? process.cwd();
32
+ await runInit({ cwd, quiet: true });
33
+ };
34
+ main().catch((err) => {
35
+ console.warn("[agents-sync] init skipped:", err?.message ?? err);
36
+ });
package/dist/sync.js ADDED
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { loadConfig, resolveTargetAgentsDir } from "./config.js";
5
+ import { parseAgentsMarkdown, inferKindFromPath } from "./schema.js";
6
+ import { translators } from "./translation/index.js";
7
+ export const ensureDir = async (dirPath) => {
8
+ await fs.mkdir(dirPath, { recursive: true });
9
+ };
10
+ export const writeIfChanged = async (filePath, content) => {
11
+ let existing = null;
12
+ try {
13
+ existing = await fs.readFile(filePath, "utf8");
14
+ }
15
+ catch {
16
+ existing = null;
17
+ }
18
+ if (existing === content)
19
+ return;
20
+ await ensureDir(path.dirname(filePath));
21
+ await fs.writeFile(filePath, content, "utf8");
22
+ };
23
+ export const walkFiles = async (dirPath) => {
24
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
25
+ const files = [];
26
+ for (const entry of entries) {
27
+ const full = path.join(dirPath, entry.name);
28
+ if (entry.isDirectory()) {
29
+ files.push(...(await walkFiles(full)));
30
+ }
31
+ else if (entry.isFile()) {
32
+ files.push(full);
33
+ }
34
+ }
35
+ return files;
36
+ };
37
+ export const createSyncContext = async (cwd, configPath = path.join(cwd, "agents.config.json")) => {
38
+ const config = await loadConfig(configPath);
39
+ const sourceDir = path.resolve(cwd, config.sourceDir);
40
+ const rootDoc = path.resolve(cwd, config.rootDoc);
41
+ const targetEntries = Object.entries(config.targets);
42
+ return { cwd, config, sourceDir, rootDoc, targetEntries };
43
+ };
44
+ export const ensureTargetDirs = async (ctx) => {
45
+ for (const [, target] of ctx.targetEntries) {
46
+ await ensureDir(resolveTargetAgentsDir(ctx.cwd, target, ctx.config.agentsSubdir));
47
+ }
48
+ };
49
+ export const syncSourceFile = async (ctx, filePath) => {
50
+ const rel = path.relative(ctx.sourceDir, filePath);
51
+ const kind = inferKindFromPath(rel);
52
+ if (!kind)
53
+ return;
54
+ const id = path.basename(rel, path.extname(rel));
55
+ const content = await fs.readFile(filePath, "utf8");
56
+ const doc = parseAgentsMarkdown(content, { kind, id });
57
+ await Promise.all(ctx.targetEntries.map(async ([targetKey, target]) => {
58
+ const translator = translators[targetKey];
59
+ if (!translator)
60
+ return;
61
+ const baseDir = resolveTargetAgentsDir(ctx.cwd, target, ctx.config.agentsSubdir);
62
+ const subdir = translator.subdir(doc, target);
63
+ const ext = translator.fileExtension(doc, target);
64
+ const outPath = path.join(baseDir, subdir, `${doc.id}${ext}`);
65
+ const translated = translator.renderDoc(doc, target);
66
+ await writeIfChanged(outPath, translated);
67
+ }));
68
+ };
69
+ export const removeSourceFile = async (ctx, filePath) => {
70
+ const rel = path.relative(ctx.sourceDir, filePath);
71
+ const kind = inferKindFromPath(rel);
72
+ if (!kind)
73
+ return;
74
+ const id = path.basename(rel, path.extname(rel));
75
+ const doc = parseAgentsMarkdown("", { kind, id });
76
+ await Promise.all(ctx.targetEntries.map(async ([targetKey, target]) => {
77
+ const translator = translators[targetKey];
78
+ if (!translator)
79
+ return;
80
+ const baseDir = resolveTargetAgentsDir(ctx.cwd, target, ctx.config.agentsSubdir);
81
+ const subdir = translator.subdir(doc, target);
82
+ const ext = translator.fileExtension(doc, target);
83
+ const outPath = path.join(baseDir, subdir, `${id}${ext}`);
84
+ await fs.rm(outPath, { force: true });
85
+ }));
86
+ };
87
+ export const syncRootDoc = async (ctx) => {
88
+ if (!existsSync(ctx.rootDoc))
89
+ return;
90
+ const content = await fs.readFile(ctx.rootDoc, "utf8");
91
+ const doc = parseAgentsMarkdown(content, { kind: "config", id: "root" });
92
+ const rootTargets = ctx.config.rootTargets;
93
+ await Promise.all(Object.entries(rootTargets).map(async ([targetKey, outName]) => {
94
+ const translator = translators[targetKey];
95
+ if (!translator)
96
+ return;
97
+ const outPath = path.resolve(ctx.cwd, outName);
98
+ const translated = translator.renderDoc(doc, ctx.config.targets[targetKey]);
99
+ await writeIfChanged(outPath, translated);
100
+ }));
101
+ };
102
+ export const syncOnce = async (cwd, configPath = path.join(cwd, "agents.config.json")) => {
103
+ const ctx = await createSyncContext(cwd, configPath);
104
+ await ensureTargetDirs(ctx);
105
+ if (existsSync(ctx.sourceDir)) {
106
+ const files = await walkFiles(ctx.sourceDir);
107
+ for (const filePath of files) {
108
+ await syncSourceFile(ctx, filePath);
109
+ }
110
+ }
111
+ await syncRootDoc(ctx);
112
+ return ctx;
113
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-sync",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -16,7 +16,8 @@
16
16
  "build": "tsc -p tsconfig.json",
17
17
  "postbuild": "node scripts/add-shebang.mjs",
18
18
  "start": "node dist/index.js",
19
- "prepare": "npm run build"
19
+ "prepare": "npm run build",
20
+ "postinstall": "node dist/postinstall.js"
20
21
  },
21
22
  "dependencies": {
22
23
  "chokidar": "^3.6.0"
package/readme.md CHANGED
@@ -26,6 +26,30 @@ npm i -D agents-sync
26
26
  npx agents-sync
27
27
  ```
28
28
 
29
+ ### Auto Init On Install
30
+
31
+ When installed as a direct dependency, `agents-sync` will scaffold:
32
+
33
+ - `agents.config.json`
34
+ - `.agents/SCHEMA.MD`
35
+ - `.agents/commands/schema.md`
36
+ - `.agents/commands`, `.agents/hooks`, `.agents/skills`
37
+
38
+ It then runs a one-time sync to create the target folders and the `schema`
39
+ command in each tool folder.
40
+
41
+ Skip this behavior with:
42
+
43
+ ```bash
44
+ AGENTS_SYNC_SKIP_INIT=1 npm i -D agents-sync
45
+ ```
46
+
47
+ You can also run it manually:
48
+
49
+ ```bash
50
+ npx agents-sync init
51
+ ```
52
+
29
53
  ## Canonical Schema
30
54
 
31
55
  The canonical format is Markdown with an optional `agents` metadata fence. See `SCHEMA.MD` for the full schema and translation rules.