nymor 1.0.1
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/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/agents/targets.js +111 -0
- package/dist/commands/add.js +121 -0
- package/dist/commands/compile.js +159 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/inject.js +24 -0
- package/dist/commands/learn.js +145 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/remove.js +38 -0
- package/dist/commands/update.js +80 -0
- package/dist/commands/validate.js +82 -0
- package/dist/compiler/agentsmd.js +17 -0
- package/dist/compiler/block.js +25 -0
- package/dist/compiler/claude.js +16 -0
- package/dist/compiler/copilot.js +29 -0
- package/dist/compiler/cursor.js +38 -0
- package/dist/compiler/kiro.js +24 -0
- package/dist/detector/agents.js +24 -0
- package/dist/detector/stack.js +113 -0
- package/dist/index.js +52 -0
- package/dist/registry/cache.js +60 -0
- package/dist/registry/client.js +135 -0
- package/dist/registry/resolver.js +29 -0
- package/dist/registry/types.js +2 -0
- package/dist/templates/bootstrap.js +97 -0
- package/dist/templates/cicada-json.js +11 -0
- package/dist/templates/nymor-json.js +11 -0
- package/dist/utils/manifest.js +32 -0
- package/dist/utils/paths.js +30 -0
- package/dist/utils/skills.js +114 -0
- package/package.json +32 -0
- package/src/agents/targets.ts +141 -0
- package/src/commands/compile.ts +202 -0
- package/src/commands/doctor.ts +253 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/learn.ts +175 -0
- package/src/commands/list.ts +57 -0
- package/src/commands/validate.ts +89 -0
- package/src/compiler/block.ts +26 -0
- package/src/compiler/claude.ts +13 -0
- package/src/compiler/copilot.ts +28 -0
- package/src/compiler/cursor.ts +38 -0
- package/src/compiler/kiro.ts +22 -0
- package/src/detector/agents.ts +26 -0
- package/src/detector/stack.ts +135 -0
- package/src/index.ts +59 -0
- package/src/templates/bootstrap.ts +109 -0
- package/src/templates/nymor-json.ts +15 -0
- package/src/utils/manifest.ts +38 -0
- package/src/utils/paths.ts +25 -0
- package/src/utils/skills.ts +152 -0
- package/tests/compiler/__snapshots__/claude.test.ts.snap +65 -0
- package/tests/compiler/__snapshots__/copilot.test.ts.snap +54 -0
- package/tests/compiler/__snapshots__/cursor.test.ts.snap +62 -0
- package/tests/compiler/__snapshots__/kiro.test.ts.snap +54 -0
- package/tests/compiler/block.test.ts +24 -0
- package/tests/compiler/claude.test.ts +46 -0
- package/tests/compiler/copilot.test.ts +15 -0
- package/tests/compiler/cursor.test.ts +15 -0
- package/tests/compiler/kiro.test.ts +15 -0
- package/tests/detector/agents.test.ts +48 -0
- package/tests/detector/stack.test.ts +29 -0
- package/tests/e2e/init-and-compile.test.ts +227 -0
- package/tests/fixtures/skills/scoped/SKILL.md +18 -0
- package/tests/fixtures/skills/simple/SKILL.md +16 -0
- package/tests/fixtures/skills/with-examples/SKILL.md +18 -0
- package/tests/fixtures/skills/with-examples/examples/example.md +3 -0
- package/tests/fixtures/stacks/django/manage.py +2 -0
- package/tests/fixtures/stacks/django/requirements.txt +1 -0
- package/tests/fixtures/stacks/fastapi/requirements.txt +1 -0
- package/tests/fixtures/stacks/go/go.mod +3 -0
- package/tests/fixtures/stacks/nodejs/package.json +5 -0
- package/tests/fixtures/stacks/react/package.json +5 -0
- package/tests/fixtures/stacks/rust/Cargo.toml +4 -0
- package/tests/fixtures/stacks/vue/package.json +8 -0
- package/tests/fixtures/stacks/vue/vite.config.ts +5 -0
- package/tests/utils/manifest.test.ts +31 -0
- package/tests/utils/paths.test.ts +23 -0
- package/tests/utils/skills.test.ts +49 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doctorCommand = doctorCommand;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const glob_1 = require("glob");
|
|
10
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
11
|
+
const targets_1 = require("../agents/targets");
|
|
12
|
+
const compile_1 = require("./compile");
|
|
13
|
+
const paths_1 = require("../utils/paths");
|
|
14
|
+
const skills_1 = require("../utils/skills");
|
|
15
|
+
const VALID_AGENTS = targets_1.AGENT_TARGETS.map((target) => target.id);
|
|
16
|
+
async function doctorCommand() {
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
const results = [];
|
|
19
|
+
await checkManifest(projectRoot, results);
|
|
20
|
+
const frontmatters = await checkSkillFrontmatter(projectRoot, results);
|
|
21
|
+
await checkGlobExistence(projectRoot, frontmatters, results);
|
|
22
|
+
checkDuplicateNames(projectRoot, frontmatters, results);
|
|
23
|
+
await checkCompiledOutput(projectRoot, results);
|
|
24
|
+
for (const result of results) {
|
|
25
|
+
const status = result.warn ? "WARN" : result.ok ? "PASS" : "FAIL";
|
|
26
|
+
console.log(`${status} ${result.label} - ${result.filePath}${result.message ? ` - ${result.message}` : ""}`);
|
|
27
|
+
}
|
|
28
|
+
if (results.some((result) => !result.ok && !result.warn)) {
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function checkManifest(projectRoot, results) {
|
|
33
|
+
const manifestPath = (0, paths_1.getManifestPath)(projectRoot);
|
|
34
|
+
try {
|
|
35
|
+
const manifest = (await fs_extra_1.default.readJson(manifestPath));
|
|
36
|
+
const errors = [];
|
|
37
|
+
if (manifest.version !== "1") {
|
|
38
|
+
errors.push('version must be "1"');
|
|
39
|
+
}
|
|
40
|
+
const invalidAgents = (manifest.agents ?? []).filter((agent) => !VALID_AGENTS.includes(agent));
|
|
41
|
+
if (invalidAgents.length > 0) {
|
|
42
|
+
errors.push(`invalid agents: ${invalidAgents.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
results.push({
|
|
45
|
+
ok: errors.length === 0,
|
|
46
|
+
label: "Manifest sanity",
|
|
47
|
+
filePath: manifestPath,
|
|
48
|
+
message: errors.join("; ")
|
|
49
|
+
});
|
|
50
|
+
return errors.length === 0 ? normalizeManifest(manifest) : null;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
results.push({
|
|
54
|
+
ok: false,
|
|
55
|
+
label: "Manifest sanity",
|
|
56
|
+
filePath: manifestPath,
|
|
57
|
+
message: err instanceof Error ? err.message : String(err)
|
|
58
|
+
});
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function checkSkillFrontmatter(projectRoot, results) {
|
|
63
|
+
const skillsDir = (0, paths_1.getSkillsDir)(projectRoot);
|
|
64
|
+
const skillDirs = await (0, skills_1.listSkillDirectories)(skillsDir);
|
|
65
|
+
const parsed = [];
|
|
66
|
+
for (const id of skillDirs) {
|
|
67
|
+
const skillPath = path_1.default.join(skillsDir, id, "SKILL.md");
|
|
68
|
+
const errors = [];
|
|
69
|
+
let frontmatter = {};
|
|
70
|
+
if (!(await fs_extra_1.default.pathExists(skillPath))) {
|
|
71
|
+
errors.push("missing SKILL.md");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
try {
|
|
75
|
+
frontmatter = parseFrontmatter(await fs_extra_1.default.readFile(skillPath, "utf8"));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!frontmatter.name) {
|
|
82
|
+
errors.push("missing name");
|
|
83
|
+
}
|
|
84
|
+
if (frontmatter.globs !== undefined && !Array.isArray(frontmatter.globs)) {
|
|
85
|
+
errors.push("globs must be an array");
|
|
86
|
+
}
|
|
87
|
+
const isValid = errors.length === 0;
|
|
88
|
+
results.push({
|
|
89
|
+
ok: isValid,
|
|
90
|
+
label: "Frontmatter validity",
|
|
91
|
+
filePath: skillPath,
|
|
92
|
+
message: errors.join("; ")
|
|
93
|
+
});
|
|
94
|
+
parsed.push({
|
|
95
|
+
id,
|
|
96
|
+
filePath: skillPath,
|
|
97
|
+
name: typeof frontmatter.name === "string" ? frontmatter.name : "",
|
|
98
|
+
globs: Array.isArray(frontmatter.globs) ? frontmatter.globs.map(String) : [],
|
|
99
|
+
alwaysApply: Boolean(frontmatter.alwaysApply),
|
|
100
|
+
valid: isValid
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
async function checkGlobExistence(projectRoot, frontmatters, results) {
|
|
106
|
+
for (const skill of frontmatters) {
|
|
107
|
+
if (!skill.valid || skill.alwaysApply || skill.globs.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const missing = [];
|
|
111
|
+
for (const pattern of skill.globs) {
|
|
112
|
+
const matches = await (0, glob_1.glob)(pattern, {
|
|
113
|
+
cwd: projectRoot,
|
|
114
|
+
nodir: true,
|
|
115
|
+
dot: true,
|
|
116
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/.nymor/**"]
|
|
117
|
+
});
|
|
118
|
+
if (matches.length === 0) {
|
|
119
|
+
missing.push(pattern);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
results.push({
|
|
123
|
+
ok: missing.length === 0,
|
|
124
|
+
label: "Glob existence",
|
|
125
|
+
filePath: skill.filePath,
|
|
126
|
+
message: missing.length > 0 ? `no matches: ${missing.join(", ")}` : ""
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function checkDuplicateNames(projectRoot, frontmatters, results) {
|
|
131
|
+
const seen = new Map();
|
|
132
|
+
for (const skill of frontmatters.filter((item) => item.valid)) {
|
|
133
|
+
const previous = seen.get(skill.name);
|
|
134
|
+
if (previous) {
|
|
135
|
+
results.push({
|
|
136
|
+
ok: false,
|
|
137
|
+
label: "Duplicate names",
|
|
138
|
+
filePath: skill.filePath,
|
|
139
|
+
message: `duplicates ${previous.filePath}`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
seen.set(skill.name, skill);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (![...seen.values()].some((skill) => hasDuplicate(skill.name, frontmatters))) {
|
|
147
|
+
results.push({
|
|
148
|
+
ok: true,
|
|
149
|
+
label: "Duplicate names",
|
|
150
|
+
filePath: (0, paths_1.getSkillsDir)(projectRoot),
|
|
151
|
+
message: ""
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function checkCompiledOutput(projectRoot, results) {
|
|
156
|
+
try {
|
|
157
|
+
const planned = await (0, compile_1.planCompileOutputs)(projectRoot);
|
|
158
|
+
const stale = [];
|
|
159
|
+
for (const file of planned) {
|
|
160
|
+
if (!(await fs_extra_1.default.pathExists(file.path))) {
|
|
161
|
+
stale.push(file.path);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const actual = await fs_extra_1.default.readFile(file.path);
|
|
165
|
+
if (!actual.equals(file.content)) {
|
|
166
|
+
stale.push(file.path);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
results.push({
|
|
170
|
+
ok: stale.length === 0,
|
|
171
|
+
label: "Compiled output staleness",
|
|
172
|
+
filePath: projectRoot,
|
|
173
|
+
message: stale.length > 0 ? "run `nymor compile`" : ""
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
results.push({
|
|
178
|
+
ok: false,
|
|
179
|
+
label: "Compiled output staleness",
|
|
180
|
+
filePath: projectRoot,
|
|
181
|
+
message: err instanceof Error ? err.message : String(err)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function parseFrontmatter(content) {
|
|
186
|
+
const lines = content.split(/\r?\n/);
|
|
187
|
+
if (lines[0]?.trim() !== "---") {
|
|
188
|
+
throw new Error("missing frontmatter");
|
|
189
|
+
}
|
|
190
|
+
const endIndex = lines.slice(1).findIndex((line) => line.trim() === "---");
|
|
191
|
+
if (endIndex === -1) {
|
|
192
|
+
throw new Error("frontmatter is not closed");
|
|
193
|
+
}
|
|
194
|
+
return (yaml_1.default.parse(lines.slice(1, endIndex + 1).join("\n")) ?? {});
|
|
195
|
+
}
|
|
196
|
+
function normalizeManifest(manifest) {
|
|
197
|
+
return {
|
|
198
|
+
version: manifest.version,
|
|
199
|
+
agents: manifest.agents ?? [],
|
|
200
|
+
local: manifest.local ?? []
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function hasDuplicate(name, frontmatters) {
|
|
204
|
+
return frontmatters.filter((skill) => skill.valid && skill.name === name).length > 1;
|
|
205
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initCommand = initCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
|
+
const targets_1 = require("../agents/targets");
|
|
10
|
+
const compile_1 = require("./compile");
|
|
11
|
+
const agents_1 = require("../detector/agents");
|
|
12
|
+
const nymor_json_1 = require("../templates/nymor-json");
|
|
13
|
+
const manifest_1 = require("../utils/manifest");
|
|
14
|
+
const paths_1 = require("../utils/paths");
|
|
15
|
+
const AGENT_CHOICES = targets_1.AGENT_TARGETS.map((target) => ({
|
|
16
|
+
name: `${target.label} (${target.description})`,
|
|
17
|
+
value: target.id,
|
|
18
|
+
short: target.short
|
|
19
|
+
}));
|
|
20
|
+
async function initCommand() {
|
|
21
|
+
const projectRoot = process.cwd();
|
|
22
|
+
const nymorDir = (0, paths_1.getNymorDir)(projectRoot);
|
|
23
|
+
const skillsDir = (0, paths_1.getSkillsDir)(projectRoot);
|
|
24
|
+
const mode = await resolveInitMode(nymorDir);
|
|
25
|
+
if (!mode) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (mode === "reinit") {
|
|
29
|
+
await fs_extra_1.default.remove(nymorDir);
|
|
30
|
+
}
|
|
31
|
+
const manifestPath = (0, paths_1.getManifestPath)(projectRoot);
|
|
32
|
+
const existingManifest = (await fs_extra_1.default.pathExists(manifestPath)) ? await (0, manifest_1.readManifest)(projectRoot) : null;
|
|
33
|
+
const manifest = (0, nymor_json_1.createDefaultManifest)();
|
|
34
|
+
manifest.agents = await selectAgentTargets(projectRoot, existingManifest?.agents);
|
|
35
|
+
manifest.local = mode === "reinit" ? [] : existingManifest?.local ?? [];
|
|
36
|
+
await (0, manifest_1.writeManifest)(projectRoot, manifest);
|
|
37
|
+
await fs_extra_1.default.ensureDir(skillsDir);
|
|
38
|
+
await (0, compile_1.compileCommand)();
|
|
39
|
+
printSummary(manifest.agents);
|
|
40
|
+
}
|
|
41
|
+
async function resolveInitMode(nymorDir) {
|
|
42
|
+
if (!(await fs_extra_1.default.pathExists(nymorDir))) {
|
|
43
|
+
return "new";
|
|
44
|
+
}
|
|
45
|
+
if (!process.stdin.isTTY) {
|
|
46
|
+
return "update";
|
|
47
|
+
}
|
|
48
|
+
const { action } = await inquirer_1.default.prompt([
|
|
49
|
+
{
|
|
50
|
+
type: "list",
|
|
51
|
+
name: "action",
|
|
52
|
+
message: "Nymor already initialized. What do you want to do?",
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: "Update agent targets", value: "update" },
|
|
55
|
+
{ name: "Reinitialize (overwrites .nymor/)", value: "reinit" },
|
|
56
|
+
{ name: "Cancel", value: "cancel" }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
return action === "cancel" ? null : action;
|
|
61
|
+
}
|
|
62
|
+
async function selectAgentTargets(projectRoot, existingAgents) {
|
|
63
|
+
const detectedAgents = await detectExistingAgentTargets(projectRoot);
|
|
64
|
+
const defaults = existingAgents && existingAgents.length > 0 ? existingAgents : detectedAgents;
|
|
65
|
+
if (!process.stdin.isTTY) {
|
|
66
|
+
return defaults.length > 0 ? defaults : [...targets_1.DEFAULT_AGENT_TARGETS];
|
|
67
|
+
}
|
|
68
|
+
const { agents } = await inquirer_1.default.prompt([
|
|
69
|
+
{
|
|
70
|
+
type: "checkbox",
|
|
71
|
+
name: "agents",
|
|
72
|
+
message: "Which agent outputs should Nymor manage?",
|
|
73
|
+
choices: AGENT_CHOICES,
|
|
74
|
+
default: defaults.length > 0 ? defaults : targets_1.DEFAULT_AGENT_TARGETS
|
|
75
|
+
}
|
|
76
|
+
]);
|
|
77
|
+
return agents;
|
|
78
|
+
}
|
|
79
|
+
async function detectExistingAgentTargets(projectRoot) {
|
|
80
|
+
const detected = await (0, agents_1.detectAgents)(projectRoot);
|
|
81
|
+
return targets_1.AGENT_TARGETS.filter((target) => detected[target.id]).map((target) => target.id);
|
|
82
|
+
}
|
|
83
|
+
function printSummary(agents) {
|
|
84
|
+
const checkMark = "\u2713";
|
|
85
|
+
console.log("");
|
|
86
|
+
console.log(`${checkMark} Nymor initialized`);
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log("Skills directory: .nymor/skills/");
|
|
89
|
+
console.log("Index created: .nymor/index.md");
|
|
90
|
+
console.log(`Agent outputs: ${formatAgentSummary(agents)}`);
|
|
91
|
+
}
|
|
92
|
+
function formatAgentSummary(agents) {
|
|
93
|
+
if (agents.length === 0) {
|
|
94
|
+
return "none selected";
|
|
95
|
+
}
|
|
96
|
+
const labels = new Map(AGENT_CHOICES.map((choice) => [choice.value, choice.short]));
|
|
97
|
+
return agents.map((agent) => labels.get(agent) ?? agent).join(", ");
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.injectCommand = injectCommand;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const skills_1 = require("../utils/skills");
|
|
10
|
+
async function injectCommand() {
|
|
11
|
+
const skillsDir = path_1.default.join(process.cwd(), ".cicada", "skills");
|
|
12
|
+
if (!(await fs_extra_1.default.pathExists(skillsDir))) {
|
|
13
|
+
console.log("No skills found. Run cicada init first.");
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const files = (await (0, skills_1.listMarkdownFiles)(skillsDir)).sort();
|
|
18
|
+
const contents = [];
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const content = await fs_extra_1.default.readFile(path_1.default.join(skillsDir, file), "utf8");
|
|
21
|
+
contents.push(content.trimEnd());
|
|
22
|
+
}
|
|
23
|
+
process.stdout.write(contents.join("\n---\n"));
|
|
24
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.learnCommand = learnCommand;
|
|
7
|
+
exports.slugifyRule = slugifyRule;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
12
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
13
|
+
const compile_1 = require("./compile");
|
|
14
|
+
const manifest_1 = require("../utils/manifest");
|
|
15
|
+
const paths_1 = require("../utils/paths");
|
|
16
|
+
async function learnCommand(rule, options = {}) {
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
const slug = slugifyRule(options.id ?? rule);
|
|
19
|
+
const skillDir = path_1.default.join((0, paths_1.getSkillsDir)(projectRoot), slug);
|
|
20
|
+
const skillPath = path_1.default.join(skillDir, "SKILL.md");
|
|
21
|
+
if (await fs_extra_1.default.pathExists(skillPath)) {
|
|
22
|
+
throw new Error(`A local skill already exists at ${skillPath}`);
|
|
23
|
+
}
|
|
24
|
+
const answers = await promptForSkill(rule, slug, options);
|
|
25
|
+
const frontmatter = yaml_1.default.stringify({
|
|
26
|
+
name: answers.name,
|
|
27
|
+
description: answers.description,
|
|
28
|
+
globs: parseGlobs(answers.globs),
|
|
29
|
+
alwaysApply: answers.alwaysApply
|
|
30
|
+
});
|
|
31
|
+
const content = [
|
|
32
|
+
"---",
|
|
33
|
+
frontmatter.trimEnd(),
|
|
34
|
+
"---",
|
|
35
|
+
"",
|
|
36
|
+
`# Skill: ${answers.name}`,
|
|
37
|
+
"",
|
|
38
|
+
"## Rule",
|
|
39
|
+
rule,
|
|
40
|
+
"",
|
|
41
|
+
"## Why",
|
|
42
|
+
answers.why,
|
|
43
|
+
"",
|
|
44
|
+
"## Example",
|
|
45
|
+
answers.example,
|
|
46
|
+
""
|
|
47
|
+
].join("\n");
|
|
48
|
+
await fs_extra_1.default.ensureDir(skillDir);
|
|
49
|
+
await fs_extra_1.default.writeFile(skillPath, content, "utf8");
|
|
50
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
51
|
+
if (!manifest.local.includes(slug)) {
|
|
52
|
+
manifest.local.push(slug);
|
|
53
|
+
}
|
|
54
|
+
await (0, manifest_1.writeManifest)(projectRoot, manifest);
|
|
55
|
+
await (0, compile_1.compileCommand)();
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(`${picocolors_1.default.green("✓")} Created local skill: ${skillPath}`);
|
|
58
|
+
}
|
|
59
|
+
function slugifyRule(rule) {
|
|
60
|
+
const slug = rule
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/['"]/g, "")
|
|
63
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
64
|
+
.replace(/^-+|-+$/g, "")
|
|
65
|
+
.slice(0, 50)
|
|
66
|
+
.replace(/-+$/g, "");
|
|
67
|
+
return slug || "new-skill";
|
|
68
|
+
}
|
|
69
|
+
function parseGlobs(value) {
|
|
70
|
+
return value
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((glob) => glob.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
function titleCase(value) {
|
|
76
|
+
return value
|
|
77
|
+
.split("-")
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
|
80
|
+
.join(" ");
|
|
81
|
+
}
|
|
82
|
+
async function promptForSkill(rule, slug, options) {
|
|
83
|
+
const defaults = {
|
|
84
|
+
name: options.name ?? titleCase(slug),
|
|
85
|
+
description: options.description ?? rule,
|
|
86
|
+
globs: options.globs ?? "**/*",
|
|
87
|
+
alwaysApply: options.alwaysApply ?? false,
|
|
88
|
+
why: options.why ?? "TBD - describe why",
|
|
89
|
+
example: options.example ?? "TBD - add an example"
|
|
90
|
+
};
|
|
91
|
+
if (!process.stdin.isTTY) {
|
|
92
|
+
return defaults;
|
|
93
|
+
}
|
|
94
|
+
const questions = [];
|
|
95
|
+
if (options.name === undefined) {
|
|
96
|
+
questions.push({
|
|
97
|
+
type: "input",
|
|
98
|
+
name: "name",
|
|
99
|
+
message: "Skill name",
|
|
100
|
+
default: defaults.name
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (options.description === undefined) {
|
|
104
|
+
questions.push({
|
|
105
|
+
type: "input",
|
|
106
|
+
name: "description",
|
|
107
|
+
message: "Description",
|
|
108
|
+
default: defaults.description
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (options.globs === undefined) {
|
|
112
|
+
questions.push({
|
|
113
|
+
type: "input",
|
|
114
|
+
name: "globs",
|
|
115
|
+
message: "Globs",
|
|
116
|
+
default: defaults.globs
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (options.alwaysApply === undefined) {
|
|
120
|
+
questions.push({
|
|
121
|
+
type: "confirm",
|
|
122
|
+
name: "alwaysApply",
|
|
123
|
+
message: "Always apply?",
|
|
124
|
+
default: defaults.alwaysApply
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (options.why === undefined) {
|
|
128
|
+
questions.push({
|
|
129
|
+
type: "input",
|
|
130
|
+
name: "why",
|
|
131
|
+
message: "Why",
|
|
132
|
+
default: defaults.why
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (options.example === undefined) {
|
|
136
|
+
questions.push({
|
|
137
|
+
type: "input",
|
|
138
|
+
name: "example",
|
|
139
|
+
message: "Example",
|
|
140
|
+
default: defaults.example
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const answers = questions.length > 0 ? await inquirer_1.default.prompt(questions) : {};
|
|
144
|
+
return { ...defaults, ...answers };
|
|
145
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listCommand = listCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const paths_1 = require("../utils/paths");
|
|
9
|
+
const skills_1 = require("../utils/skills");
|
|
10
|
+
const manifest_1 = require("../utils/manifest");
|
|
11
|
+
async function listCommand() {
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
const skillsDir = (0, paths_1.getSkillsDir)(projectRoot);
|
|
14
|
+
const indexJsonPath = (0, paths_1.getIndexJsonPath)(projectRoot);
|
|
15
|
+
if (!(await fs_extra_1.default.pathExists(skillsDir))) {
|
|
16
|
+
console.log("No skills found. Run nymor init first.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let entries = [];
|
|
20
|
+
if (await fs_extra_1.default.pathExists(indexJsonPath)) {
|
|
21
|
+
const index = await fs_extra_1.default.readJson(indexJsonPath);
|
|
22
|
+
entries = Array.isArray(index.skills) ? index.skills : [];
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const skills = await (0, skills_1.loadSkills)(skillsDir);
|
|
26
|
+
entries = skills.map((skill) => ({
|
|
27
|
+
id: skill.id,
|
|
28
|
+
name: skill.frontmatter.name,
|
|
29
|
+
description: skill.frontmatter.description ?? "",
|
|
30
|
+
globs: skill.frontmatter.globs ?? [],
|
|
31
|
+
alwaysApply: Boolean(skill.frontmatter.alwaysApply)
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
console.log(`Nymor Skills (${entries.length})`);
|
|
35
|
+
console.log("");
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const arrow = "\u2192";
|
|
40
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
41
|
+
const rows = entries.map((entry) => ({
|
|
42
|
+
entry,
|
|
43
|
+
source: manifest.local.includes(entry.id) ? "local" : "unknown"
|
|
44
|
+
}));
|
|
45
|
+
const skillWidth = Math.max("Skill".length, ...rows.map((row) => row.entry.id.length));
|
|
46
|
+
const sourceWidth = Math.max("Source".length, ...rows.map((row) => row.source.length));
|
|
47
|
+
console.log(` ${"Skill".padEnd(skillWidth)} ${"Source".padEnd(sourceWidth)} Description`);
|
|
48
|
+
console.log(` ${"-".repeat(skillWidth)} ${"-".repeat(sourceWidth)} -----------`);
|
|
49
|
+
rows.forEach(({ entry, source }) => {
|
|
50
|
+
const slug = entry.id.padEnd(skillWidth, " ");
|
|
51
|
+
const sourceColumn = source.padEnd(sourceWidth, " ");
|
|
52
|
+
const description = entry.description || entry.name || "(no description)";
|
|
53
|
+
console.log(` ${slug} ${sourceColumn} ${arrow} ${description}`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.removeCommand = removeCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const compile_1 = require("./compile");
|
|
11
|
+
const add_1 = require("./add");
|
|
12
|
+
const manifest_1 = require("../utils/manifest");
|
|
13
|
+
const paths_1 = require("../utils/paths");
|
|
14
|
+
async function removeCommand(skill) {
|
|
15
|
+
const projectRoot = process.cwd();
|
|
16
|
+
const parsed = (0, add_1.parseRegistrySkill)(skill);
|
|
17
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
18
|
+
if (!manifest.skills[parsed.packageName]) {
|
|
19
|
+
throw new Error(`Skill ${parsed.packageName} is not installed.`);
|
|
20
|
+
}
|
|
21
|
+
await fs_extra_1.default.remove(path_1.default.join((0, paths_1.getSkillsDir)(projectRoot), parsed.folderName));
|
|
22
|
+
await removeCompiledOutputs(projectRoot, parsed.folderName);
|
|
23
|
+
delete manifest.skills[parsed.packageName];
|
|
24
|
+
await (0, manifest_1.writeManifest)(projectRoot, manifest);
|
|
25
|
+
const lockfile = (await (0, manifest_1.readLockfile)(projectRoot)) ?? (0, add_1.createEmptyLockfile)();
|
|
26
|
+
delete lockfile.skills[parsed.packageName];
|
|
27
|
+
await (0, manifest_1.writeLockfile)(projectRoot, lockfile);
|
|
28
|
+
await (0, compile_1.compileCommand)();
|
|
29
|
+
console.log(`${picocolors_1.default.green("✓")} Removed ${parsed.packageName}`);
|
|
30
|
+
}
|
|
31
|
+
async function removeCompiledOutputs(projectRoot, folderName) {
|
|
32
|
+
await Promise.all([
|
|
33
|
+
fs_extra_1.default.remove(path_1.default.join(projectRoot, ".claude", "skills", folderName)),
|
|
34
|
+
fs_extra_1.default.remove(path_1.default.join(projectRoot, ".cursor", "rules", `cicada-${folderName}.mdc`)),
|
|
35
|
+
fs_extra_1.default.remove(path_1.default.join(projectRoot, ".github", "instructions", `cicada-${folderName}.instructions.md`)),
|
|
36
|
+
fs_extra_1.default.remove(path_1.default.join(projectRoot, ".kiro", "steering", `cicada-${folderName}.md`))
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.updateCommand = updateCommand;
|
|
7
|
+
const semver_1 = __importDefault(require("semver"));
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
const compile_1 = require("./compile");
|
|
10
|
+
const add_1 = require("./add");
|
|
11
|
+
const client_1 = require("../registry/client");
|
|
12
|
+
const cache_1 = require("../registry/cache");
|
|
13
|
+
const resolver_1 = require("../registry/resolver");
|
|
14
|
+
const manifest_1 = require("../utils/manifest");
|
|
15
|
+
const INDEX_TTL_MS = 60 * 60 * 1000;
|
|
16
|
+
async function updateCommand(skill, options = {}) {
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
19
|
+
const lockfile = (await (0, manifest_1.readLockfile)(projectRoot)) ?? (0, add_1.createEmptyLockfile)();
|
|
20
|
+
const targets = skill ? [skill] : Object.keys(manifest.skills);
|
|
21
|
+
const rows = [];
|
|
22
|
+
let changed = false;
|
|
23
|
+
if (targets.length === 0) {
|
|
24
|
+
console.log("No registry skills installed.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
const parsed = (0, add_1.parseRegistrySkill)(target);
|
|
29
|
+
const manifestRange = manifest.skills[parsed.packageName];
|
|
30
|
+
if (!manifestRange) {
|
|
31
|
+
throw new Error(`Skill ${parsed.packageName} is not installed.`);
|
|
32
|
+
}
|
|
33
|
+
const index = await getFreshSkillIndex(parsed.scope, parsed.name, parsed.folderName);
|
|
34
|
+
const range = options.latest ? "latest" : manifestRange;
|
|
35
|
+
const nextVersion = (0, resolver_1.resolveVersion)(range, Object.keys(index.versions));
|
|
36
|
+
const lockedVersion = lockfile.skills[parsed.packageName]?.version ?? "none";
|
|
37
|
+
const shouldInstall = lockedVersion === "none" ||
|
|
38
|
+
!semver_1.default.valid(lockedVersion) ||
|
|
39
|
+
semver_1.default.gt(nextVersion, lockedVersion);
|
|
40
|
+
if (shouldInstall) {
|
|
41
|
+
const result = await (0, add_1.installRegistrySkill)(projectRoot, parsed, range, false);
|
|
42
|
+
lockfile.skills[parsed.packageName] = {
|
|
43
|
+
version: result.version,
|
|
44
|
+
integrity: result.integrity,
|
|
45
|
+
resolved: result.resolved
|
|
46
|
+
};
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
rows.push({
|
|
50
|
+
skill: parsed.packageName,
|
|
51
|
+
oldVersion: lockedVersion,
|
|
52
|
+
newVersion: shouldInstall ? nextVersion : lockedVersion,
|
|
53
|
+
changed: shouldInstall
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (changed) {
|
|
57
|
+
await (0, manifest_1.writeLockfile)(projectRoot, lockfile);
|
|
58
|
+
await (0, compile_1.compileCommand)();
|
|
59
|
+
}
|
|
60
|
+
printRows(rows);
|
|
61
|
+
}
|
|
62
|
+
async function getFreshSkillIndex(scope, name, cacheKey) {
|
|
63
|
+
const cached = await (0, cache_1.getCachedIndex)(cacheKey, INDEX_TTL_MS);
|
|
64
|
+
if (cached) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
const index = await (0, client_1.fetchSkillIndex)(scope, name);
|
|
68
|
+
await (0, cache_1.putCachedIndex)(cacheKey, index);
|
|
69
|
+
return index;
|
|
70
|
+
}
|
|
71
|
+
function printRows(rows) {
|
|
72
|
+
const skillWidth = Math.max("Skill".length, ...rows.map((row) => row.skill.length));
|
|
73
|
+
const oldWidth = Math.max("Old".length, ...rows.map((row) => row.oldVersion.length));
|
|
74
|
+
console.log(`${"Skill".padEnd(skillWidth)} ${"Old".padEnd(oldWidth)} New`);
|
|
75
|
+
console.log(`${"-".repeat(skillWidth)} ${"-".repeat(oldWidth)} ---`);
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
const marker = row.changed ? picocolors_1.default.green(row.newVersion) : row.newVersion;
|
|
78
|
+
console.log(`${row.skill.padEnd(skillWidth)} ${row.oldVersion.padEnd(oldWidth)} ${marker}`);
|
|
79
|
+
}
|
|
80
|
+
}
|