oris-skills 2.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.
- package/.cursor-plugin/plugin.json +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/agents/oris-loop-debriefer.md +29 -0
- package/agents/oris-loop-doctor.md +32 -0
- package/agents/oris-loop-executor.md +32 -0
- package/agents/oris-loop-verifier.md +31 -0
- package/docs/architecture.md +26 -0
- package/docs/distribution.md +38 -0
- package/docs/maintainer-guide.md +32 -0
- package/docs/user-guide.md +42 -0
- package/package.json +49 -0
- package/references/clean-code-checklist.md +107 -0
- package/references/conventions.md +39 -0
- package/references/doc-policy.md +24 -0
- package/references/loop-adapter.schema.json +126 -0
- package/references/loop-contract.md +119 -0
- package/references/loop.schema.json +143 -0
- package/references/questions.md +38 -0
- package/references/repo-map.md +53 -0
- package/references/repo-map.schema.json +198 -0
- package/references/settings.md +35 -0
- package/references/settings.schema.json +75 -0
- package/scripts/flow/oris-flow-clean-runtime.mjs +182 -0
- package/scripts/flow/oris-flow-layout.mjs +53 -0
- package/scripts/flow/oris-flow-scan.mjs +350 -0
- package/scripts/flow/oris-flow-version-control.mjs +42 -0
- package/scripts/flow/oris-gitignore.mjs +79 -0
- package/scripts/install/generate-agent-adapters.mjs +74 -0
- package/scripts/install/install-user-skills.mjs +271 -0
- package/scripts/install/uninstall-user-skills.mjs +163 -0
- package/scripts/loop/oris-loop-bootstrap.mjs +383 -0
- package/scripts/loop/oris-loop-bundle.mjs +22 -0
- package/scripts/loop/oris-loop-chat.mjs +408 -0
- package/scripts/loop/oris-loop-demo.mjs +180 -0
- package/scripts/loop/oris-loop-document.mjs +179 -0
- package/scripts/loop/oris-loop-dry-run.mjs +110 -0
- package/scripts/loop/oris-loop-fixtures.mjs +148 -0
- package/scripts/loop/oris-loop-list.mjs +81 -0
- package/scripts/loop/oris-loop-paths.mjs +232 -0
- package/scripts/loop/oris-loop-run.mjs +241 -0
- package/scripts/loop/oris-loop-stop.mjs +319 -0
- package/scripts/loop/oris-loop-templates.mjs +77 -0
- package/scripts/loop/oris-loop-verify.mjs +206 -0
- package/scripts/oris-skills.mjs +116 -0
- package/scripts/package-oris-skills.mjs +53 -0
- package/scripts/tests/run-all-tests.mjs +38 -0
- package/scripts/tests/test-agent-adapters.mjs +19 -0
- package/scripts/tests/test-oris-1-0-cleanliness.mjs +58 -0
- package/scripts/tests/test-oris-flow-clean-runtime.mjs +75 -0
- package/scripts/tests/test-oris-flow-scan.mjs +49 -0
- package/scripts/tests/test-oris-gitignore.mjs +35 -0
- package/scripts/tests/test-oris-loop-bootstrap.mjs +94 -0
- package/scripts/tests/test-oris-loop-document.mjs +112 -0
- package/scripts/tests/test-oris-loop-list.mjs +74 -0
- package/scripts/tests/test-oris-loop-run.mjs +45 -0
- package/scripts/tests/test-oris-loop-smoke.mjs +130 -0
- package/scripts/tests/test-oris-loop-stop.mjs +371 -0
- package/scripts/tests/test-routing-lifecycle.mjs +181 -0
- package/scripts/tests/test-schemas.mjs +55 -0
- package/skills/oris-flow/SKILL.md +55 -0
- package/skills/oris-flow/agents/openai.yaml +4 -0
- package/skills/oris-flow-criteria/SKILL.md +49 -0
- package/skills/oris-flow-discover/SKILL.md +58 -0
- package/skills/oris-flow-docs/SKILL.md +31 -0
- package/skills/oris-flow-fix/SKILL.md +42 -0
- package/skills/oris-flow-implement/SKILL.md +43 -0
- package/skills/oris-flow-plan/SKILL.md +51 -0
- package/skills/oris-flow-setup/SKILL.md +49 -0
- package/skills/oris-help/SKILL.md +30 -0
- package/skills/oris-help/agents/openai.yaml +4 -0
- package/skills/oris-loop/SKILL.md +66 -0
- package/skills/oris-loop/agents/openai.yaml +4 -0
- package/skills/oris-loop/references/craft.md +48 -0
- package/skills/oris-loop/references/improve.md +23 -0
- package/skills/oris-loop/references/run.md +30 -0
- package/skills/oris-loop/templates/debriefer.md +18 -0
- package/skills/oris-loop/templates/doctor.md +20 -0
- package/skills/oris-loop/templates/executor.md +19 -0
- package/skills/oris-loop/templates/orchestrator.md +33 -0
- package/skills/oris-loop/templates/verifier.md +21 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { applyOrisFlowVersionControl } from "./oris-gitignore.mjs";
|
|
6
|
+
|
|
7
|
+
function fail(message) {
|
|
8
|
+
console.error(message);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const options = {
|
|
14
|
+
repositoryRoot: "",
|
|
15
|
+
mode: "",
|
|
16
|
+
dryRun: false,
|
|
17
|
+
};
|
|
18
|
+
const readValue = (index, raw) => {
|
|
19
|
+
if (raw.includes("=")) return [index, raw.slice(raw.indexOf("=") + 1)];
|
|
20
|
+
if (index + 1 >= argv.length) fail(`Missing value for ${raw}.`);
|
|
21
|
+
return [index + 1, argv[index + 1]];
|
|
22
|
+
};
|
|
23
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
+
const raw = argv[i];
|
|
25
|
+
const key = raw.toLowerCase();
|
|
26
|
+
if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
|
|
27
|
+
else if (key === "--mode" || key.startsWith("--mode=")) [i, options.mode] = readValue(i, raw);
|
|
28
|
+
else if (key === "--dry-run") options.dryRun = true;
|
|
29
|
+
else fail(`Unknown argument: ${raw}`);
|
|
30
|
+
}
|
|
31
|
+
return options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const options = parseArgs(process.argv.slice(2));
|
|
35
|
+
if (!options.mode) fail("Pass --mode commit or --mode gitignore.");
|
|
36
|
+
if (options.mode !== "commit" && options.mode !== "gitignore") {
|
|
37
|
+
fail(`Unsupported mode '${options.mode}'. Use commit or gitignore.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const root = options.repositoryRoot ? path.resolve(options.repositoryRoot) : process.cwd();
|
|
41
|
+
const result = applyOrisFlowVersionControl(root, options.mode, options.dryRun);
|
|
42
|
+
console.log(JSON.stringify({ repositoryRoot: root.replace(/\\/g, "/"), mode: options.mode, ...result }, null, 2));
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const ORIS_FLOW_GITIGNORE_ENTRIES = [
|
|
5
|
+
".oris-flow/manifest.json",
|
|
6
|
+
".oris-flow/maps/",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const ORIS_FLOW_RUNTIME_GITIGNORE_ENTRY = ".oris-flow/runtime/";
|
|
10
|
+
export const ORIS_LOOP_GITIGNORE_ENTRY = ORIS_FLOW_RUNTIME_GITIGNORE_ENTRY;
|
|
11
|
+
|
|
12
|
+
function normalizeGitignoreLines(text) {
|
|
13
|
+
return text.replace(/\r\n/g, "\n").split("\n");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasEntry(lines, entry) {
|
|
17
|
+
const trimmed = entry.trim();
|
|
18
|
+
return lines.some((line) => {
|
|
19
|
+
const value = line.trim();
|
|
20
|
+
if (!value || value.startsWith("#")) return false;
|
|
21
|
+
return value === trimmed || value.replace(/\/$/, "") === trimmed.replace(/\/$/, "");
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readGitignore(repositoryRoot) {
|
|
26
|
+
const gitignorePath = path.join(repositoryRoot, ".gitignore");
|
|
27
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
28
|
+
return { gitignorePath, text: "", lines: [] };
|
|
29
|
+
}
|
|
30
|
+
const text = fs.readFileSync(gitignorePath, "utf8");
|
|
31
|
+
return { gitignorePath, text, lines: normalizeGitignoreLines(text) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ensureGitignoreEntries(repositoryRoot, entries, dryRun = false) {
|
|
35
|
+
const { gitignorePath, text, lines } = readGitignore(repositoryRoot);
|
|
36
|
+
const missing = entries.filter((entry) => !hasEntry(lines, entry));
|
|
37
|
+
if (missing.length === 0) {
|
|
38
|
+
return { action: "present", path: gitignorePath, entries: [] };
|
|
39
|
+
}
|
|
40
|
+
if (dryRun) {
|
|
41
|
+
return { action: "append", path: gitignorePath, entries: missing };
|
|
42
|
+
}
|
|
43
|
+
const next = text.trimEnd();
|
|
44
|
+
const suffix = missing.map((entry) => entry.trim()).join("\n");
|
|
45
|
+
fs.writeFileSync(gitignorePath, `${next ? `${next}\n` : ""}${suffix}\n`, "utf8");
|
|
46
|
+
return { action: next ? "appended" : "created", path: gitignorePath, entries: missing };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function removeGitignoreEntries(repositoryRoot, entries, dryRun = false) {
|
|
50
|
+
const { gitignorePath, text, lines } = readGitignore(repositoryRoot);
|
|
51
|
+
const targets = new Set(entries.map((entry) => entry.trim().replace(/\/$/, "")));
|
|
52
|
+
const kept = lines.filter((line) => {
|
|
53
|
+
const value = line.trim();
|
|
54
|
+
if (!value || value.startsWith("#")) return true;
|
|
55
|
+
const normalized = value.replace(/\/$/, "");
|
|
56
|
+
return !targets.has(normalized);
|
|
57
|
+
});
|
|
58
|
+
if (kept.length === lines.length) {
|
|
59
|
+
return { action: "absent", path: gitignorePath, entries: [] };
|
|
60
|
+
}
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
return { action: "remove", path: gitignorePath, entries: [...targets] };
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(gitignorePath, `${kept.join("\n").replace(/\n+$/, "")}\n`, "utf8");
|
|
65
|
+
return { action: "removed", path: gitignorePath, entries: [...targets] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {"commit"|"gitignore"} mode
|
|
70
|
+
*/
|
|
71
|
+
export function applyOrisFlowVersionControl(repositoryRoot, mode, dryRun = false) {
|
|
72
|
+
if (mode === "gitignore") {
|
|
73
|
+
return ensureGitignoreEntries(repositoryRoot, ORIS_FLOW_GITIGNORE_ENTRIES, dryRun);
|
|
74
|
+
}
|
|
75
|
+
if (mode === "commit") {
|
|
76
|
+
return removeGitignoreEntries(repositoryRoot, ORIS_FLOW_GITIGNORE_ENTRIES, dryRun);
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unsupported Oris Flow version control mode '${mode}'. Use commit or gitignore.`);
|
|
79
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
9
|
+
const sourceRoot = path.join(repoRoot, "agents");
|
|
10
|
+
|
|
11
|
+
function readAgent(filePath) {
|
|
12
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
13
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
14
|
+
if (!match) throw new Error(`Agent file is missing front matter: ${filePath}`);
|
|
15
|
+
const frontMatter = Object.fromEntries(
|
|
16
|
+
match[1]
|
|
17
|
+
.split(/\r?\n/)
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((line) => {
|
|
21
|
+
const index = line.indexOf(":");
|
|
22
|
+
if (index < 0) throw new Error(`Unsupported front matter line in ${filePath}: ${line}`);
|
|
23
|
+
return [line.slice(0, index).trim(), line.slice(index + 1).trim()];
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
return { frontMatter, body: match[2].trim() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeFile(filePath, content) {
|
|
30
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
31
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderMarkdownAgent(agent) {
|
|
35
|
+
return `---
|
|
36
|
+
name: ${agent.frontMatter.name}
|
|
37
|
+
description: ${agent.frontMatter.description}
|
|
38
|
+
readonly: ${agent.frontMatter.readonly ?? "false"}
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
${agent.body}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderCodexAgent(agent) {
|
|
46
|
+
const name = JSON.stringify(agent.frontMatter.name);
|
|
47
|
+
const description = JSON.stringify(agent.frontMatter.description);
|
|
48
|
+
const instructions = JSON.stringify(agent.body);
|
|
49
|
+
return `name = ${name}
|
|
50
|
+
description = ${description}
|
|
51
|
+
developer_instructions = ${instructions}
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function generateAgentAdapters(root = repoRoot) {
|
|
56
|
+
const agents = fs.readdirSync(sourceRoot)
|
|
57
|
+
.filter((name) => name.endsWith(".md"))
|
|
58
|
+
.map((name) => ({ name, ...readAgent(path.join(sourceRoot, name)) }));
|
|
59
|
+
|
|
60
|
+
for (const agent of agents) {
|
|
61
|
+
writeFile(path.join(root, ".cursor", "agents", agent.name), renderMarkdownAgent(agent));
|
|
62
|
+
writeFile(path.join(root, ".claude", "agents", agent.name), renderMarkdownAgent(agent));
|
|
63
|
+
writeFile(path.join(root, ".codex", "agents", agent.name.replace(/\.md$/, ".toml")), renderCodexAgent(agent));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return agents.map((agent) => agent.frontMatter.name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
70
|
+
const rootArg = process.argv.find((arg) => arg.startsWith("--root="));
|
|
71
|
+
const root = rootArg ? path.resolve(rootArg.slice("--root=".length)) : repoRoot;
|
|
72
|
+
const generated = generateAgentAdapters(root);
|
|
73
|
+
console.log(JSON.stringify({ generated }, null, 2));
|
|
74
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-agent installer.
|
|
5
|
+
*
|
|
6
|
+
* One bundle, many agents:
|
|
7
|
+
* - bundle (scripts + skills + references): ~/.oris/oris-skills
|
|
8
|
+
* - Cursor skills: ~/.cursor/skills/<name>/
|
|
9
|
+
* - Claude Code skills: ~/.claude/skills/<name>/
|
|
10
|
+
* - Codex prompts: ~/.codex/prompts/<name>.md (pointer to the bundled SKILL.md)
|
|
11
|
+
*
|
|
12
|
+
* Agents are auto-detected from existing home directories; override with
|
|
13
|
+
* --agents cursor,claude,codex.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import childProcess from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import process from "node:process";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const markerName = ".oris-skills-managed.json";
|
|
24
|
+
const installManifestName = ".oris-skills-install.json";
|
|
25
|
+
const nodeDownloadUrl = "https://nodejs.org/en/download";
|
|
26
|
+
const KNOWN_AGENTS = ["cursor", "claude", "codex"];
|
|
27
|
+
|
|
28
|
+
function fail(message) {
|
|
29
|
+
throw new Error(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function log(message) {
|
|
33
|
+
console.log(`[oris-skills] ${message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultPaths() {
|
|
37
|
+
const home = os.homedir();
|
|
38
|
+
return {
|
|
39
|
+
bundleRoot: path.join(home, ".oris", "oris-skills"),
|
|
40
|
+
settingsPath: path.join(home, ".oris", "settings.json"),
|
|
41
|
+
agentRoots: {
|
|
42
|
+
cursor: path.join(home, ".cursor"),
|
|
43
|
+
claude: path.join(home, ".claude"),
|
|
44
|
+
codex: path.join(home, ".codex"),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const paths = defaultPaths();
|
|
51
|
+
const options = {
|
|
52
|
+
dryRun: false,
|
|
53
|
+
force: false,
|
|
54
|
+
agents: [],
|
|
55
|
+
bundleRoot: paths.bundleRoot,
|
|
56
|
+
settingsPath: paths.settingsPath,
|
|
57
|
+
agentRoots: paths.agentRoots,
|
|
58
|
+
};
|
|
59
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
+
const raw = argv[i];
|
|
61
|
+
const key = raw.toLowerCase();
|
|
62
|
+
const value = () => {
|
|
63
|
+
if (raw.includes("=")) return raw.slice(raw.indexOf("=") + 1);
|
|
64
|
+
if (i + 1 >= argv.length) fail(`Missing value for ${raw}.`);
|
|
65
|
+
i += 1;
|
|
66
|
+
return argv[i];
|
|
67
|
+
};
|
|
68
|
+
if (key === "--dry-run") options.dryRun = true;
|
|
69
|
+
else if (key === "--force") options.force = true;
|
|
70
|
+
else if (key === "--agents" || key.startsWith("--agents=")) {
|
|
71
|
+
options.agents = value().split(",").map((name) => name.trim().toLowerCase()).filter(Boolean);
|
|
72
|
+
} else if (key === "--bundle-root" || key.startsWith("--bundle-root=")) options.bundleRoot = value();
|
|
73
|
+
else if (key === "--settings" || key.startsWith("--settings=")) options.settingsPath = value();
|
|
74
|
+
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.agentRoots.cursor = value();
|
|
75
|
+
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.agentRoots.claude = value();
|
|
76
|
+
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.agentRoots.codex = value();
|
|
77
|
+
else fail(`Unknown argument: ${raw}`);
|
|
78
|
+
}
|
|
79
|
+
for (const agent of options.agents) {
|
|
80
|
+
if (!KNOWN_AGENTS.includes(agent)) fail(`Unknown agent '${agent}'. Known: ${KNOWN_AGENTS.join(", ")}.`);
|
|
81
|
+
}
|
|
82
|
+
return options;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectAgents(agentRoots) {
|
|
86
|
+
return KNOWN_AGENTS.filter((agent) => fs.existsSync(agentRoots[agent]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function openNodeDownload() {
|
|
90
|
+
try {
|
|
91
|
+
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
92
|
+
const args = process.platform === "win32" ? ["/c", "start", "", nodeDownloadUrl] : [nodeDownloadUrl];
|
|
93
|
+
childProcess.spawn(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
94
|
+
} catch {
|
|
95
|
+
// Best effort only.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireNode() {
|
|
100
|
+
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
101
|
+
if (!Number.isFinite(major) || major < 20) {
|
|
102
|
+
openNodeDownload();
|
|
103
|
+
fail(`Node.js 20+ is required: ${nodeDownloadUrl}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assertInsideHome(target) {
|
|
108
|
+
const home = path.resolve(os.homedir());
|
|
109
|
+
const relative = path.relative(home, path.resolve(target));
|
|
110
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
111
|
+
fail(`Refusing to write outside ${home}: ${target}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readJson(filePath) {
|
|
116
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeJson(filePath, value) {
|
|
120
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
121
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function copy(source, target) {
|
|
125
|
+
fs.cpSync(source, target, { recursive: true, force: true, errorOnExist: false });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function skillName(skillDir) {
|
|
129
|
+
const text = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
|
|
130
|
+
const match = text.match(/^---\r?\n[\s\S]*?^name:\s*(.+?)\s*$/m);
|
|
131
|
+
if (!match) fail(`Skill is missing front matter name: ${skillDir}`);
|
|
132
|
+
return match[1].trim().replace(/^["']|["']$/g, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function skillDescription(skillDir) {
|
|
136
|
+
const text = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
|
|
137
|
+
const match = text.match(/^---\r?\n[\s\S]*?^description:\s*(.+?)\s*$/m);
|
|
138
|
+
return match ? match[1].trim() : "";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function manifestSkills(manifest, repoRoot) {
|
|
142
|
+
if (!Array.isArray(manifest.skills)) fail("Plugin manifest must declare skills as an array.");
|
|
143
|
+
return manifest.skills.map((entry) => {
|
|
144
|
+
const relativePath = String(entry).replace(/^[.\\/]+/, "");
|
|
145
|
+
const sourceDir = path.join(repoRoot, relativePath);
|
|
146
|
+
if (!fs.existsSync(path.join(sourceDir, "SKILL.md"))) fail(`Missing skill: ${relativePath}`);
|
|
147
|
+
return { name: skillName(sourceDir), relativePath, sourceDir };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function defaultSettings() {
|
|
152
|
+
return {
|
|
153
|
+
version: 1,
|
|
154
|
+
orisFlow: {
|
|
155
|
+
uiLanguage: "en",
|
|
156
|
+
artifactLanguage: "en",
|
|
157
|
+
},
|
|
158
|
+
defaultProfiles: {
|
|
159
|
+
testing: "default",
|
|
160
|
+
},
|
|
161
|
+
profiles: {
|
|
162
|
+
default: {
|
|
163
|
+
environment: "local",
|
|
164
|
+
values: {},
|
|
165
|
+
secrets: {},
|
|
166
|
+
flags: {
|
|
167
|
+
allowDataMutation: false,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function installSkillDirs(skills, skillsRoot, manifest, bundleRoot, options) {
|
|
175
|
+
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
176
|
+
for (const skill of skills) {
|
|
177
|
+
const target = path.join(skillsRoot, skill.name);
|
|
178
|
+
if (fs.existsSync(target) && !fs.existsSync(path.join(target, markerName)) && !options.force) {
|
|
179
|
+
fail(`Skill exists and is not managed by Oris: ${target}`);
|
|
180
|
+
}
|
|
181
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
182
|
+
copy(skill.sourceDir, target);
|
|
183
|
+
writeJson(path.join(target, markerName), {
|
|
184
|
+
package: manifest.name,
|
|
185
|
+
version: manifest.version,
|
|
186
|
+
sourcePath: skill.relativePath,
|
|
187
|
+
bundleRoot,
|
|
188
|
+
installedAt: new Date().toISOString(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function installCodexPrompts(skills, codexRoot, bundleRoot) {
|
|
194
|
+
const promptsRoot = path.join(codexRoot, "prompts");
|
|
195
|
+
fs.mkdirSync(promptsRoot, { recursive: true });
|
|
196
|
+
for (const skill of skills) {
|
|
197
|
+
const bundledSkill = path.join(bundleRoot, skill.relativePath, "SKILL.md").replace(/\\/g, "/");
|
|
198
|
+
const description = skillDescription(skill.sourceDir);
|
|
199
|
+
fs.writeFileSync(path.join(promptsRoot, `${skill.name}.md`), `<!-- ${markerName} -->
|
|
200
|
+
Read the Oris skill file at \`${bundledSkill}\` and follow it exactly.
|
|
201
|
+
${description ? `Skill purpose: ${description}\n` : ""}
|
|
202
|
+
User request: $ARGUMENTS
|
|
203
|
+
`, "utf8");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function main() {
|
|
208
|
+
requireNode();
|
|
209
|
+
const options = parseArgs(process.argv.slice(2));
|
|
210
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
211
|
+
const manifest = readJson(path.join(repoRoot, ".cursor-plugin", "plugin.json"));
|
|
212
|
+
const skills = manifestSkills(manifest, repoRoot);
|
|
213
|
+
const agents = options.agents.length > 0 ? options.agents : detectAgents(options.agentRoots);
|
|
214
|
+
if (agents.length === 0) {
|
|
215
|
+
fail("No agent detected (~/.cursor, ~/.claude, ~/.codex). Pass --agents cursor,claude,codex explicitly.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
assertInsideHome(options.bundleRoot);
|
|
219
|
+
assertInsideHome(options.settingsPath);
|
|
220
|
+
for (const agent of agents) assertInsideHome(options.agentRoots[agent]);
|
|
221
|
+
|
|
222
|
+
if (options.dryRun) {
|
|
223
|
+
log(`Would install ${skills.length} skills from ${repoRoot}`);
|
|
224
|
+
log(`Would write bundle: ${options.bundleRoot}`);
|
|
225
|
+
for (const agent of agents) {
|
|
226
|
+
if (agent === "codex") log(`Would write Codex prompts: ${path.join(options.agentRoots.codex, "prompts")}`);
|
|
227
|
+
else log(`Would write ${agent} skills: ${path.join(options.agentRoots[agent], "skills")}`);
|
|
228
|
+
}
|
|
229
|
+
log(`Would create settings if missing: ${options.settingsPath}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (fs.existsSync(options.bundleRoot) && !options.force) {
|
|
234
|
+
fail(`Bundle already exists. Re-run with --force: ${options.bundleRoot}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fs.rmSync(options.bundleRoot, { recursive: true, force: true });
|
|
238
|
+
fs.mkdirSync(options.bundleRoot, { recursive: true });
|
|
239
|
+
for (const item of [".cursor-plugin", "skills", "references", "agents", "scripts", "docs", "package.json", "README.md", "LICENSE"]) {
|
|
240
|
+
const source = path.join(repoRoot, item);
|
|
241
|
+
if (fs.existsSync(source)) copy(source, path.join(options.bundleRoot, item));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const agent of agents) {
|
|
245
|
+
if (agent === "codex") {
|
|
246
|
+
installCodexPrompts(skills, options.agentRoots.codex, options.bundleRoot);
|
|
247
|
+
log(`Installed ${skills.length} Codex prompts`);
|
|
248
|
+
} else {
|
|
249
|
+
installSkillDirs(skills, path.join(options.agentRoots[agent], "skills"), manifest, options.bundleRoot, options);
|
|
250
|
+
log(`Installed ${skills.length} ${agent} skills`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!fs.existsSync(options.settingsPath)) writeJson(options.settingsPath, defaultSettings());
|
|
255
|
+
writeJson(path.join(options.bundleRoot, installManifestName), {
|
|
256
|
+
package: manifest.name,
|
|
257
|
+
version: manifest.version,
|
|
258
|
+
agents,
|
|
259
|
+
skills: skills.map((skill) => skill.name),
|
|
260
|
+
installedAt: new Date().toISOString(),
|
|
261
|
+
});
|
|
262
|
+
log(`Done for agents: ${agents.join(", ")}. Reload your agent environment.`);
|
|
263
|
+
log("Next: type /oris-flow in your agent chat, or try the loop demo: oris-skills loop demo");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
main();
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(`[oris-skills] ERROR ${error.message}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/** Multi-agent uninstaller: removes Oris-owned assets for Cursor, Claude Code, and Codex. */
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const markerName = ".oris-skills-managed.json";
|
|
12
|
+
const installManifestName = ".oris-skills-install.json";
|
|
13
|
+
|
|
14
|
+
function fail(message) {
|
|
15
|
+
throw new Error(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function log(message) {
|
|
19
|
+
console.log(`[oris-skills] ${message}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const home = os.homedir();
|
|
24
|
+
const options = {
|
|
25
|
+
dryRun: false,
|
|
26
|
+
bundleRoot: path.join(home, ".oris", "oris-skills"),
|
|
27
|
+
settingsPath: path.join(home, ".oris", "settings.json"),
|
|
28
|
+
cursorRoot: path.join(home, ".cursor"),
|
|
29
|
+
claudeRoot: path.join(home, ".claude"),
|
|
30
|
+
codexRoot: path.join(home, ".codex"),
|
|
31
|
+
};
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
const raw = argv[i];
|
|
34
|
+
const key = raw.toLowerCase();
|
|
35
|
+
const value = () => {
|
|
36
|
+
if (raw.includes("=")) return raw.slice(raw.indexOf("=") + 1);
|
|
37
|
+
if (i + 1 >= argv.length) fail(`Missing value for ${raw}.`);
|
|
38
|
+
i += 1;
|
|
39
|
+
return argv[i];
|
|
40
|
+
};
|
|
41
|
+
if (key === "--dry-run") options.dryRun = true;
|
|
42
|
+
else if (key === "--bundle-root" || key.startsWith("--bundle-root=")) options.bundleRoot = value();
|
|
43
|
+
else if (key === "--settings" || key.startsWith("--settings=")) options.settingsPath = value();
|
|
44
|
+
else if (key === "--cursor-root" || key.startsWith("--cursor-root=")) options.cursorRoot = value();
|
|
45
|
+
else if (key === "--claude-root" || key.startsWith("--claude-root=")) options.claudeRoot = value();
|
|
46
|
+
else if (key === "--codex-root" || key.startsWith("--codex-root=")) options.codexRoot = value();
|
|
47
|
+
else fail(`Unknown argument: ${raw}`);
|
|
48
|
+
}
|
|
49
|
+
return options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertInsideHome(target) {
|
|
53
|
+
const home = path.resolve(os.homedir());
|
|
54
|
+
const relative = path.relative(home, path.resolve(target));
|
|
55
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
56
|
+
fail(`Refusing to remove outside ${home}: ${target}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeOrisPath(target) {
|
|
61
|
+
const name = path.basename(target).toLowerCase();
|
|
62
|
+
return name.includes("oris") || name.includes("cursor-plugin-install-backups");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJson(filePath) {
|
|
66
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function managedSkillDirs(skillsRoot) {
|
|
70
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
71
|
+
return fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
72
|
+
.filter((entry) => entry.isDirectory())
|
|
73
|
+
.map((entry) => path.join(skillsRoot, entry.name))
|
|
74
|
+
.filter((skillDir) => {
|
|
75
|
+
const markerPath = path.join(skillDir, markerName);
|
|
76
|
+
if (fs.existsSync(markerPath)) return true;
|
|
77
|
+
return path.basename(skillDir).toLowerCase().startsWith("oris-");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function managedCodexPrompts(codexRoot) {
|
|
82
|
+
const promptsRoot = path.join(codexRoot, "prompts");
|
|
83
|
+
if (!fs.existsSync(promptsRoot)) return [];
|
|
84
|
+
return fs.readdirSync(promptsRoot, { withFileTypes: true })
|
|
85
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().startsWith("oris-") && entry.name.endsWith(".md"))
|
|
86
|
+
.map((entry) => path.join(promptsRoot, entry.name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function legacyOrisPaths(options) {
|
|
90
|
+
return [
|
|
91
|
+
path.join(options.cursorRoot, "oris-skills"),
|
|
92
|
+
path.join(options.cursorRoot, "oris-flow"),
|
|
93
|
+
path.join(options.cursorRoot, "oris-loop"),
|
|
94
|
+
path.join(options.cursorRoot, "oris-skills.settings.json"),
|
|
95
|
+
path.join(options.cursorRoot, ".cursor-plugin-install-backups"),
|
|
96
|
+
path.join(options.cursorRoot, "plugins", "oris-skills"),
|
|
97
|
+
options.settingsPath,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function plannedRemovals(options) {
|
|
102
|
+
const paths = new Set([
|
|
103
|
+
options.bundleRoot,
|
|
104
|
+
...managedSkillDirs(path.join(options.cursorRoot, "skills")),
|
|
105
|
+
...managedSkillDirs(path.join(options.claudeRoot, "skills")),
|
|
106
|
+
...managedCodexPrompts(options.codexRoot),
|
|
107
|
+
...legacyOrisPaths(options),
|
|
108
|
+
]);
|
|
109
|
+
return [...paths].map((target) => path.resolve(target)).filter(safeOrisPath).sort();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function removeOrisMcpServers(userMcpPath, dryRun) {
|
|
113
|
+
if (!fs.existsSync(userMcpPath)) return { changed: false, path: userMcpPath };
|
|
114
|
+
const mcp = readJson(userMcpPath);
|
|
115
|
+
const servers = mcp.mcpServers ?? {};
|
|
116
|
+
const nextServers = Object.fromEntries(
|
|
117
|
+
Object.entries(servers).filter(([name]) => !name.toLowerCase().includes("oris")),
|
|
118
|
+
);
|
|
119
|
+
const changed = Object.keys(nextServers).length !== Object.keys(servers).length;
|
|
120
|
+
if (changed && !dryRun) {
|
|
121
|
+
fs.writeFileSync(userMcpPath, `${JSON.stringify({ ...mcp, mcpServers: nextServers }, null, 2)}\n`, "utf8");
|
|
122
|
+
}
|
|
123
|
+
return { changed, path: userMcpPath };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function uninstallUserSkills(rawArgs = []) {
|
|
127
|
+
const options = parseArgs(rawArgs);
|
|
128
|
+
for (const target of [options.bundleRoot, options.settingsPath, options.cursorRoot, options.claudeRoot, options.codexRoot]) {
|
|
129
|
+
assertInsideHome(target);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const removals = plannedRemovals(options);
|
|
133
|
+
log(options.dryRun ? "Dry run uninstall plan:" : "Uninstalling Oris-owned assets:");
|
|
134
|
+
for (const target of removals) log(`${options.dryRun ? "Would remove" : "Remove"} ${target}`);
|
|
135
|
+
|
|
136
|
+
// Legacy cleanup: older releases merged an Oris MCP server into Cursor's user config.
|
|
137
|
+
const mcpResult = removeOrisMcpServers(path.join(options.cursorRoot, "mcp.json"), options.dryRun);
|
|
138
|
+
if (mcpResult.changed) log(`${options.dryRun ? "Would update" : "Updated"} ${mcpResult.path}`);
|
|
139
|
+
|
|
140
|
+
if (!options.dryRun) {
|
|
141
|
+
for (const target of removals) {
|
|
142
|
+
if (!safeOrisPath(target)) fail(`Refusing non-Oris path: ${target}`);
|
|
143
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
dryRun: options.dryRun,
|
|
149
|
+
removals,
|
|
150
|
+
mcpChanged: mcpResult.changed,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1])) {
|
|
155
|
+
try {
|
|
156
|
+
uninstallUserSkills(process.argv.slice(2));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`[oris-skills] ERROR ${error.message}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { installManifestName, markerName };
|