@opellen/opengoal 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.
- package/LICENSE +21 -0
- package/README.ja.md +400 -0
- package/README.ko.md +409 -0
- package/README.md +416 -0
- package/README.zh-CN.md +436 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +298 -0
- package/dist/extend.d.ts +13 -0
- package/dist/extend.js +286 -0
- package/dist/hook-parser.d.ts +15 -0
- package/dist/hook-parser.js +95 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +20 -0
- package/dist/i18n/messages.d.ts +28 -0
- package/dist/i18n/messages.js +49 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/installer.d.ts +30 -0
- package/dist/installer.js +165 -0
- package/dist/marker.d.ts +11 -0
- package/dist/marker.js +25 -0
- package/dist/platforms/amazon-q.d.ts +2 -0
- package/dist/platforms/amazon-q.js +12 -0
- package/dist/platforms/antigravity.d.ts +2 -0
- package/dist/platforms/antigravity.js +12 -0
- package/dist/platforms/auggie.d.ts +2 -0
- package/dist/platforms/auggie.js +15 -0
- package/dist/platforms/claude.d.ts +2 -0
- package/dist/platforms/claude.js +7 -0
- package/dist/platforms/cline.d.ts +2 -0
- package/dist/platforms/cline.js +11 -0
- package/dist/platforms/codebuddy.d.ts +2 -0
- package/dist/platforms/codebuddy.js +16 -0
- package/dist/platforms/codex.d.ts +2 -0
- package/dist/platforms/codex.js +7 -0
- package/dist/platforms/continue.d.ts +2 -0
- package/dist/platforms/continue.js +16 -0
- package/dist/platforms/costrict.d.ts +2 -0
- package/dist/platforms/costrict.js +15 -0
- package/dist/platforms/crush.d.ts +2 -0
- package/dist/platforms/crush.js +19 -0
- package/dist/platforms/cursor.d.ts +2 -0
- package/dist/platforms/cursor.js +18 -0
- package/dist/platforms/factory.d.ts +2 -0
- package/dist/platforms/factory.js +15 -0
- package/dist/platforms/gemini.d.ts +2 -0
- package/dist/platforms/gemini.js +14 -0
- package/dist/platforms/github-copilot.d.ts +2 -0
- package/dist/platforms/github-copilot.js +12 -0
- package/dist/platforms/iflow.d.ts +2 -0
- package/dist/platforms/iflow.js +18 -0
- package/dist/platforms/index.d.ts +10 -0
- package/dist/platforms/index.js +68 -0
- package/dist/platforms/kilocode.d.ts +2 -0
- package/dist/platforms/kilocode.js +8 -0
- package/dist/platforms/kiro.d.ts +2 -0
- package/dist/platforms/kiro.js +12 -0
- package/dist/platforms/opencode.d.ts +2 -0
- package/dist/platforms/opencode.js +12 -0
- package/dist/platforms/pi.d.ts +2 -0
- package/dist/platforms/pi.js +12 -0
- package/dist/platforms/qoder.d.ts +2 -0
- package/dist/platforms/qoder.js +19 -0
- package/dist/platforms/qwen.d.ts +2 -0
- package/dist/platforms/qwen.js +14 -0
- package/dist/platforms/roocode.d.ts +2 -0
- package/dist/platforms/roocode.js +11 -0
- package/dist/platforms/toml.d.ts +9 -0
- package/dist/platforms/toml.js +18 -0
- package/dist/platforms/trae.d.ts +2 -0
- package/dist/platforms/trae.js +11 -0
- package/dist/platforms/types.d.ts +19 -0
- package/dist/platforms/types.js +1 -0
- package/dist/platforms/windsurf.d.ts +2 -0
- package/dist/platforms/windsurf.js +19 -0
- package/dist/platforms/yaml.d.ts +10 -0
- package/dist/platforms/yaml.js +30 -0
- package/dist/prompts/platform-select.d.ts +1 -0
- package/dist/prompts/platform-select.js +174 -0
- package/dist/skills/generate.d.ts +13 -0
- package/dist/skills/generate.js +16 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.js +98 -0
- package/dist/ui/ascii-logo.d.ts +6 -0
- package/dist/ui/ascii-logo.js +45 -0
- package/dist/ui/colors.d.ts +6 -0
- package/dist/ui/colors.js +17 -0
- package/dist/ui/welcome.d.ts +1 -0
- package/dist/ui/welcome.js +77 -0
- package/package.json +51 -0
- package/templates/commands/context.md +92 -0
- package/templates/commands/design.md +113 -0
- package/templates/commands/go.md +84 -0
- package/templates/commands/goal.md +360 -0
- package/templates/commands/overview.md +78 -0
- package/templates/commands/recap.md +77 -0
- package/templates/commands/roadmap.md +99 -0
- package/templates/commands/scout.md +89 -0
- package/templates/commands/verify.md +140 -0
- package/templates/skills/opengoal-flow/SKILL.md +169 -0
- package/templates/skills/opengoal-subagent/SKILL.md +159 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const HOOK_BEGIN = /^\s*<!--\s*@hook\s+(\S+)\s*-->/;
|
|
2
|
+
const HOOK_END = /^\s*<!--\s*@end-hook\s+(\S+)\s*-->/;
|
|
3
|
+
/**
|
|
4
|
+
* Validate that a file has properly paired hook markers.
|
|
5
|
+
* Returns list of hook ids found.
|
|
6
|
+
*/
|
|
7
|
+
export function findHookPoints(fileContent) {
|
|
8
|
+
const ids = [];
|
|
9
|
+
const lines = fileContent.split("\n");
|
|
10
|
+
let openId = null;
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const beginMatch = line.match(HOOK_BEGIN);
|
|
13
|
+
const endMatch = line.match(HOOK_END);
|
|
14
|
+
if (beginMatch) {
|
|
15
|
+
if (openId !== null) {
|
|
16
|
+
throw new Error(`Unclosed hook '${openId}'`);
|
|
17
|
+
}
|
|
18
|
+
openId = beginMatch[1];
|
|
19
|
+
}
|
|
20
|
+
else if (endMatch) {
|
|
21
|
+
const endId = endMatch[1];
|
|
22
|
+
if (openId === null) {
|
|
23
|
+
throw new Error(`Unexpected hook end '${endId}'`);
|
|
24
|
+
}
|
|
25
|
+
if (endId !== openId) {
|
|
26
|
+
throw new Error(`Mismatched hook end: expected '${openId}', got '${endId}'`);
|
|
27
|
+
}
|
|
28
|
+
ids.push(openId);
|
|
29
|
+
openId = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (openId !== null) {
|
|
33
|
+
throw new Error(`Unclosed hook '${openId}'`);
|
|
34
|
+
}
|
|
35
|
+
return ids;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Inject hook content into a file's text.
|
|
39
|
+
* Returns the modified text.
|
|
40
|
+
* Throws on errors (missing markers, mismatched pairs, non-empty hooks).
|
|
41
|
+
*/
|
|
42
|
+
export function injectHooks(fileContent, hooks) {
|
|
43
|
+
if (hooks.length === 0) {
|
|
44
|
+
return fileContent;
|
|
45
|
+
}
|
|
46
|
+
const lines = fileContent.split("\n");
|
|
47
|
+
const result = [];
|
|
48
|
+
const injected = new Set();
|
|
49
|
+
const hookMap = new Map(hooks.map((h) => [h.id, h.content]));
|
|
50
|
+
let openId = null;
|
|
51
|
+
let openLine = -1;
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
const beginMatch = line.match(HOOK_BEGIN);
|
|
55
|
+
const endMatch = line.match(HOOK_END);
|
|
56
|
+
if (beginMatch) {
|
|
57
|
+
if (openId !== null) {
|
|
58
|
+
throw new Error(`Unclosed hook '${openId}'`);
|
|
59
|
+
}
|
|
60
|
+
openId = beginMatch[1];
|
|
61
|
+
openLine = i;
|
|
62
|
+
result.push(line);
|
|
63
|
+
}
|
|
64
|
+
else if (endMatch) {
|
|
65
|
+
const endId = endMatch[1];
|
|
66
|
+
if (openId === null) {
|
|
67
|
+
throw new Error(`Unexpected hook end '${endId}'`);
|
|
68
|
+
}
|
|
69
|
+
if (endId !== openId) {
|
|
70
|
+
throw new Error(`Mismatched hook end: expected '${openId}', got '${endId}'`);
|
|
71
|
+
}
|
|
72
|
+
if (i !== openLine + 1) {
|
|
73
|
+
throw new Error(`Hook '${openId}' already has content`);
|
|
74
|
+
}
|
|
75
|
+
if (hookMap.has(openId)) {
|
|
76
|
+
result.push(hookMap.get(openId));
|
|
77
|
+
injected.add(openId);
|
|
78
|
+
}
|
|
79
|
+
result.push(line);
|
|
80
|
+
openId = null;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
result.push(line);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (openId !== null) {
|
|
87
|
+
throw new Error(`Unclosed hook '${openId}'`);
|
|
88
|
+
}
|
|
89
|
+
for (const hook of hooks) {
|
|
90
|
+
if (!injected.has(hook.id)) {
|
|
91
|
+
throw new Error(`Hook point '${hook.id}' not found in file`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result.join("\n");
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { messages } from "./messages.js";
|
|
2
|
+
function detectLocale() {
|
|
3
|
+
const raw = process.env.LC_ALL ||
|
|
4
|
+
process.env.LC_MESSAGES ||
|
|
5
|
+
process.env.LANG ||
|
|
6
|
+
"";
|
|
7
|
+
if (raw.startsWith("ko"))
|
|
8
|
+
return "ko";
|
|
9
|
+
return "en";
|
|
10
|
+
}
|
|
11
|
+
let current = detectLocale();
|
|
12
|
+
export function getLocale() {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
export function setLocale(locale) {
|
|
16
|
+
current = locale;
|
|
17
|
+
}
|
|
18
|
+
export function t(key) {
|
|
19
|
+
return messages[current][key];
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare const en: {
|
|
2
|
+
readonly "welcome.tagline": "Goal-centric AI harness. Open structure, clear present.";
|
|
3
|
+
readonly "welcome.setsUp": "Installs:";
|
|
4
|
+
readonly "welcome.skills": "Slash commands";
|
|
5
|
+
readonly "welcome.commands": "Agent skills";
|
|
6
|
+
readonly "welcome.pressEnter": "Press Enter to select tools.";
|
|
7
|
+
readonly "select.message": "Select tools";
|
|
8
|
+
readonly "select.typeToFilter": "type to filter";
|
|
9
|
+
readonly "select.detected": "detected";
|
|
10
|
+
readonly "select.selected": "selected";
|
|
11
|
+
readonly "select.none": "none";
|
|
12
|
+
readonly "select.error.empty": "Select at least one tool.";
|
|
13
|
+
readonly "select.hint": "↑↓ navigate • Space toggle • Backspace remove • Enter confirm";
|
|
14
|
+
readonly "select.available": "available";
|
|
15
|
+
readonly "select.selectedLabel": "Selected:";
|
|
16
|
+
readonly "select.searchLabel": "Search:";
|
|
17
|
+
readonly "cli.dryRun": "Dry run — files that would be created:";
|
|
18
|
+
readonly "cli.complete": "opengoal init complete.";
|
|
19
|
+
readonly "cli.installed": "Installed files:";
|
|
20
|
+
readonly "cli.skipped": "Skipped (use --force to overwrite):";
|
|
21
|
+
readonly "cli.nextSteps": "What's next:";
|
|
22
|
+
readonly "cli.step1": "Review installed files";
|
|
23
|
+
readonly "cli.step2": "Run {cmd}";
|
|
24
|
+
};
|
|
25
|
+
export type MessageKey = keyof typeof en;
|
|
26
|
+
export type Locale = "en" | "ko";
|
|
27
|
+
export declare const messages: Record<Locale, Record<MessageKey, string>>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const en = {
|
|
2
|
+
"welcome.tagline": "Goal-centric AI harness. Open structure, clear present.",
|
|
3
|
+
"welcome.setsUp": "Installs:",
|
|
4
|
+
"welcome.skills": "Slash commands",
|
|
5
|
+
"welcome.commands": "Agent skills",
|
|
6
|
+
"welcome.pressEnter": "Press Enter to select tools.",
|
|
7
|
+
"select.message": "Select tools",
|
|
8
|
+
"select.typeToFilter": "type to filter",
|
|
9
|
+
"select.detected": "detected",
|
|
10
|
+
"select.selected": "selected",
|
|
11
|
+
"select.none": "none",
|
|
12
|
+
"select.error.empty": "Select at least one tool.",
|
|
13
|
+
"select.hint": "↑↓ navigate • Space toggle • Backspace remove • Enter confirm",
|
|
14
|
+
"select.available": "available",
|
|
15
|
+
"select.selectedLabel": "Selected:",
|
|
16
|
+
"select.searchLabel": "Search:",
|
|
17
|
+
"cli.dryRun": "Dry run — files that would be created:",
|
|
18
|
+
"cli.complete": "opengoal init complete.",
|
|
19
|
+
"cli.installed": "Installed files:",
|
|
20
|
+
"cli.skipped": "Skipped (use --force to overwrite):",
|
|
21
|
+
"cli.nextSteps": "What's next:",
|
|
22
|
+
"cli.step1": "Review installed files",
|
|
23
|
+
"cli.step2": "Run {cmd}",
|
|
24
|
+
};
|
|
25
|
+
const ko = {
|
|
26
|
+
"welcome.tagline": "목표 중심 AI 하네스. 열린 구조, 선명한 현재.",
|
|
27
|
+
"welcome.setsUp": "포함:",
|
|
28
|
+
"welcome.skills": "슬래시 커맨드",
|
|
29
|
+
"welcome.commands": "에이전트 스킬",
|
|
30
|
+
"welcome.pressEnter": "Enter를 눌러 도구를 선택합니다.",
|
|
31
|
+
"select.message": "설치할 도구 선택",
|
|
32
|
+
"select.typeToFilter": "검색어 입력",
|
|
33
|
+
"select.detected": "감지됨",
|
|
34
|
+
"select.selected": "선택됨",
|
|
35
|
+
"select.none": "없음",
|
|
36
|
+
"select.error.empty": "하나 이상 선택해야 합니다.",
|
|
37
|
+
"select.hint": "↑↓ 이동 • Space 토글 • Backspace 제거 • Enter 확인",
|
|
38
|
+
"select.available": "개",
|
|
39
|
+
"select.selectedLabel": "선택:",
|
|
40
|
+
"select.searchLabel": "검색:",
|
|
41
|
+
"cli.dryRun": "드라이 런 — 생성될 파일:",
|
|
42
|
+
"cli.complete": "opengoal init 완료.",
|
|
43
|
+
"cli.installed": "설치된 파일:",
|
|
44
|
+
"cli.skipped": "건너뜀 (--force로 덮어쓰기):",
|
|
45
|
+
"cli.nextSteps": "다음:",
|
|
46
|
+
"cli.step1": "설치된 파일 확인",
|
|
47
|
+
"cli.step2": "{cmd} 실행",
|
|
48
|
+
};
|
|
49
|
+
export const messages = { en, ko };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { parseMarkers, renderTemplate, type MarkerBlock, type RenderContext } from "./marker.js";
|
|
2
|
+
export { resolveTemplates, renderFile, type OpenGoalConfig, type TemplateEntry } from "./templates.js";
|
|
3
|
+
export { install, dryRun, type InstallResult, type PromptFn } from "./installer.js";
|
|
4
|
+
export { getAdapter, getAdapters, getAllAdapters, isValidPlatform, PLATFORM_IDS, DEFAULT_PLATFORM, type PlatformId, type PlatformAdapter, type CommandMeta, } from "./platforms/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { parseMarkers, renderTemplate } from "./marker.js";
|
|
2
|
+
export { resolveTemplates, renderFile } from "./templates.js";
|
|
3
|
+
export { install, dryRun } from "./installer.js";
|
|
4
|
+
export { getAdapter, getAdapters, getAllAdapters, isValidPlatform, PLATFORM_IDS, DEFAULT_PLATFORM, } from "./platforms/index.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type OpenGoalConfig, type TemplateEntry } from "./templates.js";
|
|
2
|
+
export interface InstallResult {
|
|
3
|
+
files: string[];
|
|
4
|
+
skipped: string[];
|
|
5
|
+
}
|
|
6
|
+
export type PromptFn = (question: string) => Promise<string>;
|
|
7
|
+
export declare function install(config: OpenGoalConfig, prompt?: PromptFn): Promise<InstallResult>;
|
|
8
|
+
export declare function dryRun(config: OpenGoalConfig): {
|
|
9
|
+
files: string[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Finds command files left behind by a previous install under a stale
|
|
13
|
+
* prefix (e.g. after `--prefix` changes). Candidate paths are never
|
|
14
|
+
* globbed: they are exactly what resolveTemplates() would produce for the
|
|
15
|
+
* old prefix, so only files opengoal itself would have written are ever
|
|
16
|
+
* eligible. Skill entries are excluded (rule: skills don't carry the
|
|
17
|
+
* prefix, so they can never go stale), and the current prefix is never
|
|
18
|
+
* considered stale even if passed in explicitly.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectStale(config: OpenGoalConfig, oldPrefixes: string[]): TemplateEntry[];
|
|
21
|
+
/**
|
|
22
|
+
* Removes previously-detected stale files, with the same consent model as
|
|
23
|
+
* install(): prompt via the shared askUser style unless config.force is
|
|
24
|
+
* set, never delete silently, and report what was left behind if the user
|
|
25
|
+
* declines. Emptied directories are pruned afterward (see pruneEmptyDirs).
|
|
26
|
+
*/
|
|
27
|
+
export declare function removeStale(config: OpenGoalConfig, stale: TemplateEntry[], prompt?: PromptFn): Promise<{
|
|
28
|
+
removed: string[];
|
|
29
|
+
kept: string[];
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync, rmdirSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { join, dirname, resolve, sep } from "node:path";
|
|
4
|
+
import { resolveTemplates, renderFile } from "./templates.js";
|
|
5
|
+
function createDefaultPrompt() {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
let closed = false;
|
|
8
|
+
rl.on("close", () => { closed = true; });
|
|
9
|
+
const prompt = (question) => {
|
|
10
|
+
if (closed)
|
|
11
|
+
return Promise.resolve("");
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
resolve(answer.trim().toLowerCase());
|
|
15
|
+
});
|
|
16
|
+
rl.once("close", () => resolve(""));
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
return { prompt, close: () => rl.close() };
|
|
20
|
+
}
|
|
21
|
+
export async function install(config, prompt) {
|
|
22
|
+
const defaultPrompt = prompt ? null : createDefaultPrompt();
|
|
23
|
+
const askUser = prompt ?? defaultPrompt.prompt;
|
|
24
|
+
const entries = resolveTemplates(config);
|
|
25
|
+
const files = [];
|
|
26
|
+
const skipped = [];
|
|
27
|
+
let applyAll = null;
|
|
28
|
+
try {
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const dst = join(config.root, entry.dst);
|
|
31
|
+
const dir = dirname(dst);
|
|
32
|
+
if (existsSync(dst) && !config.force) {
|
|
33
|
+
if (applyAll === "skip") {
|
|
34
|
+
skipped.push(entry.dst);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (applyAll !== "overwrite") {
|
|
38
|
+
const answer = await askUser(` ${entry.dst} already exists. Overwrite? [y/N/a(all)/s(skip all)] `);
|
|
39
|
+
if (answer === "a" || answer === "all") {
|
|
40
|
+
applyAll = "overwrite";
|
|
41
|
+
}
|
|
42
|
+
else if (answer === "s") {
|
|
43
|
+
applyAll = "skip";
|
|
44
|
+
skipped.push(entry.dst);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
else if (answer !== "y" && answer !== "yes") {
|
|
48
|
+
skipped.push(entry.dst);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
const content = renderFile(entry, config);
|
|
57
|
+
writeFileSync(dst, content, "utf-8");
|
|
58
|
+
files.push(entry.dst);
|
|
59
|
+
}
|
|
60
|
+
const docsDir = join(config.root, config.docsDir);
|
|
61
|
+
if (!existsSync(docsDir)) {
|
|
62
|
+
mkdirSync(docsDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
defaultPrompt?.close();
|
|
67
|
+
}
|
|
68
|
+
return { files, skipped };
|
|
69
|
+
}
|
|
70
|
+
export function dryRun(config) {
|
|
71
|
+
const entries = resolveTemplates(config);
|
|
72
|
+
const files = entries.map((e) => e.dst);
|
|
73
|
+
return { files };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Finds command files left behind by a previous install under a stale
|
|
77
|
+
* prefix (e.g. after `--prefix` changes). Candidate paths are never
|
|
78
|
+
* globbed: they are exactly what resolveTemplates() would produce for the
|
|
79
|
+
* old prefix, so only files opengoal itself would have written are ever
|
|
80
|
+
* eligible. Skill entries are excluded (rule: skills don't carry the
|
|
81
|
+
* prefix, so they can never go stale), and the current prefix is never
|
|
82
|
+
* considered stale even if passed in explicitly.
|
|
83
|
+
*/
|
|
84
|
+
export function detectStale(config, oldPrefixes) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const stale = [];
|
|
87
|
+
for (const oldPrefix of oldPrefixes) {
|
|
88
|
+
if (oldPrefix === config.prefix)
|
|
89
|
+
continue;
|
|
90
|
+
const entries = resolveTemplates({ ...config, prefix: oldPrefix });
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.src.startsWith("skills/"))
|
|
93
|
+
continue;
|
|
94
|
+
if (seen.has(entry.dst))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(entry.dst);
|
|
97
|
+
if (existsSync(join(config.root, entry.dst))) {
|
|
98
|
+
stale.push(entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return stale;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Walks upward from startDir, removing directories that are now empty,
|
|
106
|
+
* stopping at (and never removing) boundaryDir itself, and stopping as
|
|
107
|
+
* soon as a directory still has contents. This is what lets directory-
|
|
108
|
+
* shaped adapters (.claude/commands/<prefix>/) and per-command-directory
|
|
109
|
+
* shapes (.codex/skills/<prefix>-<name>/) get cleaned up without any
|
|
110
|
+
* adapter-specific logic.
|
|
111
|
+
*/
|
|
112
|
+
function pruneEmptyDirs(startDir, boundaryDir) {
|
|
113
|
+
const boundary = resolve(boundaryDir);
|
|
114
|
+
let dir = resolve(startDir);
|
|
115
|
+
while (dir !== boundary && dir.startsWith(boundary + sep)) {
|
|
116
|
+
if (!existsSync(dir)) {
|
|
117
|
+
dir = dirname(dir);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (readdirSync(dir).length > 0)
|
|
121
|
+
break;
|
|
122
|
+
rmdirSync(dir);
|
|
123
|
+
dir = dirname(dir);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Removes previously-detected stale files, with the same consent model as
|
|
128
|
+
* install(): prompt via the shared askUser style unless config.force is
|
|
129
|
+
* set, never delete silently, and report what was left behind if the user
|
|
130
|
+
* declines. Emptied directories are pruned afterward (see pruneEmptyDirs).
|
|
131
|
+
*/
|
|
132
|
+
export async function removeStale(config, stale, prompt) {
|
|
133
|
+
const removed = [];
|
|
134
|
+
const kept = [];
|
|
135
|
+
if (stale.length === 0) {
|
|
136
|
+
return { removed, kept };
|
|
137
|
+
}
|
|
138
|
+
const defaultPrompt = prompt ? null : createDefaultPrompt();
|
|
139
|
+
const askUser = prompt ?? defaultPrompt.prompt;
|
|
140
|
+
try {
|
|
141
|
+
let proceed = config.force === true;
|
|
142
|
+
if (!proceed) {
|
|
143
|
+
const answer = await askUser(` Found ${stale.length} stale command file(s) from a previous prefix. Remove? [y/N] `);
|
|
144
|
+
proceed = answer === "y" || answer === "yes";
|
|
145
|
+
}
|
|
146
|
+
if (!proceed) {
|
|
147
|
+
for (const entry of stale)
|
|
148
|
+
kept.push(entry.dst);
|
|
149
|
+
return { removed, kept };
|
|
150
|
+
}
|
|
151
|
+
for (const entry of stale) {
|
|
152
|
+
const dst = join(config.root, entry.dst);
|
|
153
|
+
if (!existsSync(dst)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
unlinkSync(dst);
|
|
157
|
+
removed.push(entry.dst);
|
|
158
|
+
pruneEmptyDirs(join(config.root, dirname(entry.dst)), join(config.root, entry.adapter.configDir));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
defaultPrompt?.close();
|
|
163
|
+
}
|
|
164
|
+
return { removed, kept };
|
|
165
|
+
}
|
package/dist/marker.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MarkerBlock {
|
|
2
|
+
fullMatch: string;
|
|
3
|
+
condition: string;
|
|
4
|
+
ifBlock: string;
|
|
5
|
+
elseBlock: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface RenderContext {
|
|
8
|
+
conditions: Record<string, boolean>;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseMarkers(content: string): MarkerBlock[];
|
|
11
|
+
export declare function renderTemplate(content: string, ctx: RenderContext): string;
|
package/dist/marker.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const MARKER_SOURCE = "<!-- @if (\\w+) -->\\n([\\s\\S]*?)(?:<!-- @else -->\\n([\\s\\S]*?))?<!-- @endif -->";
|
|
2
|
+
export function parseMarkers(content) {
|
|
3
|
+
const blocks = [];
|
|
4
|
+
let match;
|
|
5
|
+
const regex = new RegExp(MARKER_SOURCE, "g");
|
|
6
|
+
while ((match = regex.exec(content)) !== null) {
|
|
7
|
+
blocks.push({
|
|
8
|
+
fullMatch: match[0],
|
|
9
|
+
condition: match[1],
|
|
10
|
+
ifBlock: match[2],
|
|
11
|
+
elseBlock: match[3],
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return blocks;
|
|
15
|
+
}
|
|
16
|
+
export function renderTemplate(content, ctx) {
|
|
17
|
+
const regex = new RegExp(MARKER_SOURCE, "g");
|
|
18
|
+
return content.replace(regex, (_match, condition, ifBlock, elseBlock) => {
|
|
19
|
+
const enabled = ctx.conditions[condition] ?? false;
|
|
20
|
+
if (enabled) {
|
|
21
|
+
return ifBlock.trimEnd();
|
|
22
|
+
}
|
|
23
|
+
return elseBlock?.trimEnd() ?? "";
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
|
|
2
|
+
export const amazonQ = {
|
|
3
|
+
id: "amazon-q",
|
|
4
|
+
displayName: "Amazon Q Developer",
|
|
5
|
+
configDir: ".amazonq",
|
|
6
|
+
commandPath: (name, prefix) => `.amazonq/prompts/${prefix}-${name}.md`,
|
|
7
|
+
formatCommand: (body, meta) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fm = yamlFrontmatter({ description: meta.description });
|
|
10
|
+
return fm + stripped;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
|
|
2
|
+
export const antigravity = {
|
|
3
|
+
id: "antigravity",
|
|
4
|
+
displayName: "Antigravity",
|
|
5
|
+
configDir: ".agent",
|
|
6
|
+
commandPath: (name, prefix) => `.agent/workflows/${prefix}-${name}.md`,
|
|
7
|
+
formatCommand: (body, meta) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fm = yamlFrontmatter({ description: meta.description });
|
|
10
|
+
return fm + stripped;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
|
|
2
|
+
export const auggie = {
|
|
3
|
+
id: "auggie",
|
|
4
|
+
displayName: "Augment CLI",
|
|
5
|
+
configDir: ".augment",
|
|
6
|
+
commandPath: (name, prefix) => `.augment/commands/${prefix}-${name}.md`,
|
|
7
|
+
formatCommand: (body, meta) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fm = yamlFrontmatter({
|
|
10
|
+
description: meta.description,
|
|
11
|
+
"argument-hint": "command arguments",
|
|
12
|
+
});
|
|
13
|
+
return fm + stripped;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { stripFrontmatter } from "./yaml.js";
|
|
2
|
+
export const cline = {
|
|
3
|
+
id: "cline",
|
|
4
|
+
displayName: "Cline",
|
|
5
|
+
configDir: ".cline",
|
|
6
|
+
commandPath: (name, prefix) => `.clinerules/workflows/${prefix}-${name}.md`,
|
|
7
|
+
formatCommand: (body, meta, prefix) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
return `# ${prefix}-${meta.name}\n\n${meta.description}\n\n${stripped}`;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
|
|
2
|
+
export const codebuddy = {
|
|
3
|
+
id: "codebuddy",
|
|
4
|
+
displayName: "CodeBuddy",
|
|
5
|
+
configDir: ".codebuddy",
|
|
6
|
+
commandPath: (name, prefix) => `.codebuddy/commands/${prefix}/${name}.md`,
|
|
7
|
+
formatCommand: (body, meta, prefix) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fm = yamlFrontmatter({
|
|
10
|
+
name: `${prefix}:${meta.name}`,
|
|
11
|
+
description: meta.description,
|
|
12
|
+
"argument-hint": "[command arguments]",
|
|
13
|
+
});
|
|
14
|
+
return fm + stripped;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
|
|
2
|
+
export const continueAdapter = {
|
|
3
|
+
id: "continue",
|
|
4
|
+
displayName: "Continue",
|
|
5
|
+
configDir: ".continue",
|
|
6
|
+
commandPath: (name, prefix) => `.continue/prompts/${prefix}-${name}.prompt`,
|
|
7
|
+
formatCommand: (body, meta, prefix) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fm = yamlFrontmatter({
|
|
10
|
+
name: `${prefix}-${meta.name}`,
|
|
11
|
+
description: meta.description,
|
|
12
|
+
invokable: "true",
|
|
13
|
+
});
|
|
14
|
+
return fm + stripped;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
|
|
2
|
+
export const costrict = {
|
|
3
|
+
id: "costrict",
|
|
4
|
+
displayName: "CoStrict",
|
|
5
|
+
configDir: ".cospec",
|
|
6
|
+
commandPath: (name, prefix) => `.cospec/${prefix}/commands/${prefix}-${name}.md`,
|
|
7
|
+
formatCommand: (body, meta) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fm = yamlFrontmatter({
|
|
10
|
+
description: meta.description,
|
|
11
|
+
"argument-hint": "command arguments",
|
|
12
|
+
});
|
|
13
|
+
return fm + stripped;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter, } from "./yaml.js";
|
|
2
|
+
export const crush = {
|
|
3
|
+
id: "crush",
|
|
4
|
+
displayName: "Crush",
|
|
5
|
+
configDir: ".crush",
|
|
6
|
+
commandPath: (name, prefix) => `.crush/commands/${prefix}/${name}.md`,
|
|
7
|
+
formatCommand: (body, meta, prefix) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fields = {
|
|
10
|
+
name: `${prefix}:${meta.name}`,
|
|
11
|
+
description: meta.description,
|
|
12
|
+
};
|
|
13
|
+
if (meta.category)
|
|
14
|
+
fields.category = meta.category;
|
|
15
|
+
if (meta.tags && meta.tags.length > 0)
|
|
16
|
+
fields.tags = meta.tags;
|
|
17
|
+
return yamlFrontmatter(fields) + stripped;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { stripFrontmatter, yamlFrontmatter, } from "./yaml.js";
|
|
2
|
+
export const cursor = {
|
|
3
|
+
id: "cursor",
|
|
4
|
+
displayName: "Cursor",
|
|
5
|
+
configDir: ".cursor",
|
|
6
|
+
commandPath: (name, prefix) => `.cursor/commands/${prefix}-${name}.md`,
|
|
7
|
+
formatCommand: (body, meta, prefix) => {
|
|
8
|
+
const stripped = stripFrontmatter(body);
|
|
9
|
+
const fields = {
|
|
10
|
+
name: `/${prefix}-${meta.name}`,
|
|
11
|
+
id: `${prefix}-${meta.name}`,
|
|
12
|
+
};
|
|
13
|
+
if (meta.category)
|
|
14
|
+
fields.category = meta.category;
|
|
15
|
+
fields.description = meta.description;
|
|
16
|
+
return yamlFrontmatter(fields) + stripped;
|
|
17
|
+
},
|
|
18
|
+
};
|