@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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +400 -0
  3. package/README.ko.md +409 -0
  4. package/README.md +416 -0
  5. package/README.zh-CN.md +436 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +298 -0
  8. package/dist/extend.d.ts +13 -0
  9. package/dist/extend.js +286 -0
  10. package/dist/hook-parser.d.ts +15 -0
  11. package/dist/hook-parser.js +95 -0
  12. package/dist/i18n/index.d.ts +5 -0
  13. package/dist/i18n/index.js +20 -0
  14. package/dist/i18n/messages.d.ts +28 -0
  15. package/dist/i18n/messages.js +49 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/installer.d.ts +30 -0
  19. package/dist/installer.js +165 -0
  20. package/dist/marker.d.ts +11 -0
  21. package/dist/marker.js +25 -0
  22. package/dist/platforms/amazon-q.d.ts +2 -0
  23. package/dist/platforms/amazon-q.js +12 -0
  24. package/dist/platforms/antigravity.d.ts +2 -0
  25. package/dist/platforms/antigravity.js +12 -0
  26. package/dist/platforms/auggie.d.ts +2 -0
  27. package/dist/platforms/auggie.js +15 -0
  28. package/dist/platforms/claude.d.ts +2 -0
  29. package/dist/platforms/claude.js +7 -0
  30. package/dist/platforms/cline.d.ts +2 -0
  31. package/dist/platforms/cline.js +11 -0
  32. package/dist/platforms/codebuddy.d.ts +2 -0
  33. package/dist/platforms/codebuddy.js +16 -0
  34. package/dist/platforms/codex.d.ts +2 -0
  35. package/dist/platforms/codex.js +7 -0
  36. package/dist/platforms/continue.d.ts +2 -0
  37. package/dist/platforms/continue.js +16 -0
  38. package/dist/platforms/costrict.d.ts +2 -0
  39. package/dist/platforms/costrict.js +15 -0
  40. package/dist/platforms/crush.d.ts +2 -0
  41. package/dist/platforms/crush.js +19 -0
  42. package/dist/platforms/cursor.d.ts +2 -0
  43. package/dist/platforms/cursor.js +18 -0
  44. package/dist/platforms/factory.d.ts +2 -0
  45. package/dist/platforms/factory.js +15 -0
  46. package/dist/platforms/gemini.d.ts +2 -0
  47. package/dist/platforms/gemini.js +14 -0
  48. package/dist/platforms/github-copilot.d.ts +2 -0
  49. package/dist/platforms/github-copilot.js +12 -0
  50. package/dist/platforms/iflow.d.ts +2 -0
  51. package/dist/platforms/iflow.js +18 -0
  52. package/dist/platforms/index.d.ts +10 -0
  53. package/dist/platforms/index.js +68 -0
  54. package/dist/platforms/kilocode.d.ts +2 -0
  55. package/dist/platforms/kilocode.js +8 -0
  56. package/dist/platforms/kiro.d.ts +2 -0
  57. package/dist/platforms/kiro.js +12 -0
  58. package/dist/platforms/opencode.d.ts +2 -0
  59. package/dist/platforms/opencode.js +12 -0
  60. package/dist/platforms/pi.d.ts +2 -0
  61. package/dist/platforms/pi.js +12 -0
  62. package/dist/platforms/qoder.d.ts +2 -0
  63. package/dist/platforms/qoder.js +19 -0
  64. package/dist/platforms/qwen.d.ts +2 -0
  65. package/dist/platforms/qwen.js +14 -0
  66. package/dist/platforms/roocode.d.ts +2 -0
  67. package/dist/platforms/roocode.js +11 -0
  68. package/dist/platforms/toml.d.ts +9 -0
  69. package/dist/platforms/toml.js +18 -0
  70. package/dist/platforms/trae.d.ts +2 -0
  71. package/dist/platforms/trae.js +11 -0
  72. package/dist/platforms/types.d.ts +19 -0
  73. package/dist/platforms/types.js +1 -0
  74. package/dist/platforms/windsurf.d.ts +2 -0
  75. package/dist/platforms/windsurf.js +19 -0
  76. package/dist/platforms/yaml.d.ts +10 -0
  77. package/dist/platforms/yaml.js +30 -0
  78. package/dist/prompts/platform-select.d.ts +1 -0
  79. package/dist/prompts/platform-select.js +174 -0
  80. package/dist/skills/generate.d.ts +13 -0
  81. package/dist/skills/generate.js +16 -0
  82. package/dist/templates.d.ts +20 -0
  83. package/dist/templates.js +98 -0
  84. package/dist/ui/ascii-logo.d.ts +6 -0
  85. package/dist/ui/ascii-logo.js +45 -0
  86. package/dist/ui/colors.d.ts +6 -0
  87. package/dist/ui/colors.js +17 -0
  88. package/dist/ui/welcome.d.ts +1 -0
  89. package/dist/ui/welcome.js +77 -0
  90. package/package.json +51 -0
  91. package/templates/commands/context.md +92 -0
  92. package/templates/commands/design.md +113 -0
  93. package/templates/commands/go.md +84 -0
  94. package/templates/commands/goal.md +360 -0
  95. package/templates/commands/overview.md +78 -0
  96. package/templates/commands/recap.md +77 -0
  97. package/templates/commands/roadmap.md +99 -0
  98. package/templates/commands/scout.md +89 -0
  99. package/templates/commands/verify.md +140 -0
  100. package/templates/skills/opengoal-flow/SKILL.md +169 -0
  101. package/templates/skills/opengoal-subagent/SKILL.md +159 -0
package/dist/cli.js ADDED
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { install, dryRun, detectStale, removeStale } from "./installer.js";
5
+ import { DEFAULT_PREFIX } from "./templates.js";
6
+ import { PLATFORM_IDS, DEFAULT_PLATFORM, isValidPlatform, detectInstalledPlatforms } from "./platforms/index.js";
7
+ import { showWelcome } from "./ui/welcome.js";
8
+ import { selectPlatforms } from "./prompts/platform-select.js";
9
+ import { t } from "./i18n/index.js";
10
+ import chalk from "chalk";
11
+ import { goal, title } from "./ui/colors.js";
12
+ import { extend } from "./extend.js";
13
+ function printUsage() {
14
+ console.log(`
15
+ opengoal — Goal-centric AI harness. Open structure, clear present.
16
+
17
+ Usage:
18
+ opengoal init [options]
19
+ opengoal extend <path> [--force] [--root <dir>]
20
+
21
+ Options:
22
+ --docs <dir> Documentation directory (default: docs)
23
+ --codebase <dir> Codebase directory (default: .)
24
+ --tools <list> Target platforms: comma-separated, or "all" (default: claude)
25
+ Available: ${PLATFORM_IDS.join(", ")}
26
+ --no-subagent Disable subagent delegation (enabled by default)
27
+ --root <dir> Target directory (default: cwd)
28
+ --prefix <name> Slash-command prefix (default: opgl)
29
+ --from-prefix <name> Also detect stale files from this previous prefix
30
+ (the legacy "scaff" prefix is always checked)
31
+ --force Overwrite existing files without prompting
32
+ -y, --yes Auto-confirm with detected platforms (skip interactive selection)
33
+ --dry-run Preview without writing files
34
+ -h, --help Show this help
35
+
36
+ Commands:
37
+ init Install opengoal into the current project
38
+ extend <path> Install a local extension (reads <path>/opengoal-extend.yml)
39
+ `);
40
+ }
41
+ function parseArgs(argv) {
42
+ let command = null;
43
+ const positional = [];
44
+ let docsDir = "docs";
45
+ let codebaseDir = ".";
46
+ let tools = null;
47
+ let subagent = true;
48
+ let root = process.cwd();
49
+ let prefix = DEFAULT_PREFIX;
50
+ let prefixExplicit = false;
51
+ let fromPrefix = null;
52
+ let fromPrefixExplicit = false;
53
+ let force = false;
54
+ let yes = false;
55
+ let isDryRun = false;
56
+ let help = false;
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ if (arg === "-h" || arg === "--help") {
60
+ help = true;
61
+ }
62
+ else if (arg === "--docs") {
63
+ const next = argv[++i];
64
+ if (!next) {
65
+ console.error("Error: --docs requires a directory path");
66
+ process.exit(1);
67
+ }
68
+ docsDir = next;
69
+ }
70
+ else if (arg === "--codebase") {
71
+ const next = argv[++i];
72
+ if (!next) {
73
+ console.error("Error: --codebase requires a directory path");
74
+ process.exit(1);
75
+ }
76
+ codebaseDir = next;
77
+ }
78
+ else if (arg === "--tools") {
79
+ const next = argv[++i];
80
+ if (!next) {
81
+ console.error("Error: --tools requires a platform list");
82
+ process.exit(1);
83
+ }
84
+ if (next === "all") {
85
+ tools = [...PLATFORM_IDS];
86
+ }
87
+ else {
88
+ tools = next.split(",").map((id) => id.trim());
89
+ for (const id of tools) {
90
+ if (!isValidPlatform(id)) {
91
+ console.error(`Error: unknown platform "${id}". Available: ${PLATFORM_IDS.join(", ")}`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ else if (arg === "--no-subagent") {
98
+ subagent = false;
99
+ }
100
+ else if (arg === "--force") {
101
+ force = true;
102
+ }
103
+ else if (arg === "-y" || arg === "--yes") {
104
+ yes = true;
105
+ }
106
+ else if (arg === "--dry-run") {
107
+ isDryRun = true;
108
+ }
109
+ else if (arg === "--root") {
110
+ const next = argv[++i];
111
+ if (!next) {
112
+ console.error("Error: --root requires a directory path");
113
+ process.exit(1);
114
+ }
115
+ root = resolve(next);
116
+ }
117
+ else if (arg === "--prefix") {
118
+ const next = argv[++i];
119
+ if (!next) {
120
+ console.error("Error: --prefix requires a value");
121
+ process.exit(1);
122
+ }
123
+ if (!/^[a-z][a-z0-9-]{0,15}$/.test(next)) {
124
+ console.error(`Error: invalid --prefix "${next}". Must start with a lowercase letter and contain only lowercase letters, digits, and hyphens, 1-16 characters long.`);
125
+ process.exit(1);
126
+ }
127
+ prefix = next;
128
+ prefixExplicit = true;
129
+ }
130
+ else if (arg === "--from-prefix") {
131
+ const next = argv[++i];
132
+ if (!next) {
133
+ console.error("Error: --from-prefix requires a value");
134
+ process.exit(1);
135
+ }
136
+ if (!/^[a-z][a-z0-9-]{0,15}$/.test(next)) {
137
+ console.error(`Error: invalid --from-prefix "${next}". Must start with a lowercase letter and contain only lowercase letters, digits, and hyphens, 1-16 characters long.`);
138
+ process.exit(1);
139
+ }
140
+ fromPrefix = next;
141
+ fromPrefixExplicit = true;
142
+ }
143
+ else if (!arg.startsWith("-")) {
144
+ if (command === null) {
145
+ command = arg;
146
+ }
147
+ else {
148
+ positional.push(arg);
149
+ }
150
+ }
151
+ else {
152
+ console.error(`Unknown option: ${arg}`);
153
+ printUsage();
154
+ process.exit(1);
155
+ }
156
+ }
157
+ return { command, positional, docsDir, codebaseDir, tools, subagent, root, prefix, prefixExplicit, fromPrefix, fromPrefixExplicit, force, yes, dryRun: isDryRun, help };
158
+ }
159
+ async function runExtend(args) {
160
+ const extPath = args.positional[0];
161
+ if (!extPath) {
162
+ console.error("Error: opengoal extend requires a path argument");
163
+ printUsage();
164
+ process.exit(1);
165
+ }
166
+ const resolvedExt = resolve(extPath);
167
+ if (!existsSync(resolvedExt)) {
168
+ console.error(`Error: extension path does not exist: ${resolvedExt}`);
169
+ process.exit(1);
170
+ }
171
+ if (!existsSync(args.root)) {
172
+ console.error(`Error: target directory does not exist: ${args.root}`);
173
+ process.exit(1);
174
+ }
175
+ try {
176
+ const result = await extend(resolvedExt, {
177
+ force: args.force,
178
+ root: args.root,
179
+ prefix: args.prefixExplicit ? args.prefix : undefined,
180
+ });
181
+ if (result.commandsAdded.length > 0) {
182
+ console.log(`${chalk.green("\u2713")} Commands added: ${result.commandsAdded.join(", ")}`);
183
+ }
184
+ if (result.commandsSkipped.length > 0) {
185
+ console.log(`${chalk.yellow("~")} Commands skipped: ${result.commandsSkipped.join(", ")}`);
186
+ }
187
+ if (result.hooksApplied.length > 0) {
188
+ console.log(`${chalk.green("\u2713")} Hooks applied: ${result.hooksApplied.join(", ")}`);
189
+ }
190
+ if (result.dependenciesChecked.length > 0) {
191
+ console.log(`${chalk.green("\u2713")} Dependencies: all found`);
192
+ }
193
+ process.exit(0);
194
+ }
195
+ catch (err) {
196
+ console.error(`Error: ${err.message}`);
197
+ process.exit(1);
198
+ }
199
+ }
200
+ async function main() {
201
+ const args = parseArgs(process.argv.slice(2));
202
+ if (args.help || !args.command) {
203
+ printUsage();
204
+ process.exit(args.help ? 0 : 1);
205
+ }
206
+ if (args.command === "extend") {
207
+ await runExtend(args);
208
+ return;
209
+ }
210
+ if (args.command !== "init") {
211
+ console.error(`Unknown command: ${args.command}`);
212
+ printUsage();
213
+ process.exit(1);
214
+ }
215
+ if (!existsSync(args.root)) {
216
+ console.error(`Error: target directory does not exist: ${args.root}`);
217
+ process.exit(1);
218
+ }
219
+ let tools;
220
+ if (args.tools) {
221
+ tools = args.tools;
222
+ }
223
+ else if (args.yes) {
224
+ const detected = detectInstalledPlatforms(args.root);
225
+ tools = detected.length > 0 ? detected : [DEFAULT_PLATFORM];
226
+ console.log(chalk.dim(`Auto-selected: ${tools.join(", ")}`));
227
+ }
228
+ else if (process.stdin.isTTY && !args.dryRun) {
229
+ await showWelcome();
230
+ tools = await selectPlatforms(args.root);
231
+ }
232
+ else {
233
+ tools = [DEFAULT_PLATFORM];
234
+ }
235
+ const config = {
236
+ tools,
237
+ subagent: args.subagent,
238
+ root: args.root,
239
+ docsDir: args.docsDir,
240
+ codebaseDir: args.codebaseDir,
241
+ prefix: args.prefix,
242
+ force: args.force,
243
+ };
244
+ const oldPrefixes = Array.from(new Set(["scaff", ...(args.fromPrefixExplicit ? [args.fromPrefix] : [])])).filter((p) => p !== config.prefix);
245
+ const stale = detectStale(config, oldPrefixes);
246
+ if (args.dryRun) {
247
+ const result = dryRun(config);
248
+ console.log(t("cli.dryRun") + "\n");
249
+ for (const file of result.files) {
250
+ console.log(` ${chalk.green("+")} ${file}`);
251
+ }
252
+ if (stale.length > 0) {
253
+ console.log("\nStale files from a previous prefix (would be removed):");
254
+ for (const entry of stale) {
255
+ console.log(` ${chalk.red("-")} ${entry.dst}`);
256
+ }
257
+ }
258
+ return;
259
+ }
260
+ const result = await install(config);
261
+ console.log(t("cli.complete") + "\n");
262
+ if (result.files.length > 0) {
263
+ console.log(t("cli.installed"));
264
+ for (const file of result.files) {
265
+ console.log(` ${chalk.green("+")} ${file}`);
266
+ }
267
+ }
268
+ if (result.skipped.length > 0) {
269
+ console.log("\n" + t("cli.skipped"));
270
+ for (const file of result.skipped) {
271
+ console.log(` ${chalk.yellow("~")} ${file}`);
272
+ }
273
+ }
274
+ if (stale.length > 0) {
275
+ console.log("\nStale files from a previous prefix:");
276
+ for (const entry of stale) {
277
+ console.log(` ${chalk.yellow("!")} ${entry.dst}`);
278
+ }
279
+ const { removed, kept } = await removeStale(config, stale);
280
+ if (removed.length > 0) {
281
+ console.log("\nRemoved stale files:");
282
+ for (const file of removed) {
283
+ console.log(` ${chalk.red("-")} ${file}`);
284
+ }
285
+ }
286
+ if (kept.length > 0) {
287
+ console.log("\nLeft in place (not removed):");
288
+ for (const file of kept) {
289
+ console.log(` ${chalk.dim("~")} ${file}`);
290
+ }
291
+ }
292
+ }
293
+ console.log("\n" + title(t("cli.nextSteps")));
294
+ console.log(" " + chalk.dim("1.") + " " + t("cli.step1"));
295
+ console.log(" " + chalk.dim("2.") + " " + t("cli.step2").replace("{cmd}", goal(`/${args.prefix}:scout`)));
296
+ process.exit(0);
297
+ }
298
+ main();
@@ -0,0 +1,13 @@
1
+ export interface ExtendOptions {
2
+ force: boolean;
3
+ root: string;
4
+ prefix?: string;
5
+ }
6
+ export type PromptFn = (question: string) => Promise<string>;
7
+ export interface ExtendResult {
8
+ commandsAdded: string[];
9
+ commandsSkipped: string[];
10
+ hooksApplied: string[];
11
+ dependenciesChecked: string[];
12
+ }
13
+ export declare function extend(extensionPath: string, opts: ExtendOptions, promptFn?: PromptFn): Promise<ExtendResult>;
package/dist/extend.js ADDED
@@ -0,0 +1,286 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, } from "node:fs";
2
+ import { join, dirname, isAbsolute, resolve } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { createInterface } from "node:readline";
5
+ import { parse as parseYaml } from "yaml";
6
+ import { injectHooks } from "./hook-parser.js";
7
+ import { getAdapter } from "./platforms/index.js";
8
+ // `index.ts` only exposes adapters by id (no per-adapter named exports), so
9
+ // look the claude adapter up once here. It's always present in the adapter
10
+ // map (it's the platform default), so this can never be undefined.
11
+ const claude = getAdapter("claude");
12
+ function createDefaultPrompt() {
13
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
14
+ let closed = false;
15
+ rl.on("close", () => {
16
+ closed = true;
17
+ });
18
+ const prompt = (question) => {
19
+ if (closed)
20
+ return Promise.resolve("");
21
+ return new Promise((resolveP) => {
22
+ rl.question(question, (answer) => {
23
+ resolveP(answer.trim().toLowerCase());
24
+ });
25
+ rl.once("close", () => resolveP(""));
26
+ });
27
+ };
28
+ return { prompt, close: () => rl.close() };
29
+ }
30
+ function readManifest(extensionPath) {
31
+ const manifestPath = join(extensionPath, "opengoal-extend.yml");
32
+ if (!existsSync(manifestPath)) {
33
+ throw new Error(`Missing opengoal-extend.yml at ${manifestPath}`);
34
+ }
35
+ const raw = readFileSync(manifestPath, "utf-8");
36
+ let parsed;
37
+ try {
38
+ parsed = parseYaml(raw);
39
+ }
40
+ catch (err) {
41
+ throw new Error(`Failed to parse opengoal-extend.yml: ${err.message}`);
42
+ }
43
+ if (!parsed || typeof parsed !== "object") {
44
+ throw new Error("opengoal-extend.yml must be a YAML mapping");
45
+ }
46
+ return validateManifest(parsed);
47
+ }
48
+ function validateManifest(raw) {
49
+ if (typeof raw.name !== "string" || !raw.name) {
50
+ throw new Error("opengoal-extend.yml: 'name' is required");
51
+ }
52
+ if (typeof raw.extends !== "string" || !raw.extends) {
53
+ throw new Error("opengoal-extend.yml: 'extends' is required");
54
+ }
55
+ if (raw.extends !== "opengoal") {
56
+ throw new Error(`opengoal-extend.yml: 'extends' must be "opengoal" (got "${raw.extends}"). Extension chains are not supported yet.`);
57
+ }
58
+ const commands = [];
59
+ if (raw.commands !== undefined) {
60
+ if (!Array.isArray(raw.commands)) {
61
+ throw new Error("opengoal-extend.yml: 'commands' must be a list");
62
+ }
63
+ for (const c of raw.commands) {
64
+ if (typeof c !== "string" || !c) {
65
+ throw new Error("opengoal-extend.yml: each command must be a non-empty string");
66
+ }
67
+ commands.push(c);
68
+ }
69
+ }
70
+ const hooks = [];
71
+ if (raw.hooks !== undefined) {
72
+ if (!Array.isArray(raw.hooks)) {
73
+ throw new Error("opengoal-extend.yml: 'hooks' must be a list");
74
+ }
75
+ for (const h of raw.hooks) {
76
+ if (!h || typeof h !== "object") {
77
+ throw new Error("opengoal-extend.yml: each hook must be a mapping");
78
+ }
79
+ const hook = h;
80
+ if (typeof hook.id !== "string" || !hook.id) {
81
+ throw new Error("opengoal-extend.yml: hook 'id' is required");
82
+ }
83
+ if (typeof hook.target !== "string" || !hook.target) {
84
+ throw new Error(`opengoal-extend.yml: hook '${hook.id}' missing 'target'`);
85
+ }
86
+ const hasContent = hook.content !== undefined;
87
+ const hasFile = hook.file !== undefined;
88
+ if (hasContent && hasFile) {
89
+ throw new Error(`opengoal-extend.yml: hook '${hook.id}' cannot have both 'content' and 'file'`);
90
+ }
91
+ if (!hasContent && !hasFile) {
92
+ throw new Error(`opengoal-extend.yml: hook '${hook.id}' must have either 'content' or 'file'`);
93
+ }
94
+ if (hasContent && typeof hook.content !== "string") {
95
+ throw new Error(`opengoal-extend.yml: hook '${hook.id}' 'content' must be a string`);
96
+ }
97
+ if (hasFile && typeof hook.file !== "string") {
98
+ throw new Error(`opengoal-extend.yml: hook '${hook.id}' 'file' must be a string`);
99
+ }
100
+ hooks.push({
101
+ id: hook.id,
102
+ target: hook.target,
103
+ content: hasContent ? hook.content : undefined,
104
+ file: hasFile ? hook.file : undefined,
105
+ });
106
+ }
107
+ }
108
+ const dependencies = [];
109
+ if (raw.dependencies !== undefined) {
110
+ if (!Array.isArray(raw.dependencies)) {
111
+ throw new Error("opengoal-extend.yml: 'dependencies' must be a list");
112
+ }
113
+ for (const d of raw.dependencies) {
114
+ if (!d || typeof d !== "object") {
115
+ throw new Error("opengoal-extend.yml: each dependency must be a mapping");
116
+ }
117
+ const dep = d;
118
+ if (typeof dep.name !== "string" || !dep.name) {
119
+ throw new Error("opengoal-extend.yml: dependency 'name' is required");
120
+ }
121
+ if (typeof dep.check !== "string" || !dep.check) {
122
+ throw new Error(`opengoal-extend.yml: dependency '${dep.name}' missing 'check'`);
123
+ }
124
+ dependencies.push({ name: dep.name, check: dep.check });
125
+ }
126
+ }
127
+ return {
128
+ name: raw.name,
129
+ version: typeof raw.version === "string" ? raw.version : undefined,
130
+ extends: raw.extends,
131
+ requires: typeof raw.requires === "string" ? raw.requires : undefined,
132
+ commands,
133
+ hooks,
134
+ dependencies,
135
+ };
136
+ }
137
+ function checkDependencies(deps) {
138
+ for (const dep of deps) {
139
+ try {
140
+ execSync(dep.check, { stdio: "ignore" });
141
+ }
142
+ catch {
143
+ throw new Error(`Dependency '${dep.name}' not found (check: ${dep.check})`);
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Directory the claude adapter installs commands under, relative to a
149
+ * project root (currently `.claude/commands`). Derived from the adapter's
150
+ * own commandPath rather than hard-coded, so it stays in sync if the
151
+ * adapter's layout ever changes.
152
+ */
153
+ function claudeCommandsDir(root) {
154
+ const probePath = claude.commandPath("__probe__", "__prefix__");
155
+ return join(root, dirname(dirname(probePath)));
156
+ }
157
+ /**
158
+ * Detects the installed slash-command prefix by inspecting what's actually
159
+ * on disk under the claude adapter's command directory. A single
160
+ * subdirectory there is taken as the installed prefix. There is no state
161
+ * file to consult (this project guarantees zero runtime after install), so
162
+ * when the directory is missing, empty, or ambiguous we fail with a clear
163
+ * message rather than guess.
164
+ */
165
+ function detectPrefix(root) {
166
+ const commandsDir = claudeCommandsDir(root);
167
+ if (!existsSync(commandsDir)) {
168
+ throw new Error(`Cannot detect installed command prefix: ${commandsDir} does not exist. Pass --prefix explicitly.`);
169
+ }
170
+ const subdirs = readdirSync(commandsDir, { withFileTypes: true })
171
+ .filter((entry) => entry.isDirectory())
172
+ .map((entry) => entry.name);
173
+ if (subdirs.length === 0) {
174
+ throw new Error(`Cannot detect installed command prefix: no subdirectories found under ${commandsDir}. Pass --prefix explicitly.`);
175
+ }
176
+ if (subdirs.length > 1) {
177
+ throw new Error(`Cannot detect installed command prefix: multiple candidates found under ${commandsDir} (${subdirs.sort().join(", ")}). Pass --prefix explicitly.`);
178
+ }
179
+ return subdirs[0];
180
+ }
181
+ function resolvePrefix(root, explicit) {
182
+ return explicit ?? detectPrefix(root);
183
+ }
184
+ function resolveWithinExtension(extensionPath, relPath) {
185
+ if (isAbsolute(relPath))
186
+ return relPath;
187
+ return resolve(extensionPath, relPath);
188
+ }
189
+ async function copyCommands(extensionPath, commands, root, prefix, force, ask) {
190
+ const copied = [];
191
+ const skipped = [];
192
+ let forceAll = force;
193
+ for (const name of commands) {
194
+ const src = join(extensionPath, "commands", `${name}.md`);
195
+ if (!existsSync(src)) {
196
+ throw new Error(`Missing command source file: ${src}`);
197
+ }
198
+ const dst = join(root, claude.commandPath(name, prefix));
199
+ if (existsSync(dst) && !forceAll) {
200
+ const answer = await ask(`File exists: ${dst}. Overwrite? (y/n/a) `);
201
+ if (answer === "a" || answer === "all") {
202
+ forceAll = true;
203
+ }
204
+ else if (answer !== "y" && answer !== "yes") {
205
+ skipped.push(name);
206
+ continue;
207
+ }
208
+ }
209
+ const dir = dirname(dst);
210
+ if (!existsSync(dir)) {
211
+ mkdirSync(dir, { recursive: true });
212
+ }
213
+ const content = readFileSync(src, "utf-8");
214
+ writeFileSync(dst, content, "utf-8");
215
+ copied.push(name);
216
+ }
217
+ return { copied, skipped };
218
+ }
219
+ function applyHooks(extensionPath, hooks, root) {
220
+ const applied = [];
221
+ const byTarget = new Map();
222
+ for (const hook of hooks) {
223
+ const list = byTarget.get(hook.target) ?? [];
224
+ list.push(hook);
225
+ byTarget.set(hook.target, list);
226
+ }
227
+ for (const [target, specs] of byTarget) {
228
+ const targetPath = isAbsolute(target) ? target : join(root, target);
229
+ if (!existsSync(targetPath)) {
230
+ throw new Error(`Hook target file not found: ${targetPath}`);
231
+ }
232
+ const original = readFileSync(targetPath, "utf-8");
233
+ const entries = specs.map((spec) => {
234
+ let content;
235
+ if (spec.content !== undefined) {
236
+ content = spec.content;
237
+ }
238
+ else {
239
+ const filePath = resolveWithinExtension(extensionPath, spec.file);
240
+ if (!existsSync(filePath)) {
241
+ throw new Error(`Hook '${spec.id}' file not found: ${filePath}`);
242
+ }
243
+ content = readFileSync(filePath, "utf-8");
244
+ }
245
+ // Strip trailing newline so injected content fits cleanly between markers.
246
+ if (content.endsWith("\n")) {
247
+ content = content.slice(0, -1);
248
+ }
249
+ return { id: spec.id, content };
250
+ });
251
+ const next = injectHooks(original, entries);
252
+ writeFileSync(targetPath, next, "utf-8");
253
+ for (const spec of specs)
254
+ applied.push(spec.id);
255
+ }
256
+ return applied;
257
+ }
258
+ export async function extend(extensionPath, opts, promptFn) {
259
+ if (!existsSync(extensionPath)) {
260
+ throw new Error(`Extension path does not exist: ${extensionPath}`);
261
+ }
262
+ const manifest = readManifest(extensionPath);
263
+ const deps = manifest.dependencies ?? [];
264
+ checkDependencies(deps);
265
+ const commands = manifest.commands ?? [];
266
+ // Only resolve/detect a prefix when there are commands to install — an
267
+ // extension that only applies hooks or checks dependencies has no need
268
+ // for one, and shouldn't fail just because no prefix can be found.
269
+ const prefix = commands.length > 0 ? resolvePrefix(opts.root, opts.prefix) : "";
270
+ const defaultPrompt = promptFn ? null : createDefaultPrompt();
271
+ const ask = promptFn ?? defaultPrompt.prompt;
272
+ let copyResult;
273
+ try {
274
+ copyResult = await copyCommands(extensionPath, commands, opts.root, prefix, opts.force, ask);
275
+ }
276
+ finally {
277
+ defaultPrompt?.close();
278
+ }
279
+ const hooksApplied = applyHooks(extensionPath, manifest.hooks ?? [], opts.root);
280
+ return {
281
+ commandsAdded: copyResult.copied,
282
+ commandsSkipped: copyResult.skipped,
283
+ hooksApplied,
284
+ dependenciesChecked: deps.map((d) => d.name),
285
+ };
286
+ }
@@ -0,0 +1,15 @@
1
+ export interface HookEntry {
2
+ id: string;
3
+ content: string;
4
+ }
5
+ /**
6
+ * Validate that a file has properly paired hook markers.
7
+ * Returns list of hook ids found.
8
+ */
9
+ export declare function findHookPoints(fileContent: string): string[];
10
+ /**
11
+ * Inject hook content into a file's text.
12
+ * Returns the modified text.
13
+ * Throws on errors (missing markers, mismatched pairs, non-empty hooks).
14
+ */
15
+ export declare function injectHooks(fileContent: string, hooks: HookEntry[]): string;