avenic 1.0.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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/package.json +30 -0
  4. package/scripts/skills.mjs +13 -0
  5. package/scripts/watchdog.mjs +50 -0
  6. package/src/cli/dispatcher.mjs +439 -0
  7. package/src/cli/self-update.mjs +27 -0
  8. package/src/cli/skills-cli.mjs +1060 -0
  9. package/src/cli/watchdog.mjs +30 -0
  10. package/vendor/core-src/index.mjs +143 -0
  11. package/vendor/core-src/runtime/adapters/claude.mjs +81 -0
  12. package/vendor/core-src/runtime/adapters/codex.mjs +142 -0
  13. package/vendor/core-src/runtime/adapters/index.mjs +9 -0
  14. package/vendor/core-src/runtime/adapters/opencode.mjs +76 -0
  15. package/vendor/core-src/runtime/agents.mjs +39 -0
  16. package/vendor/core-src/runtime/config.mjs +227 -0
  17. package/vendor/core-src/runtime/gitignore.mjs +69 -0
  18. package/vendor/core-src/runtime/process.mjs +41 -0
  19. package/vendor/core-src/runtime/project-root.mjs +42 -0
  20. package/vendor/core-src/runtime/sessions.mjs +312 -0
  21. package/vendor/core-src/skills/catalog.mjs +130 -0
  22. package/vendor/core-src/skills/direct.mjs +209 -0
  23. package/vendor/core-src/skills/git.mjs +93 -0
  24. package/vendor/core-src/skills/ids.mjs +40 -0
  25. package/vendor/core-src/skills/install.mjs +357 -0
  26. package/vendor/core-src/skills/packs.mjs +256 -0
  27. package/vendor/core-src/skills/paths.mjs +92 -0
  28. package/vendor/core-src/skills/sources.mjs +246 -0
  29. package/vendor/core-src/skills/ui.mjs +21 -0
  30. package/vendor/core-src/skills/vendor.mjs +57 -0
  31. package/vendor/core-src/util/fail.mjs +3 -0
  32. package/vendor/core-src/util/fs.mjs +14 -0
  33. package/vendor/core-src/util/json.mjs +14 -0
@@ -0,0 +1,246 @@
1
+ import { existsSync } from "node:fs";
2
+ import { cp, mkdir, readFile, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fail } from "../util/fail.mjs";
5
+ import { isInside } from "../util/fs.mjs";
6
+ import { readJson, writeJson } from "../util/json.mjs";
7
+ import {
8
+ assertSafeId,
9
+ assertSafeSkillName,
10
+ assertSafeSkillPath,
11
+ assertSafeSkillRoot,
12
+ } from "./ids.mjs";
13
+ import { cloneHead, normalizeRepositoryInput, repositoryIdentity } from "./git.mjs";
14
+ import { createTempDirectory, removeTempDirectory } from "./vendor.mjs";
15
+
16
+ export function parseFrontmatterName(content, file) {
17
+ const frontmatter = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
18
+ const match = frontmatter?.[1].match(/^name:\s*(.+?)\s*$/m);
19
+ if (!match) {
20
+ fail(`SKILL.md is missing name: ${file}`);
21
+ }
22
+ return match[1].replace(/^["']|["']$/g, "").trim();
23
+ }
24
+
25
+ export async function readSkill(skillDirectory, requireMatchingFolder = true) {
26
+ const file = path.join(skillDirectory, "SKILL.md");
27
+ if (!existsSync(file)) {
28
+ fail(`Missing SKILL.md: ${skillDirectory}`);
29
+ }
30
+ const name = parseFrontmatterName(await readFile(file, "utf8"), file);
31
+ const folder = path.basename(skillDirectory);
32
+ if (requireMatchingFolder && name !== folder) {
33
+ fail(`Skill folder must match its upstream name: ${folder} != ${name}`);
34
+ }
35
+ return { name, directory: skillDirectory };
36
+ }
37
+
38
+ export async function loadSources(catalogRoot) {
39
+ const data = await readJson(path.join(catalogRoot, "sources.lock.json"));
40
+ if (!Array.isArray(data.sources) || data.sources.length === 0) {
41
+ fail("sources.lock.json has no upstream sources");
42
+ }
43
+ const ids = new Set();
44
+ for (const source of data.sources) {
45
+ assertSafeId(source.id, "Source id");
46
+ if (ids.has(source.id)) {
47
+ fail(`Duplicate source id: ${source.id}`);
48
+ }
49
+ ids.add(source.id);
50
+ if (!source.name || !source.repository || !source.skillRoot || !source.revision) {
51
+ fail(`Incomplete source configuration: ${source.id}`);
52
+ }
53
+ assertSafeSkillRoot(source.skillRoot);
54
+ if (source.skillPaths) {
55
+ for (const [skillName, upstreamPath] of Object.entries(source.skillPaths)) {
56
+ assertSafeSkillName(skillName);
57
+ assertSafeSkillPath(upstreamPath, `upstream path for ${skillName}`);
58
+ }
59
+ }
60
+ }
61
+ return data;
62
+ }
63
+
64
+ export async function saveSources(catalogRoot, data) {
65
+ await writeJson(path.join(catalogRoot, "sources.lock.json"), data);
66
+ }
67
+
68
+ export async function buildCatalog(config, skillsRoot) {
69
+ const byName = new Map();
70
+ const groups = [];
71
+ for (const source of config.sources) {
72
+ const sourceDirectory = path.join(skillsRoot, source.id);
73
+ if (!isInside(skillsRoot, sourceDirectory) || !existsSync(sourceDirectory)) {
74
+ fail(`Source directory does not exist: skills/${source.id}`);
75
+ }
76
+ const entries = await readdir(sourceDirectory, { withFileTypes: true });
77
+ const skills = [];
78
+ for (const entry of entries.filter((item) => item.isDirectory())) {
79
+ const skill = await readSkill(path.join(sourceDirectory, entry.name));
80
+ if (byName.has(skill.name)) {
81
+ fail(`Skill name occurs in multiple sources: ${skill.name}`);
82
+ }
83
+ const record = { ...skill, source };
84
+ byName.set(skill.name, record);
85
+ skills.push(record);
86
+ }
87
+ skills.sort((left, right) => left.name.localeCompare(right.name));
88
+ groups.push({ source, skills });
89
+ }
90
+ return { byName, groups };
91
+ }
92
+
93
+ export function findSource(sourceConfig, sourceReference) {
94
+ return sourceConfig.sources.find(
95
+ (source) =>
96
+ source.id === sourceReference ||
97
+ repositoryIdentity(source.repository) === repositoryIdentity(sourceReference),
98
+ );
99
+ }
100
+
101
+ export async function stageSource(source, cloneDirectory, stageDirectory, skillNames) {
102
+ for (const skillName of skillNames) {
103
+ assertSafeSkillName(skillName);
104
+ const upstreamPath = source.skillPaths?.[skillName] ?? skillName;
105
+ assertSafeSkillPath(upstreamPath, `upstream path for ${skillName}`);
106
+ const upstream = path.join(cloneDirectory, source.skillRoot, upstreamPath);
107
+ const metadata = await readSkill(upstream, false);
108
+ if (metadata.name !== skillName) {
109
+ fail(`Upstream Skill name mismatch: ${skillName}`);
110
+ }
111
+ await cp(upstream, path.join(stageDirectory, source.id, skillName), {
112
+ recursive: true,
113
+ filter: (sourcePath) => ![".git", "node_modules"].includes(path.basename(sourcePath)),
114
+ });
115
+ }
116
+ if (source.licenseSource && source.licenseFile) {
117
+ const upstreamLicense = path.join(cloneDirectory, source.licenseSource);
118
+ if (!existsSync(upstreamLicense)) {
119
+ fail(`Upstream license does not exist: ${source.id}/${source.licenseSource}`);
120
+ }
121
+ await mkdir(path.dirname(path.join(stageDirectory, source.licenseFile)), { recursive: true });
122
+ await cp(upstreamLicense, path.join(stageDirectory, source.licenseFile));
123
+ }
124
+ }
125
+
126
+ export async function discoverSourceSkills(source, cloneDirectory, options = {}) {
127
+ const sourceRoot = path.join(cloneDirectory, source.skillRoot);
128
+ const skillDirectories = [];
129
+ const pending = [{ directory: sourceRoot, relativePath: "" }];
130
+ while (pending.length > 0) {
131
+ const current = pending.pop();
132
+ if (existsSync(path.join(current.directory, "SKILL.md"))) {
133
+ skillDirectories.push(current);
134
+ continue;
135
+ }
136
+ const entries = await readdir(current.directory, { withFileTypes: true });
137
+ for (const entry of entries.filter((item) => item.isDirectory())) {
138
+ if (entry.name === ".git" || entry.name === "node_modules") {
139
+ continue;
140
+ }
141
+ pending.push({
142
+ directory: path.join(current.directory, entry.name),
143
+ relativePath: current.relativePath
144
+ ? path.join(current.relativePath, entry.name)
145
+ : entry.name,
146
+ });
147
+ }
148
+ }
149
+
150
+ const names = [];
151
+ let mappingsChanged = false;
152
+ for (const entry of skillDirectories) {
153
+ const skill = await readSkill(entry.directory, false);
154
+ assertSafeSkillName(skill.name);
155
+ if (names.includes(skill.name)) {
156
+ fail(`Duplicate upstream Skill name: ${skill.name}`);
157
+ }
158
+ names.push(skill.name);
159
+ const upstreamPath = entry.relativePath.replace(/\\/g, "/") || ".";
160
+ if (skill.name !== upstreamPath && source.skillPaths?.[skill.name] !== upstreamPath) {
161
+ source.skillPaths ??= {};
162
+ source.skillPaths[skill.name] = upstreamPath;
163
+ mappingsChanged = true;
164
+ }
165
+ }
166
+ names.sort((left, right) => left.localeCompare(right));
167
+ if (names.length === 0 && !options.allowEmpty) {
168
+ fail(`No Skills found under ${source.skillRoot}`);
169
+ }
170
+ return { mappingsChanged, names };
171
+ }
172
+
173
+ export async function detectSkillRoot(cloneDirectory) {
174
+ if (existsSync(path.join(cloneDirectory, "SKILL.md"))) {
175
+ return ".";
176
+ }
177
+ const candidates = ["skills", ".agents/skills", ".claude/skills", "."];
178
+ for (const candidate of candidates) {
179
+ const directory = path.join(cloneDirectory, candidate);
180
+ if (!existsSync(directory)) {
181
+ continue;
182
+ }
183
+ const discovery = await discoverSourceSkills(
184
+ { id: "discovery", skillRoot: candidate },
185
+ cloneDirectory,
186
+ { allowEmpty: true },
187
+ );
188
+ if (discovery.names.length > 0) {
189
+ return candidate;
190
+ }
191
+ }
192
+ fail("No Skill root found; use --skill-root <path>");
193
+ }
194
+
195
+ export async function registerSource(catalogRoot, sourceConfig, options, io = console) {
196
+ const repository = normalizeRepositoryInput(options.repository);
197
+ let skillRoot = options.skillRoot;
198
+ assertSafeId(options.id, "Source id");
199
+ if (skillRoot) {
200
+ skillRoot = assertSafeSkillRoot(skillRoot);
201
+ }
202
+ if (sourceConfig.sources.some((source) => source.id === options.id)) {
203
+ fail(`Source already exists: ${options.id}`);
204
+ }
205
+
206
+ const tempDirectory = await createTempDirectory(catalogRoot);
207
+ try {
208
+ const cloneDirectory = path.join(tempDirectory, "clone", options.id);
209
+ io.log(`Registering upstream: ${repository}`);
210
+ const revision = await cloneHead({ repository }, cloneDirectory);
211
+ skillRoot ??= await detectSkillRoot(cloneDirectory);
212
+ if (!existsSync(path.join(cloneDirectory, skillRoot))) {
213
+ fail(`Upstream Skill root does not exist: ${skillRoot}`);
214
+ }
215
+ const rootFiles = await readdir(cloneDirectory, { withFileTypes: true });
216
+ const autoLicense = rootFiles.find(
217
+ (entry) => entry.isFile() && /^licen[cs]e(?:\.|$)/i.test(entry.name),
218
+ );
219
+ const chosenLicense = options.licenseSource ?? autoLicense?.name;
220
+ const source = {
221
+ id: options.id,
222
+ name: options.name ?? options.id,
223
+ repository,
224
+ skillRoot: skillRoot.replace(/\\/g, "/"),
225
+ revision,
226
+ };
227
+ if (chosenLicense) {
228
+ const upstreamLicense = path.join(cloneDirectory, chosenLicense);
229
+ if (!existsSync(upstreamLicense)) {
230
+ fail(`License file does not exist: ${chosenLicense}`);
231
+ }
232
+ source.licenseSource = chosenLicense.replace(/\\/g, "/");
233
+ source.licenseFile = `licenses/${options.id}-${path.basename(chosenLicense)}`;
234
+ await mkdir(path.dirname(path.join(catalogRoot, source.licenseFile)), { recursive: true });
235
+ await cp(upstreamLicense, path.join(catalogRoot, source.licenseFile));
236
+ }
237
+ sourceConfig.sources.push(source);
238
+ await mkdir(path.join(catalogRoot, "skills", options.id), { recursive: true });
239
+ await saveSources(catalogRoot, sourceConfig);
240
+ io.log(`Registered ${options.id} @ ${revision.slice(0, 8)}`);
241
+ io.log(`Skill root: ${skillRoot}`);
242
+ return source;
243
+ } finally {
244
+ await removeTempDirectory(tempDirectory);
245
+ }
246
+ }
@@ -0,0 +1,21 @@
1
+ export function printTree(groups, title, details = [], io = console) {
2
+ const total = groups.reduce((count, group) => count + group.skills.length, 0);
3
+ io.log(`\n${title}`);
4
+ for (const detail of details) {
5
+ io.log(` ${detail}`);
6
+ }
7
+ io.log(`\nSkills · ${total} unique`);
8
+ groups.forEach((group, groupIndex) => {
9
+ const lastGroup = groupIndex === groups.length - 1;
10
+ const groupBranch = lastGroup ? "└──" : "├──";
11
+ const childPrefix = lastGroup ? " " : "│ ";
12
+ io.log(
13
+ `${groupBranch} ${group.source.name} · ${group.skills.length}`,
14
+ );
15
+ group.skills.forEach((skill, skillIndex) => {
16
+ const skillBranch = skillIndex === group.skills.length - 1 ? "└──" : "├──";
17
+ io.log(`${childPrefix}${skillBranch} ${skill.name}`);
18
+ });
19
+ });
20
+ io.log();
21
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, mkdtemp, rename, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export async function createTempDirectory(catalogRoot) {
6
+ const tempRoot = path.join(catalogRoot, ".tmp");
7
+ await mkdir(tempRoot, { recursive: true });
8
+ return mkdtemp(path.join(tempRoot, "vendor-"));
9
+ }
10
+
11
+ export async function removeTempDirectory(directory, io = console) {
12
+ try {
13
+ await rm(directory, {
14
+ recursive: true,
15
+ force: true,
16
+ maxRetries: 10,
17
+ retryDelay: 200,
18
+ });
19
+ } catch (error) {
20
+ io.warn(
21
+ `Warning: temporary directory cleanup failed: ${directory} (${error.code ?? error.message})`,
22
+ );
23
+ }
24
+ }
25
+
26
+ export async function replaceStagedFiles(replacements, tempDirectory) {
27
+ const completed = [];
28
+ try {
29
+ for (const replacement of replacements) {
30
+ const backup = path.join(tempDirectory, "backup", replacement.relativePath);
31
+ await mkdir(path.dirname(backup), { recursive: true });
32
+ let hasBackup = false;
33
+ if (existsSync(replacement.target)) {
34
+ await rename(replacement.target, backup);
35
+ hasBackup = true;
36
+ }
37
+ await mkdir(path.dirname(replacement.target), { recursive: true });
38
+ try {
39
+ await rename(replacement.staged, replacement.target);
40
+ } catch (error) {
41
+ if (hasBackup) {
42
+ await rename(backup, replacement.target);
43
+ }
44
+ throw error;
45
+ }
46
+ completed.push({ ...replacement, backup });
47
+ }
48
+ } catch (error) {
49
+ for (const replacement of completed.reverse()) {
50
+ await rm(replacement.target, { recursive: true, force: true });
51
+ if (existsSync(replacement.backup)) {
52
+ await rename(replacement.backup, replacement.target);
53
+ }
54
+ }
55
+ throw error;
56
+ }
57
+ }
@@ -0,0 +1,3 @@
1
+ export function fail(message) {
2
+ throw new Error(message);
3
+ }
@@ -0,0 +1,14 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, rmdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export function isInside(parent, target) {
6
+ const relative = path.relative(parent, target);
7
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
8
+ }
9
+
10
+ export async function removeEmptyDirectory(directory) {
11
+ if (existsSync(directory) && (await readdir(directory)).length === 0) {
12
+ await rmdir(directory);
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { fail } from "./fail.mjs";
3
+
4
+ export async function readJson(file) {
5
+ try {
6
+ return JSON.parse((await readFile(file, "utf8")).replace(/^/, ""));
7
+ } catch (error) {
8
+ fail(`Cannot parse JSON file ${file}: ${error.message}`);
9
+ }
10
+ }
11
+
12
+ export async function writeJson(file, value) {
13
+ await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
14
+ }