@opellen/scaff 0.1.11

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 (98) hide show
  1. package/README.ko.md +327 -0
  2. package/README.md +326 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +225 -0
  5. package/dist/extend.d.ts +12 -0
  6. package/dist/extend.js +242 -0
  7. package/dist/hook-parser.d.ts +15 -0
  8. package/dist/hook-parser.js +95 -0
  9. package/dist/i18n/index.d.ts +5 -0
  10. package/dist/i18n/index.js +20 -0
  11. package/dist/i18n/messages.d.ts +28 -0
  12. package/dist/i18n/messages.js +49 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/installer.d.ts +10 -0
  16. package/dist/installer.js +74 -0
  17. package/dist/marker.d.ts +11 -0
  18. package/dist/marker.js +25 -0
  19. package/dist/platforms/amazon-q.d.ts +2 -0
  20. package/dist/platforms/amazon-q.js +12 -0
  21. package/dist/platforms/antigravity.d.ts +2 -0
  22. package/dist/platforms/antigravity.js +12 -0
  23. package/dist/platforms/auggie.d.ts +2 -0
  24. package/dist/platforms/auggie.js +15 -0
  25. package/dist/platforms/claude.d.ts +2 -0
  26. package/dist/platforms/claude.js +7 -0
  27. package/dist/platforms/cline.d.ts +2 -0
  28. package/dist/platforms/cline.js +11 -0
  29. package/dist/platforms/codebuddy.d.ts +2 -0
  30. package/dist/platforms/codebuddy.js +16 -0
  31. package/dist/platforms/codex.d.ts +2 -0
  32. package/dist/platforms/codex.js +7 -0
  33. package/dist/platforms/continue.d.ts +2 -0
  34. package/dist/platforms/continue.js +16 -0
  35. package/dist/platforms/costrict.d.ts +2 -0
  36. package/dist/platforms/costrict.js +15 -0
  37. package/dist/platforms/crush.d.ts +2 -0
  38. package/dist/platforms/crush.js +19 -0
  39. package/dist/platforms/cursor.d.ts +2 -0
  40. package/dist/platforms/cursor.js +18 -0
  41. package/dist/platforms/factory.d.ts +2 -0
  42. package/dist/platforms/factory.js +15 -0
  43. package/dist/platforms/gemini.d.ts +2 -0
  44. package/dist/platforms/gemini.js +14 -0
  45. package/dist/platforms/github-copilot.d.ts +2 -0
  46. package/dist/platforms/github-copilot.js +12 -0
  47. package/dist/platforms/iflow.d.ts +2 -0
  48. package/dist/platforms/iflow.js +18 -0
  49. package/dist/platforms/index.d.ts +9 -0
  50. package/dist/platforms/index.js +57 -0
  51. package/dist/platforms/kilocode.d.ts +2 -0
  52. package/dist/platforms/kilocode.js +8 -0
  53. package/dist/platforms/kiro.d.ts +2 -0
  54. package/dist/platforms/kiro.js +12 -0
  55. package/dist/platforms/opencode.d.ts +2 -0
  56. package/dist/platforms/opencode.js +12 -0
  57. package/dist/platforms/pi.d.ts +2 -0
  58. package/dist/platforms/pi.js +12 -0
  59. package/dist/platforms/qoder.d.ts +2 -0
  60. package/dist/platforms/qoder.js +19 -0
  61. package/dist/platforms/qwen.d.ts +2 -0
  62. package/dist/platforms/qwen.js +14 -0
  63. package/dist/platforms/roocode.d.ts +2 -0
  64. package/dist/platforms/roocode.js +11 -0
  65. package/dist/platforms/toml.d.ts +9 -0
  66. package/dist/platforms/toml.js +18 -0
  67. package/dist/platforms/trae.d.ts +2 -0
  68. package/dist/platforms/trae.js +11 -0
  69. package/dist/platforms/types.d.ts +19 -0
  70. package/dist/platforms/types.js +1 -0
  71. package/dist/platforms/windsurf.d.ts +2 -0
  72. package/dist/platforms/windsurf.js +19 -0
  73. package/dist/platforms/yaml.d.ts +10 -0
  74. package/dist/platforms/yaml.js +30 -0
  75. package/dist/prompts/platform-select.d.ts +1 -0
  76. package/dist/prompts/platform-select.js +185 -0
  77. package/dist/skills/generate.d.ts +13 -0
  78. package/dist/skills/generate.js +16 -0
  79. package/dist/templates.d.ts +18 -0
  80. package/dist/templates.js +96 -0
  81. package/dist/ui/ascii-logo.d.ts +6 -0
  82. package/dist/ui/ascii-logo.js +38 -0
  83. package/dist/ui/colors.d.ts +2 -0
  84. package/dist/ui/colors.js +3 -0
  85. package/dist/ui/welcome.d.ts +1 -0
  86. package/dist/ui/welcome.js +77 -0
  87. package/package.json +51 -0
  88. package/templates/commands/context.md +93 -0
  89. package/templates/commands/design.md +108 -0
  90. package/templates/commands/go.md +56 -0
  91. package/templates/commands/goal.md +196 -0
  92. package/templates/commands/overview.md +78 -0
  93. package/templates/commands/recap.md +69 -0
  94. package/templates/commands/roadmap.md +95 -0
  95. package/templates/commands/scout.md +68 -0
  96. package/templates/commands/verify.md +112 -0
  97. package/templates/skills/scaff-flow/SKILL.md +107 -0
  98. package/templates/skills/scaff-subagent/SKILL.md +132 -0
package/dist/cli.js ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { install, dryRun } from "./installer.js";
5
+ import { PLATFORM_IDS, DEFAULT_PLATFORM, isValidPlatform } from "./platforms/index.js";
6
+ import { showWelcome } from "./ui/welcome.js";
7
+ import { selectPlatforms } from "./prompts/platform-select.js";
8
+ import { t } from "./i18n/index.js";
9
+ import chalk from "chalk";
10
+ import { brand } from "./ui/colors.js";
11
+ import { extend } from "./extend.js";
12
+ function printUsage() {
13
+ console.log(`
14
+ scaff — Lightweight context scaffolding for AI coding
15
+
16
+ Usage:
17
+ scaff init [options]
18
+ scaff extend <path> [--force] [--root <dir>]
19
+
20
+ Options:
21
+ --docs <dir> Documentation directory (default: docs)
22
+ --codebase <dir> Codebase directory (default: .)
23
+ --tools <list> Target platforms: comma-separated, or "all" (default: claude)
24
+ Available: ${PLATFORM_IDS.join(", ")}
25
+ --no-subagent Disable subagent delegation (enabled by default)
26
+ --root <dir> Target directory (default: cwd)
27
+ --force Overwrite existing files without prompting
28
+ --dry-run Preview without writing files
29
+ -h, --help Show this help
30
+
31
+ Commands:
32
+ init Install scaff into the current project
33
+ extend <path> Install a local extension (reads <path>/scaff-extend.yml)
34
+ `);
35
+ }
36
+ function parseArgs(argv) {
37
+ let command = null;
38
+ const positional = [];
39
+ let docsDir = "docs";
40
+ let codebaseDir = ".";
41
+ let tools = null;
42
+ let subagent = true;
43
+ let root = process.cwd();
44
+ let force = false;
45
+ let isDryRun = false;
46
+ let help = false;
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const arg = argv[i];
49
+ if (arg === "-h" || arg === "--help") {
50
+ help = true;
51
+ }
52
+ else if (arg === "--docs") {
53
+ const next = argv[++i];
54
+ if (!next) {
55
+ console.error("Error: --docs requires a directory path");
56
+ process.exit(1);
57
+ }
58
+ docsDir = next;
59
+ }
60
+ else if (arg === "--codebase") {
61
+ const next = argv[++i];
62
+ if (!next) {
63
+ console.error("Error: --codebase requires a directory path");
64
+ process.exit(1);
65
+ }
66
+ codebaseDir = next;
67
+ }
68
+ else if (arg === "--tools") {
69
+ const next = argv[++i];
70
+ if (!next) {
71
+ console.error("Error: --tools requires a platform list");
72
+ process.exit(1);
73
+ }
74
+ if (next === "all") {
75
+ tools = [...PLATFORM_IDS];
76
+ }
77
+ else {
78
+ tools = next.split(",").map((id) => id.trim());
79
+ for (const id of tools) {
80
+ if (!isValidPlatform(id)) {
81
+ console.error(`Error: unknown platform "${id}". Available: ${PLATFORM_IDS.join(", ")}`);
82
+ process.exit(1);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ else if (arg === "--no-subagent") {
88
+ subagent = false;
89
+ }
90
+ else if (arg === "--force") {
91
+ force = true;
92
+ }
93
+ else if (arg === "--dry-run") {
94
+ isDryRun = true;
95
+ }
96
+ else if (arg === "--root") {
97
+ const next = argv[++i];
98
+ if (!next) {
99
+ console.error("Error: --root requires a directory path");
100
+ process.exit(1);
101
+ }
102
+ root = resolve(next);
103
+ }
104
+ else if (!arg.startsWith("-")) {
105
+ if (command === null) {
106
+ command = arg;
107
+ }
108
+ else {
109
+ positional.push(arg);
110
+ }
111
+ }
112
+ else {
113
+ console.error(`Unknown option: ${arg}`);
114
+ printUsage();
115
+ process.exit(1);
116
+ }
117
+ }
118
+ return { command, positional, docsDir, codebaseDir, tools, subagent, root, force, dryRun: isDryRun, help };
119
+ }
120
+ async function runExtend(args) {
121
+ const extPath = args.positional[0];
122
+ if (!extPath) {
123
+ console.error("Error: scaff extend requires a path argument");
124
+ printUsage();
125
+ process.exit(1);
126
+ }
127
+ const resolvedExt = resolve(extPath);
128
+ if (!existsSync(resolvedExt)) {
129
+ console.error(`Error: extension path does not exist: ${resolvedExt}`);
130
+ process.exit(1);
131
+ }
132
+ if (!existsSync(args.root)) {
133
+ console.error(`Error: target directory does not exist: ${args.root}`);
134
+ process.exit(1);
135
+ }
136
+ try {
137
+ const result = await extend(resolvedExt, {
138
+ force: args.force,
139
+ root: args.root,
140
+ });
141
+ if (result.commandsAdded.length > 0) {
142
+ console.log(`${chalk.green("\u2713")} Commands added: ${result.commandsAdded.join(", ")}`);
143
+ }
144
+ if (result.commandsSkipped.length > 0) {
145
+ console.log(`${chalk.yellow("~")} Commands skipped: ${result.commandsSkipped.join(", ")}`);
146
+ }
147
+ if (result.hooksApplied.length > 0) {
148
+ console.log(`${chalk.green("\u2713")} Hooks applied: ${result.hooksApplied.join(", ")}`);
149
+ }
150
+ if (result.dependenciesChecked.length > 0) {
151
+ console.log(`${chalk.green("\u2713")} Dependencies: all found`);
152
+ }
153
+ process.exit(0);
154
+ }
155
+ catch (err) {
156
+ console.error(`Error: ${err.message}`);
157
+ process.exit(1);
158
+ }
159
+ }
160
+ async function main() {
161
+ const args = parseArgs(process.argv.slice(2));
162
+ if (args.help || !args.command) {
163
+ printUsage();
164
+ process.exit(args.help ? 0 : 1);
165
+ }
166
+ if (args.command === "extend") {
167
+ await runExtend(args);
168
+ return;
169
+ }
170
+ if (args.command !== "init") {
171
+ console.error(`Unknown command: ${args.command}`);
172
+ printUsage();
173
+ process.exit(1);
174
+ }
175
+ if (!existsSync(args.root)) {
176
+ console.error(`Error: target directory does not exist: ${args.root}`);
177
+ process.exit(1);
178
+ }
179
+ let tools;
180
+ if (args.tools) {
181
+ tools = args.tools;
182
+ }
183
+ else if (process.stdin.isTTY && !args.dryRun) {
184
+ await showWelcome();
185
+ tools = await selectPlatforms(args.root);
186
+ }
187
+ else {
188
+ tools = [DEFAULT_PLATFORM];
189
+ }
190
+ const config = {
191
+ tools,
192
+ subagent: args.subagent,
193
+ root: args.root,
194
+ docsDir: args.docsDir,
195
+ codebaseDir: args.codebaseDir,
196
+ force: args.force,
197
+ };
198
+ if (args.dryRun) {
199
+ const result = dryRun(config);
200
+ console.log(t("cli.dryRun") + "\n");
201
+ for (const file of result.files) {
202
+ console.log(` ${chalk.green("+")} ${file}`);
203
+ }
204
+ return;
205
+ }
206
+ const result = await install(config);
207
+ console.log(t("cli.complete") + "\n");
208
+ if (result.files.length > 0) {
209
+ console.log(t("cli.installed"));
210
+ for (const file of result.files) {
211
+ console.log(` ${chalk.green("+")} ${file}`);
212
+ }
213
+ }
214
+ if (result.skipped.length > 0) {
215
+ console.log("\n" + t("cli.skipped"));
216
+ for (const file of result.skipped) {
217
+ console.log(` ${chalk.yellow("~")} ${file}`);
218
+ }
219
+ }
220
+ console.log("\n" + brand(t("cli.nextSteps")));
221
+ console.log(" " + chalk.dim("1.") + " " + t("cli.step1"));
222
+ console.log(" " + chalk.dim("2.") + " " + t("cli.step2").replace("{cmd}", brand("/scaff:scout")));
223
+ process.exit(0);
224
+ }
225
+ main();
@@ -0,0 +1,12 @@
1
+ export interface ExtendOptions {
2
+ force: boolean;
3
+ root: string;
4
+ }
5
+ export type PromptFn = (question: string) => Promise<string>;
6
+ export interface ExtendResult {
7
+ commandsAdded: string[];
8
+ commandsSkipped: string[];
9
+ hooksApplied: string[];
10
+ dependenciesChecked: string[];
11
+ }
12
+ export declare function extend(extensionPath: string, opts: ExtendOptions, promptFn?: PromptFn): Promise<ExtendResult>;
package/dist/extend.js ADDED
@@ -0,0 +1,242 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, } 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
+ function createDefaultPrompt() {
8
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9
+ let closed = false;
10
+ rl.on("close", () => {
11
+ closed = true;
12
+ });
13
+ const prompt = (question) => {
14
+ if (closed)
15
+ return Promise.resolve("");
16
+ return new Promise((resolveP) => {
17
+ rl.question(question, (answer) => {
18
+ resolveP(answer.trim().toLowerCase());
19
+ });
20
+ rl.once("close", () => resolveP(""));
21
+ });
22
+ };
23
+ return { prompt, close: () => rl.close() };
24
+ }
25
+ function readManifest(extensionPath) {
26
+ const manifestPath = join(extensionPath, "scaff-extend.yml");
27
+ if (!existsSync(manifestPath)) {
28
+ throw new Error(`Missing scaff-extend.yml at ${manifestPath}`);
29
+ }
30
+ const raw = readFileSync(manifestPath, "utf-8");
31
+ let parsed;
32
+ try {
33
+ parsed = parseYaml(raw);
34
+ }
35
+ catch (err) {
36
+ throw new Error(`Failed to parse scaff-extend.yml: ${err.message}`);
37
+ }
38
+ if (!parsed || typeof parsed !== "object") {
39
+ throw new Error("scaff-extend.yml must be a YAML mapping");
40
+ }
41
+ return validateManifest(parsed);
42
+ }
43
+ function validateManifest(raw) {
44
+ if (typeof raw.name !== "string" || !raw.name) {
45
+ throw new Error("scaff-extend.yml: 'name' is required");
46
+ }
47
+ if (typeof raw.extends !== "string" || !raw.extends) {
48
+ throw new Error("scaff-extend.yml: 'extends' is required");
49
+ }
50
+ if (raw.extends !== "scaff") {
51
+ throw new Error(`scaff-extend.yml: 'extends' must be "scaff" (got "${raw.extends}"). Extension chains are not supported yet.`);
52
+ }
53
+ const commands = [];
54
+ if (raw.commands !== undefined) {
55
+ if (!Array.isArray(raw.commands)) {
56
+ throw new Error("scaff-extend.yml: 'commands' must be a list");
57
+ }
58
+ for (const c of raw.commands) {
59
+ if (typeof c !== "string" || !c) {
60
+ throw new Error("scaff-extend.yml: each command must be a non-empty string");
61
+ }
62
+ commands.push(c);
63
+ }
64
+ }
65
+ const hooks = [];
66
+ if (raw.hooks !== undefined) {
67
+ if (!Array.isArray(raw.hooks)) {
68
+ throw new Error("scaff-extend.yml: 'hooks' must be a list");
69
+ }
70
+ for (const h of raw.hooks) {
71
+ if (!h || typeof h !== "object") {
72
+ throw new Error("scaff-extend.yml: each hook must be a mapping");
73
+ }
74
+ const hook = h;
75
+ if (typeof hook.id !== "string" || !hook.id) {
76
+ throw new Error("scaff-extend.yml: hook 'id' is required");
77
+ }
78
+ if (typeof hook.target !== "string" || !hook.target) {
79
+ throw new Error(`scaff-extend.yml: hook '${hook.id}' missing 'target'`);
80
+ }
81
+ const hasContent = hook.content !== undefined;
82
+ const hasFile = hook.file !== undefined;
83
+ if (hasContent && hasFile) {
84
+ throw new Error(`scaff-extend.yml: hook '${hook.id}' cannot have both 'content' and 'file'`);
85
+ }
86
+ if (!hasContent && !hasFile) {
87
+ throw new Error(`scaff-extend.yml: hook '${hook.id}' must have either 'content' or 'file'`);
88
+ }
89
+ if (hasContent && typeof hook.content !== "string") {
90
+ throw new Error(`scaff-extend.yml: hook '${hook.id}' 'content' must be a string`);
91
+ }
92
+ if (hasFile && typeof hook.file !== "string") {
93
+ throw new Error(`scaff-extend.yml: hook '${hook.id}' 'file' must be a string`);
94
+ }
95
+ hooks.push({
96
+ id: hook.id,
97
+ target: hook.target,
98
+ content: hasContent ? hook.content : undefined,
99
+ file: hasFile ? hook.file : undefined,
100
+ });
101
+ }
102
+ }
103
+ const dependencies = [];
104
+ if (raw.dependencies !== undefined) {
105
+ if (!Array.isArray(raw.dependencies)) {
106
+ throw new Error("scaff-extend.yml: 'dependencies' must be a list");
107
+ }
108
+ for (const d of raw.dependencies) {
109
+ if (!d || typeof d !== "object") {
110
+ throw new Error("scaff-extend.yml: each dependency must be a mapping");
111
+ }
112
+ const dep = d;
113
+ if (typeof dep.name !== "string" || !dep.name) {
114
+ throw new Error("scaff-extend.yml: dependency 'name' is required");
115
+ }
116
+ if (typeof dep.check !== "string" || !dep.check) {
117
+ throw new Error(`scaff-extend.yml: dependency '${dep.name}' missing 'check'`);
118
+ }
119
+ dependencies.push({ name: dep.name, check: dep.check });
120
+ }
121
+ }
122
+ return {
123
+ name: raw.name,
124
+ version: typeof raw.version === "string" ? raw.version : undefined,
125
+ extends: raw.extends,
126
+ requires: typeof raw.requires === "string" ? raw.requires : undefined,
127
+ commands,
128
+ hooks,
129
+ dependencies,
130
+ };
131
+ }
132
+ function checkDependencies(deps) {
133
+ for (const dep of deps) {
134
+ try {
135
+ execSync(dep.check, { stdio: "ignore" });
136
+ }
137
+ catch {
138
+ throw new Error(`Dependency '${dep.name}' not found (check: ${dep.check})`);
139
+ }
140
+ }
141
+ }
142
+ function commandTargetPath(root, name) {
143
+ return join(root, ".claude", "commands", "scaff", `${name}.md`);
144
+ }
145
+ function resolveWithinExtension(extensionPath, relPath) {
146
+ if (isAbsolute(relPath))
147
+ return relPath;
148
+ return resolve(extensionPath, relPath);
149
+ }
150
+ async function copyCommands(extensionPath, commands, root, force, ask) {
151
+ const copied = [];
152
+ const skipped = [];
153
+ let forceAll = force;
154
+ for (const name of commands) {
155
+ const src = join(extensionPath, "commands", `${name}.md`);
156
+ if (!existsSync(src)) {
157
+ throw new Error(`Missing command source file: ${src}`);
158
+ }
159
+ const dst = commandTargetPath(root, name);
160
+ if (existsSync(dst) && !forceAll) {
161
+ const answer = await ask(`File exists: ${dst}. Overwrite? (y/n/a) `);
162
+ if (answer === "a" || answer === "all") {
163
+ forceAll = true;
164
+ }
165
+ else if (answer !== "y" && answer !== "yes") {
166
+ skipped.push(name);
167
+ continue;
168
+ }
169
+ }
170
+ const dir = dirname(dst);
171
+ if (!existsSync(dir)) {
172
+ mkdirSync(dir, { recursive: true });
173
+ }
174
+ const content = readFileSync(src, "utf-8");
175
+ writeFileSync(dst, content, "utf-8");
176
+ copied.push(name);
177
+ }
178
+ return { copied, skipped };
179
+ }
180
+ function applyHooks(extensionPath, hooks, root) {
181
+ const applied = [];
182
+ const byTarget = new Map();
183
+ for (const hook of hooks) {
184
+ const list = byTarget.get(hook.target) ?? [];
185
+ list.push(hook);
186
+ byTarget.set(hook.target, list);
187
+ }
188
+ for (const [target, specs] of byTarget) {
189
+ const targetPath = isAbsolute(target) ? target : join(root, target);
190
+ if (!existsSync(targetPath)) {
191
+ throw new Error(`Hook target file not found: ${targetPath}`);
192
+ }
193
+ const original = readFileSync(targetPath, "utf-8");
194
+ const entries = specs.map((spec) => {
195
+ let content;
196
+ if (spec.content !== undefined) {
197
+ content = spec.content;
198
+ }
199
+ else {
200
+ const filePath = resolveWithinExtension(extensionPath, spec.file);
201
+ if (!existsSync(filePath)) {
202
+ throw new Error(`Hook '${spec.id}' file not found: ${filePath}`);
203
+ }
204
+ content = readFileSync(filePath, "utf-8");
205
+ }
206
+ // Strip trailing newline so injected content fits cleanly between markers.
207
+ if (content.endsWith("\n")) {
208
+ content = content.slice(0, -1);
209
+ }
210
+ return { id: spec.id, content };
211
+ });
212
+ const next = injectHooks(original, entries);
213
+ writeFileSync(targetPath, next, "utf-8");
214
+ for (const spec of specs)
215
+ applied.push(spec.id);
216
+ }
217
+ return applied;
218
+ }
219
+ export async function extend(extensionPath, opts, promptFn) {
220
+ if (!existsSync(extensionPath)) {
221
+ throw new Error(`Extension path does not exist: ${extensionPath}`);
222
+ }
223
+ const manifest = readManifest(extensionPath);
224
+ const deps = manifest.dependencies ?? [];
225
+ checkDependencies(deps);
226
+ const defaultPrompt = promptFn ? null : createDefaultPrompt();
227
+ const ask = promptFn ?? defaultPrompt.prompt;
228
+ let copyResult;
229
+ try {
230
+ copyResult = await copyCommands(extensionPath, manifest.commands ?? [], opts.root, opts.force, ask);
231
+ }
232
+ finally {
233
+ defaultPrompt?.close();
234
+ }
235
+ const hooksApplied = applyHooks(extensionPath, manifest.hooks ?? [], opts.root);
236
+ return {
237
+ commandsAdded: copyResult.copied,
238
+ commandsSkipped: copyResult.skipped,
239
+ hooksApplied,
240
+ dependenciesChecked: deps.map((d) => d.name),
241
+ };
242
+ }
@@ -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;
@@ -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,5 @@
1
+ import { type Locale, type MessageKey } from "./messages.js";
2
+ export declare function getLocale(): Locale;
3
+ export declare function setLocale(locale: Locale): void;
4
+ export declare function t(key: MessageKey): string;
5
+ export type { Locale, MessageKey };
@@ -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": "Context scaffolding for AI coding. Just markdown.";
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": "scaff 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 {};