@opellen/scaff 0.1.13 → 0.1.15
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/package.json +2 -1
- package/templates/commands/go.md +1 -1
- package/templates/skills/scaff-flow/SKILL.md +8 -4
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -235
- package/dist/extend.d.ts +0 -12
- package/dist/extend.js +0 -242
- package/dist/hook-parser.d.ts +0 -15
- package/dist/hook-parser.js +0 -95
- package/dist/i18n/index.d.ts +0 -5
- package/dist/i18n/index.js +0 -20
- package/dist/i18n/messages.d.ts +0 -28
- package/dist/i18n/messages.js +0 -49
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/dist/installer.d.ts +0 -10
- package/dist/installer.js +0 -74
- package/dist/marker.d.ts +0 -11
- package/dist/marker.js +0 -25
- package/dist/platforms/amazon-q.d.ts +0 -2
- package/dist/platforms/amazon-q.js +0 -12
- package/dist/platforms/antigravity.d.ts +0 -2
- package/dist/platforms/antigravity.js +0 -12
- package/dist/platforms/auggie.d.ts +0 -2
- package/dist/platforms/auggie.js +0 -15
- package/dist/platforms/claude.d.ts +0 -2
- package/dist/platforms/claude.js +0 -7
- package/dist/platforms/cline.d.ts +0 -2
- package/dist/platforms/cline.js +0 -11
- package/dist/platforms/codebuddy.d.ts +0 -2
- package/dist/platforms/codebuddy.js +0 -16
- package/dist/platforms/codex.d.ts +0 -2
- package/dist/platforms/codex.js +0 -7
- package/dist/platforms/continue.d.ts +0 -2
- package/dist/platforms/continue.js +0 -16
- package/dist/platforms/costrict.d.ts +0 -2
- package/dist/platforms/costrict.js +0 -15
- package/dist/platforms/crush.d.ts +0 -2
- package/dist/platforms/crush.js +0 -19
- package/dist/platforms/cursor.d.ts +0 -2
- package/dist/platforms/cursor.js +0 -18
- package/dist/platforms/factory.d.ts +0 -2
- package/dist/platforms/factory.js +0 -15
- package/dist/platforms/gemini.d.ts +0 -2
- package/dist/platforms/gemini.js +0 -14
- package/dist/platforms/github-copilot.d.ts +0 -2
- package/dist/platforms/github-copilot.js +0 -12
- package/dist/platforms/iflow.d.ts +0 -2
- package/dist/platforms/iflow.js +0 -18
- package/dist/platforms/index.d.ts +0 -10
- package/dist/platforms/index.js +0 -68
- package/dist/platforms/kilocode.d.ts +0 -2
- package/dist/platforms/kilocode.js +0 -8
- package/dist/platforms/kiro.d.ts +0 -2
- package/dist/platforms/kiro.js +0 -12
- package/dist/platforms/opencode.d.ts +0 -2
- package/dist/platforms/opencode.js +0 -12
- package/dist/platforms/pi.d.ts +0 -2
- package/dist/platforms/pi.js +0 -12
- package/dist/platforms/qoder.d.ts +0 -2
- package/dist/platforms/qoder.js +0 -19
- package/dist/platforms/qwen.d.ts +0 -2
- package/dist/platforms/qwen.js +0 -14
- package/dist/platforms/roocode.d.ts +0 -2
- package/dist/platforms/roocode.js +0 -11
- package/dist/platforms/toml.d.ts +0 -9
- package/dist/platforms/toml.js +0 -18
- package/dist/platforms/trae.d.ts +0 -2
- package/dist/platforms/trae.js +0 -11
- package/dist/platforms/types.d.ts +0 -19
- package/dist/platforms/types.js +0 -1
- package/dist/platforms/windsurf.d.ts +0 -2
- package/dist/platforms/windsurf.js +0 -19
- package/dist/platforms/yaml.d.ts +0 -10
- package/dist/platforms/yaml.js +0 -30
- package/dist/prompts/platform-select.d.ts +0 -1
- package/dist/prompts/platform-select.js +0 -174
- package/dist/skills/generate.d.ts +0 -13
- package/dist/skills/generate.js +0 -16
- package/dist/templates.d.ts +0 -18
- package/dist/templates.js +0 -96
- package/dist/ui/ascii-logo.d.ts +0 -6
- package/dist/ui/ascii-logo.js +0 -38
- package/dist/ui/colors.d.ts +0 -2
- package/dist/ui/colors.js +0 -3
- package/dist/ui/welcome.d.ts +0 -1
- package/dist/ui/welcome.js +0 -77
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opellen/scaff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Agile, goal-centric AI harness. Just markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@inquirer/core": "^11.1.7",
|
|
47
47
|
"@inquirer/prompts": "^8.3.2",
|
|
48
48
|
"chalk": "^5.6.2",
|
|
49
|
+
"scaff": "^0.0.4",
|
|
49
50
|
"yaml": "^2.8.3"
|
|
50
51
|
}
|
|
51
52
|
}
|
package/templates/commands/go.md
CHANGED
|
@@ -31,7 +31,7 @@ tags: [workflow, scaff, go]
|
|
|
31
31
|
- (PLAN.md absent, task complexity ≥ multi-file) => generate PLAN.md, then report: `"Created [PLAN.md]($DocsDir/PLAN.md)."`
|
|
32
32
|
- (PLAN.md absent, task complexity < multi-file) => proceed without PLAN.md
|
|
33
33
|
5. Execute tasks using the Task Outcome loop below.
|
|
34
|
-
6.
|
|
34
|
+
6. Suggest `/scaff:goal checkpoint` only per the checkpoint triggers in the `scaff-flow` skill (platform context-pressure signal, or user pause/session end) — never on task count or goal completion.
|
|
35
35
|
|
|
36
36
|
## Task Outcome
|
|
37
37
|
|
|
@@ -73,10 +73,14 @@ When the trigger fires:
|
|
|
73
73
|
|
|
74
74
|
## Checkpoint Triggers
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
A checkpoint exists to survive context loss (compaction) *mid-goal*. Only two things justify suggesting one:
|
|
77
|
+
|
|
78
|
+
1. (platform surfaces a compaction-imminent / auto-compact / context-limit notice, or exposes actual context-usage figures nearing the limit — e.g. an injected system warning) => suggest `/scaff:goal checkpoint` NOW. This is the only reliable context-pressure signal. Never estimate your own context % or infer pressure from how long the session "feels" — that estimate tracks turn count, not tokens, and misfires early.
|
|
79
|
+
2. (user explicitly asks to save/pause, or is ending the session) => suggest `/scaff:goal checkpoint`.
|
|
80
|
+
|
|
81
|
+
On platforms that inject no warning, rule 2 is the only trigger — this is intentional: GOAL.md checkboxes and docs/logs already persist state as work completes, so a missed checkpoint is cheap.
|
|
82
|
+
|
|
83
|
+
> Do NOT suggest a checkpoint on task count, milestone/goal completion, or a sense that the session has "grown long." At goal completion the right move is `/scaff:goal archive`, not a checkpoint. When in doubt, stay silent.
|
|
80
84
|
|
|
81
85
|
## Blocker Handling
|
|
82
86
|
|
package/dist/cli.d.ts
DELETED
package/dist/cli.js
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
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, detectInstalledPlatforms } 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
|
-
-y, --yes Auto-confirm with detected platforms (skip interactive selection)
|
|
29
|
-
--dry-run Preview without writing files
|
|
30
|
-
-h, --help Show this help
|
|
31
|
-
|
|
32
|
-
Commands:
|
|
33
|
-
init Install scaff into the current project
|
|
34
|
-
extend <path> Install a local extension (reads <path>/scaff-extend.yml)
|
|
35
|
-
`);
|
|
36
|
-
}
|
|
37
|
-
function parseArgs(argv) {
|
|
38
|
-
let command = null;
|
|
39
|
-
const positional = [];
|
|
40
|
-
let docsDir = "docs";
|
|
41
|
-
let codebaseDir = ".";
|
|
42
|
-
let tools = null;
|
|
43
|
-
let subagent = true;
|
|
44
|
-
let root = process.cwd();
|
|
45
|
-
let force = false;
|
|
46
|
-
let yes = false;
|
|
47
|
-
let isDryRun = false;
|
|
48
|
-
let help = false;
|
|
49
|
-
for (let i = 0; i < argv.length; i++) {
|
|
50
|
-
const arg = argv[i];
|
|
51
|
-
if (arg === "-h" || arg === "--help") {
|
|
52
|
-
help = true;
|
|
53
|
-
}
|
|
54
|
-
else if (arg === "--docs") {
|
|
55
|
-
const next = argv[++i];
|
|
56
|
-
if (!next) {
|
|
57
|
-
console.error("Error: --docs requires a directory path");
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
docsDir = next;
|
|
61
|
-
}
|
|
62
|
-
else if (arg === "--codebase") {
|
|
63
|
-
const next = argv[++i];
|
|
64
|
-
if (!next) {
|
|
65
|
-
console.error("Error: --codebase requires a directory path");
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
codebaseDir = next;
|
|
69
|
-
}
|
|
70
|
-
else if (arg === "--tools") {
|
|
71
|
-
const next = argv[++i];
|
|
72
|
-
if (!next) {
|
|
73
|
-
console.error("Error: --tools requires a platform list");
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
if (next === "all") {
|
|
77
|
-
tools = [...PLATFORM_IDS];
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
tools = next.split(",").map((id) => id.trim());
|
|
81
|
-
for (const id of tools) {
|
|
82
|
-
if (!isValidPlatform(id)) {
|
|
83
|
-
console.error(`Error: unknown platform "${id}". Available: ${PLATFORM_IDS.join(", ")}`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else if (arg === "--no-subagent") {
|
|
90
|
-
subagent = false;
|
|
91
|
-
}
|
|
92
|
-
else if (arg === "--force") {
|
|
93
|
-
force = true;
|
|
94
|
-
}
|
|
95
|
-
else if (arg === "-y" || arg === "--yes") {
|
|
96
|
-
yes = true;
|
|
97
|
-
}
|
|
98
|
-
else if (arg === "--dry-run") {
|
|
99
|
-
isDryRun = true;
|
|
100
|
-
}
|
|
101
|
-
else if (arg === "--root") {
|
|
102
|
-
const next = argv[++i];
|
|
103
|
-
if (!next) {
|
|
104
|
-
console.error("Error: --root requires a directory path");
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
root = resolve(next);
|
|
108
|
-
}
|
|
109
|
-
else if (!arg.startsWith("-")) {
|
|
110
|
-
if (command === null) {
|
|
111
|
-
command = arg;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
positional.push(arg);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
console.error(`Unknown option: ${arg}`);
|
|
119
|
-
printUsage();
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return { command, positional, docsDir, codebaseDir, tools, subagent, root, force, yes, dryRun: isDryRun, help };
|
|
124
|
-
}
|
|
125
|
-
async function runExtend(args) {
|
|
126
|
-
const extPath = args.positional[0];
|
|
127
|
-
if (!extPath) {
|
|
128
|
-
console.error("Error: scaff extend requires a path argument");
|
|
129
|
-
printUsage();
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
const resolvedExt = resolve(extPath);
|
|
133
|
-
if (!existsSync(resolvedExt)) {
|
|
134
|
-
console.error(`Error: extension path does not exist: ${resolvedExt}`);
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
if (!existsSync(args.root)) {
|
|
138
|
-
console.error(`Error: target directory does not exist: ${args.root}`);
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
const result = await extend(resolvedExt, {
|
|
143
|
-
force: args.force,
|
|
144
|
-
root: args.root,
|
|
145
|
-
});
|
|
146
|
-
if (result.commandsAdded.length > 0) {
|
|
147
|
-
console.log(`${chalk.green("\u2713")} Commands added: ${result.commandsAdded.join(", ")}`);
|
|
148
|
-
}
|
|
149
|
-
if (result.commandsSkipped.length > 0) {
|
|
150
|
-
console.log(`${chalk.yellow("~")} Commands skipped: ${result.commandsSkipped.join(", ")}`);
|
|
151
|
-
}
|
|
152
|
-
if (result.hooksApplied.length > 0) {
|
|
153
|
-
console.log(`${chalk.green("\u2713")} Hooks applied: ${result.hooksApplied.join(", ")}`);
|
|
154
|
-
}
|
|
155
|
-
if (result.dependenciesChecked.length > 0) {
|
|
156
|
-
console.log(`${chalk.green("\u2713")} Dependencies: all found`);
|
|
157
|
-
}
|
|
158
|
-
process.exit(0);
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
console.error(`Error: ${err.message}`);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async function main() {
|
|
166
|
-
const args = parseArgs(process.argv.slice(2));
|
|
167
|
-
if (args.help || !args.command) {
|
|
168
|
-
printUsage();
|
|
169
|
-
process.exit(args.help ? 0 : 1);
|
|
170
|
-
}
|
|
171
|
-
if (args.command === "extend") {
|
|
172
|
-
await runExtend(args);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (args.command !== "init") {
|
|
176
|
-
console.error(`Unknown command: ${args.command}`);
|
|
177
|
-
printUsage();
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
if (!existsSync(args.root)) {
|
|
181
|
-
console.error(`Error: target directory does not exist: ${args.root}`);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
let tools;
|
|
185
|
-
if (args.tools) {
|
|
186
|
-
tools = args.tools;
|
|
187
|
-
}
|
|
188
|
-
else if (args.yes) {
|
|
189
|
-
const detected = detectInstalledPlatforms(args.root);
|
|
190
|
-
tools = detected.length > 0 ? detected : [DEFAULT_PLATFORM];
|
|
191
|
-
console.log(chalk.dim(`Auto-selected: ${tools.join(", ")}`));
|
|
192
|
-
}
|
|
193
|
-
else if (process.stdin.isTTY && !args.dryRun) {
|
|
194
|
-
await showWelcome();
|
|
195
|
-
tools = await selectPlatforms(args.root);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
tools = [DEFAULT_PLATFORM];
|
|
199
|
-
}
|
|
200
|
-
const config = {
|
|
201
|
-
tools,
|
|
202
|
-
subagent: args.subagent,
|
|
203
|
-
root: args.root,
|
|
204
|
-
docsDir: args.docsDir,
|
|
205
|
-
codebaseDir: args.codebaseDir,
|
|
206
|
-
force: args.force,
|
|
207
|
-
};
|
|
208
|
-
if (args.dryRun) {
|
|
209
|
-
const result = dryRun(config);
|
|
210
|
-
console.log(t("cli.dryRun") + "\n");
|
|
211
|
-
for (const file of result.files) {
|
|
212
|
-
console.log(` ${chalk.green("+")} ${file}`);
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const result = await install(config);
|
|
217
|
-
console.log(t("cli.complete") + "\n");
|
|
218
|
-
if (result.files.length > 0) {
|
|
219
|
-
console.log(t("cli.installed"));
|
|
220
|
-
for (const file of result.files) {
|
|
221
|
-
console.log(` ${chalk.green("+")} ${file}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (result.skipped.length > 0) {
|
|
225
|
-
console.log("\n" + t("cli.skipped"));
|
|
226
|
-
for (const file of result.skipped) {
|
|
227
|
-
console.log(` ${chalk.yellow("~")} ${file}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
console.log("\n" + brand(t("cli.nextSteps")));
|
|
231
|
-
console.log(" " + chalk.dim("1.") + " " + t("cli.step1"));
|
|
232
|
-
console.log(" " + chalk.dim("2.") + " " + t("cli.step2").replace("{cmd}", brand("/scaff:scout")));
|
|
233
|
-
process.exit(0);
|
|
234
|
-
}
|
|
235
|
-
main();
|
package/dist/extend.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/hook-parser.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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;
|
package/dist/hook-parser.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/i18n/index.d.ts
DELETED
package/dist/i18n/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|