ystack 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/LINTING.md +198 -0
- package/PHILOSOPHY.md +132 -0
- package/PLAN.md +515 -0
- package/README.md +103 -0
- package/RUNTIMES.md +199 -0
- package/bin/cli.js +973 -0
- package/hooks/context-monitor.js +30 -0
- package/hooks/session-start.sh +35 -0
- package/hooks/workflow-nudge.js +107 -0
- package/package.json +39 -0
- package/skills/address-review/SKILL.md +244 -0
- package/skills/build/SKILL.md +246 -0
- package/skills/build/resources/plan-checker.md +121 -0
- package/skills/docs/SKILL.md +160 -0
- package/skills/go/SKILL.md +216 -0
- package/skills/go/resources/executor.md +57 -0
- package/skills/import/SKILL.md +306 -0
- package/skills/pr/SKILL.md +152 -0
- package/skills/review/SKILL.md +184 -0
- package/skills/scaffold/SKILL.md +549 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, resolve, dirname } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const YSTACK_ROOT = resolve(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
// Colors
|
|
15
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
16
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
17
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
18
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
19
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
20
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const command = args[0];
|
|
24
|
+
const flags = args.filter((a) => a.startsWith("--"));
|
|
25
|
+
|
|
26
|
+
// --- Prompt helpers ---
|
|
27
|
+
|
|
28
|
+
function handleCancel(value) {
|
|
29
|
+
if (p.isCancel(value)) {
|
|
30
|
+
p.cancel("Cancelled.");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- File helpers ---
|
|
37
|
+
|
|
38
|
+
function hashFile(path) {
|
|
39
|
+
if (!existsSync(path)) return null;
|
|
40
|
+
const content = readFileSync(path, "utf-8");
|
|
41
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function commandExists(cmd) {
|
|
45
|
+
try {
|
|
46
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function detectProjectName() {
|
|
54
|
+
// Try package.json
|
|
55
|
+
if (existsSync("package.json")) {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
|
|
58
|
+
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
59
|
+
} catch { /* ignore */ }
|
|
60
|
+
}
|
|
61
|
+
// Fall back to directory name
|
|
62
|
+
return process.cwd().split("/").pop();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function detectDocsFramework() {
|
|
66
|
+
if (existsSync("docs/src/content/_meta.ts")) return { framework: "nextra", root: "docs/src/content" };
|
|
67
|
+
if (existsSync("content/docs/meta.json")) return { framework: "fumadocs", root: "content/docs" };
|
|
68
|
+
if (existsSync("docs/_meta.ts")) return { framework: "nextra", root: "docs" };
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectMonorepo() {
|
|
73
|
+
if (existsSync("turbo.json")) return "turborepo";
|
|
74
|
+
if (existsSync("pnpm-workspace.yaml")) return "pnpm";
|
|
75
|
+
if (existsSync("lerna.json")) return "lerna";
|
|
76
|
+
if (existsSync("nx.json")) return "nx";
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Skill installation ---
|
|
81
|
+
|
|
82
|
+
function copySkills(projectRoot, ystackRoot, silent = false) {
|
|
83
|
+
const skillsDir = join(ystackRoot, "skills");
|
|
84
|
+
const targetDir = join(projectRoot, ".claude", "skills");
|
|
85
|
+
const skills = readdirSync(skillsDir).filter((d) =>
|
|
86
|
+
statSync(join(skillsDir, d)).isDirectory(),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
let installed = 0;
|
|
90
|
+
let skipped = 0;
|
|
91
|
+
|
|
92
|
+
for (const skill of skills) {
|
|
93
|
+
const src = join(skillsDir, skill);
|
|
94
|
+
const dst = join(targetDir, skill);
|
|
95
|
+
const srcSkill = join(src, "SKILL.md");
|
|
96
|
+
const dstSkill = join(dst, "SKILL.md");
|
|
97
|
+
|
|
98
|
+
if (!existsSync(srcSkill)) continue;
|
|
99
|
+
|
|
100
|
+
if (existsSync(dstSkill)) {
|
|
101
|
+
const srcHash = hashFile(srcSkill);
|
|
102
|
+
const dstHash = hashFile(dstSkill);
|
|
103
|
+
if (srcHash !== dstHash) {
|
|
104
|
+
if (!silent) console.log(yellow(` ⚠ ${skill}/ — customized, skipping`));
|
|
105
|
+
skipped++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
mkdirSync(dst, { recursive: true });
|
|
111
|
+
cpSync(src, dst, { recursive: true });
|
|
112
|
+
installed++;
|
|
113
|
+
if (!silent) console.log(green(` ✓ ${skill}/`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { installed, skipped, total: skills.length };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function installHooks(projectRoot, ystackRoot) {
|
|
120
|
+
const settingsPath = join(projectRoot, ".claude", "settings.json");
|
|
121
|
+
let settings = {};
|
|
122
|
+
|
|
123
|
+
if (existsSync(settingsPath)) {
|
|
124
|
+
try {
|
|
125
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
126
|
+
} catch { /* create new */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!settings.hooks) settings.hooks = {};
|
|
130
|
+
|
|
131
|
+
// PostToolUse — context monitor
|
|
132
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
133
|
+
const hasContextMonitor = settings.hooks.PostToolUse.some(
|
|
134
|
+
(h) => h.hooks?.some((hh) => hh.command?.includes("context-monitor")),
|
|
135
|
+
);
|
|
136
|
+
if (!hasContextMonitor) {
|
|
137
|
+
settings.hooks.PostToolUse.push({
|
|
138
|
+
matcher: "*",
|
|
139
|
+
hooks: [{
|
|
140
|
+
type: "command",
|
|
141
|
+
command: `node "${join(projectRoot, ".claude", "hooks", "context-monitor.js")}"`,
|
|
142
|
+
timeout: 5,
|
|
143
|
+
}],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// PreToolUse — workflow nudge
|
|
148
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
149
|
+
const hasWorkflowNudge = settings.hooks.PreToolUse.some(
|
|
150
|
+
(h) => h.hooks?.some((hh) => hh.command?.includes("workflow-nudge")),
|
|
151
|
+
);
|
|
152
|
+
if (!hasWorkflowNudge) {
|
|
153
|
+
settings.hooks.PreToolUse.push({
|
|
154
|
+
matcher: "Edit|Write",
|
|
155
|
+
hooks: [{
|
|
156
|
+
type: "command",
|
|
157
|
+
command: `node "${join(projectRoot, ".claude", "hooks", "workflow-nudge.js")}"`,
|
|
158
|
+
timeout: 5,
|
|
159
|
+
}],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
164
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
165
|
+
|
|
166
|
+
// Copy hook files
|
|
167
|
+
const hooksDir = join(ystackRoot, "hooks");
|
|
168
|
+
const targetHooksDir = join(projectRoot, ".claude", "hooks");
|
|
169
|
+
mkdirSync(targetHooksDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
if (existsSync(hooksDir)) {
|
|
172
|
+
for (const file of readdirSync(hooksDir)) {
|
|
173
|
+
cpSync(join(hooksDir, file), join(targetHooksDir, file));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function removeSkills(projectRoot) {
|
|
179
|
+
const ystackSkills = join(YSTACK_ROOT, "skills");
|
|
180
|
+
if (!existsSync(ystackSkills)) return;
|
|
181
|
+
|
|
182
|
+
const skillsDir = join(projectRoot, ".claude", "skills");
|
|
183
|
+
for (const skill of readdirSync(ystackSkills)) {
|
|
184
|
+
const target = join(skillsDir, skill);
|
|
185
|
+
if (existsSync(target) && statSync(join(ystackSkills, skill)).isDirectory()) {
|
|
186
|
+
rmSync(target, { recursive: true });
|
|
187
|
+
console.log(dim(` removed ${skill}/`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function removeHooks(projectRoot) {
|
|
193
|
+
const settingsPath = join(projectRoot, ".claude", "settings.json");
|
|
194
|
+
if (!existsSync(settingsPath)) return;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
198
|
+
if (settings.hooks?.PostToolUse) {
|
|
199
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
200
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes("context-monitor")),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (settings.hooks?.PreToolUse) {
|
|
204
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
205
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes("workflow-nudge")),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
209
|
+
} catch { /* ignore */ }
|
|
210
|
+
|
|
211
|
+
const hooksDir = join(projectRoot, ".claude", "hooks");
|
|
212
|
+
for (const file of ["context-monitor.js", "session-start.sh", "workflow-nudge.js"]) {
|
|
213
|
+
const target = join(hooksDir, file);
|
|
214
|
+
if (existsSync(target)) rmSync(target);
|
|
215
|
+
}
|
|
216
|
+
console.log(dim(" removed hooks"));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ensureGitignore(projectRoot) {
|
|
220
|
+
const gitignorePath = join(projectRoot, ".gitignore");
|
|
221
|
+
if (!existsSync(gitignorePath)) return;
|
|
222
|
+
|
|
223
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
224
|
+
if (!content.includes(".context/")) {
|
|
225
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n.context/\n");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Commands ---
|
|
230
|
+
|
|
231
|
+
async function cmdInit() {
|
|
232
|
+
const projectRoot = process.cwd();
|
|
233
|
+
const skillsOnly = flags.includes("--skills-only");
|
|
234
|
+
|
|
235
|
+
p.intro("ystack init — agent harness for doc-driven development");
|
|
236
|
+
|
|
237
|
+
// --- Step 1: Project name ---
|
|
238
|
+
const detectedName = detectProjectName();
|
|
239
|
+
const projectName = handleCancel(await p.text({
|
|
240
|
+
message: "Project name:",
|
|
241
|
+
placeholder: detectedName,
|
|
242
|
+
defaultValue: detectedName,
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
// --- Step 2: Docs framework ---
|
|
246
|
+
const detectedDocs = detectDocsFramework();
|
|
247
|
+
let docsFramework;
|
|
248
|
+
let docsRoot;
|
|
249
|
+
|
|
250
|
+
if (detectedDocs) {
|
|
251
|
+
const keepDetected = handleCancel(await p.confirm({
|
|
252
|
+
message: `Detected ${detectedDocs.framework} at ${detectedDocs.root}. Use it?`,
|
|
253
|
+
}));
|
|
254
|
+
if (keepDetected) {
|
|
255
|
+
docsFramework = detectedDocs.framework;
|
|
256
|
+
docsRoot = detectedDocs.root;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!docsFramework) {
|
|
261
|
+
docsFramework = handleCancel(await p.select({
|
|
262
|
+
message: "Docs framework:",
|
|
263
|
+
options: [
|
|
264
|
+
{ label: "Nextra", value: "nextra" },
|
|
265
|
+
{ label: "Fumadocs", value: "fumadocs" },
|
|
266
|
+
{ label: "None — I'll set up docs later", value: "none" },
|
|
267
|
+
],
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
if (docsFramework === "nextra") {
|
|
271
|
+
docsRoot = handleCancel(await p.text({
|
|
272
|
+
message: "Docs root:",
|
|
273
|
+
placeholder: "docs/src/content",
|
|
274
|
+
defaultValue: "docs/src/content",
|
|
275
|
+
}));
|
|
276
|
+
} else if (docsFramework === "fumadocs") {
|
|
277
|
+
docsRoot = handleCancel(await p.text({
|
|
278
|
+
message: "Docs root:",
|
|
279
|
+
placeholder: "content/docs",
|
|
280
|
+
defaultValue: "content/docs",
|
|
281
|
+
}));
|
|
282
|
+
} else {
|
|
283
|
+
docsRoot = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Step 3: Beads ---
|
|
288
|
+
let initBeads = false;
|
|
289
|
+
if (!skillsOnly) {
|
|
290
|
+
const hasBeads = existsSync(join(projectRoot, ".beads"));
|
|
291
|
+
const hasBdCli = commandExists("bd");
|
|
292
|
+
|
|
293
|
+
if (hasBeads) {
|
|
294
|
+
p.log.info("Beads already initialized");
|
|
295
|
+
} else if (hasBdCli) {
|
|
296
|
+
initBeads = handleCancel(await p.confirm({
|
|
297
|
+
message: "Initialize Beads for persistent memory?",
|
|
298
|
+
}));
|
|
299
|
+
} else {
|
|
300
|
+
const installBeads = handleCancel(await p.select({
|
|
301
|
+
message: "Beads (persistent memory for agents):",
|
|
302
|
+
options: [
|
|
303
|
+
{ label: "Install Beads (brew install beads)", value: "install" },
|
|
304
|
+
{ label: "Skip — I'll set up Beads later", value: "skip" },
|
|
305
|
+
],
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
if (installBeads === "install") {
|
|
309
|
+
p.log.step("Installing Beads...");
|
|
310
|
+
try {
|
|
311
|
+
execSync("brew install beads", { stdio: "inherit" });
|
|
312
|
+
p.log.success("Beads installed");
|
|
313
|
+
initBeads = true;
|
|
314
|
+
} catch {
|
|
315
|
+
p.log.warn("Install failed. Try manually: brew install beads");
|
|
316
|
+
p.log.warn("or: npm install -g @beads/bd");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Step 4: Runtime ---
|
|
323
|
+
const runtime = handleCancel(await p.select({
|
|
324
|
+
message: "Runtime:",
|
|
325
|
+
options: [
|
|
326
|
+
{ label: "Claude Code", value: "claude-code" },
|
|
327
|
+
{ label: "Claude Code (skills only, no hooks)", value: "claude-code-minimal" },
|
|
328
|
+
],
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
// --- Step 5: Hooks ---
|
|
332
|
+
let installHooksFlag = true;
|
|
333
|
+
if (!skillsOnly && runtime === "claude-code") {
|
|
334
|
+
installHooksFlag = handleCancel(await p.confirm({
|
|
335
|
+
message: "Install agent linting hooks?",
|
|
336
|
+
}));
|
|
337
|
+
} else if (runtime === "claude-code-minimal") {
|
|
338
|
+
installHooksFlag = false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- Execute ---
|
|
342
|
+
const s = p.spinner();
|
|
343
|
+
s.start("Setting up...");
|
|
344
|
+
|
|
345
|
+
// Skills
|
|
346
|
+
mkdirSync(join(projectRoot, ".claude", "skills"), { recursive: true });
|
|
347
|
+
const result = copySkills(projectRoot, YSTACK_ROOT, true);
|
|
348
|
+
|
|
349
|
+
// Hooks
|
|
350
|
+
if (installHooksFlag) {
|
|
351
|
+
installHooks(projectRoot, YSTACK_ROOT);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Config
|
|
355
|
+
const configPath = join(projectRoot, "ystack.config.json");
|
|
356
|
+
const configExisted = existsSync(configPath);
|
|
357
|
+
if (!configExisted) {
|
|
358
|
+
const monorepo = detectMonorepo();
|
|
359
|
+
const config = {
|
|
360
|
+
project: projectName,
|
|
361
|
+
docs: {
|
|
362
|
+
root: docsRoot,
|
|
363
|
+
framework: docsFramework === "none" ? null : docsFramework,
|
|
364
|
+
},
|
|
365
|
+
monorepo: {
|
|
366
|
+
enabled: !!monorepo,
|
|
367
|
+
...(monorepo ? { tool: monorepo } : {}),
|
|
368
|
+
},
|
|
369
|
+
modules: {},
|
|
370
|
+
workflow: {
|
|
371
|
+
plan_checker: true,
|
|
372
|
+
fresh_context_per_task: true,
|
|
373
|
+
auto_docs_check: true,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
ensureGitignore(projectRoot);
|
|
380
|
+
|
|
381
|
+
// Beads
|
|
382
|
+
if (initBeads) {
|
|
383
|
+
try {
|
|
384
|
+
execSync("bd init", { stdio: "pipe", cwd: projectRoot });
|
|
385
|
+
} catch { /* handled below */ }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
s.stop("Setup complete");
|
|
389
|
+
|
|
390
|
+
// Summary
|
|
391
|
+
p.log.success(`Skills: ${result.installed} installed, ${result.skipped} skipped`);
|
|
392
|
+
if (installHooksFlag) {
|
|
393
|
+
p.log.success("Hooks: context-monitor, workflow-nudge");
|
|
394
|
+
}
|
|
395
|
+
if (configExisted) {
|
|
396
|
+
p.log.info("Config: ystack.config.json preserved");
|
|
397
|
+
} else {
|
|
398
|
+
p.log.success("Config: created ystack.config.json");
|
|
399
|
+
}
|
|
400
|
+
if (initBeads) {
|
|
401
|
+
p.log.success("Beads initialized");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
p.note(
|
|
405
|
+
[
|
|
406
|
+
`${cyan("/import")} — scan codebase and populate module registry`,
|
|
407
|
+
`${cyan("/build")} — plan a feature`,
|
|
408
|
+
`${cyan("/scaffold")} — scaffold docs from a plan`,
|
|
409
|
+
].join("\n"),
|
|
410
|
+
"Next steps",
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
p.outro("Done!");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function cmdUpdate() {
|
|
417
|
+
const projectRoot = process.cwd();
|
|
418
|
+
|
|
419
|
+
p.intro("ystack update");
|
|
420
|
+
|
|
421
|
+
const s = p.spinner();
|
|
422
|
+
s.start("Updating skills...");
|
|
423
|
+
const result = copySkills(projectRoot, YSTACK_ROOT, true);
|
|
424
|
+
s.stop(`Skills: ${result.installed} updated, ${result.skipped} customized (preserved)`);
|
|
425
|
+
|
|
426
|
+
const hooksDir = join(YSTACK_ROOT, "hooks");
|
|
427
|
+
const targetHooksDir = join(projectRoot, ".claude", "hooks");
|
|
428
|
+
if (existsSync(hooksDir) && existsSync(targetHooksDir)) {
|
|
429
|
+
for (const file of readdirSync(hooksDir)) {
|
|
430
|
+
cpSync(join(hooksDir, file), join(targetHooksDir, file));
|
|
431
|
+
}
|
|
432
|
+
p.log.success("Hook files updated");
|
|
433
|
+
} else {
|
|
434
|
+
p.log.info("No hooks to update");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
p.outro("Done!");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function cmdRemove() {
|
|
441
|
+
const projectRoot = process.cwd();
|
|
442
|
+
|
|
443
|
+
p.intro("ystack remove");
|
|
444
|
+
|
|
445
|
+
const proceed = handleCancel(await p.confirm({
|
|
446
|
+
message: "Remove ystack skills and hooks? (keeps config, beads, docs)",
|
|
447
|
+
}));
|
|
448
|
+
if (!proceed) {
|
|
449
|
+
p.outro("Cancelled.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const s = p.spinner();
|
|
454
|
+
s.start("Removing skills and hooks...");
|
|
455
|
+
removeSkills(projectRoot);
|
|
456
|
+
removeHooks(projectRoot);
|
|
457
|
+
s.stop("Removed skills and hooks");
|
|
458
|
+
|
|
459
|
+
p.log.info("Kept: ystack.config.json, .beads/, docs/");
|
|
460
|
+
p.outro("Done!");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function cmdCreate() {
|
|
464
|
+
const name = args[1];
|
|
465
|
+
if (!name) {
|
|
466
|
+
console.log(red("\nUsage: ystack create <project-name>\n"));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Parse flags
|
|
471
|
+
const docsFlag = flags.find((f) => f.startsWith("--docs"));
|
|
472
|
+
let docsFramework = "nextra";
|
|
473
|
+
if (docsFlag) {
|
|
474
|
+
const idx = args.indexOf(docsFlag);
|
|
475
|
+
const val = docsFlag.includes("=") ? docsFlag.split("=")[1] : args[idx + 1];
|
|
476
|
+
if (val === "fumadocs" || val === "nextra") {
|
|
477
|
+
docsFramework = val;
|
|
478
|
+
} else {
|
|
479
|
+
console.log(red(`\nUnknown docs framework: ${val}. Use "nextra" or "fumadocs".\n`));
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const fromFlag = flags.find((f) => f.startsWith("--from"));
|
|
485
|
+
if (fromFlag) {
|
|
486
|
+
console.log(yellow("\nScaffold integration from plan files is coming soon."));
|
|
487
|
+
console.log(yellow("Creating base project without plan integration.\n"));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const projectDir = resolve(process.cwd(), name);
|
|
491
|
+
|
|
492
|
+
if (existsSync(projectDir)) {
|
|
493
|
+
console.log(red(`\nDirectory "${name}" already exists.\n`));
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
p.intro(`ystack create — scaffolding ${name}`);
|
|
498
|
+
|
|
499
|
+
// --- Create directory structure ---
|
|
500
|
+
console.log(bold("Creating directories..."));
|
|
501
|
+
const dirs = [
|
|
502
|
+
"apps",
|
|
503
|
+
"packages",
|
|
504
|
+
"docs/src/content",
|
|
505
|
+
".claude/skills",
|
|
506
|
+
".context",
|
|
507
|
+
];
|
|
508
|
+
for (const dir of dirs) {
|
|
509
|
+
mkdirSync(join(projectDir, dir), { recursive: true });
|
|
510
|
+
}
|
|
511
|
+
console.log(green(" ✓ apps/, packages/, docs/, .claude/, .context/\n"));
|
|
512
|
+
|
|
513
|
+
// --- Generate files ---
|
|
514
|
+
console.log(bold("Generating config files..."));
|
|
515
|
+
|
|
516
|
+
// Root package.json
|
|
517
|
+
const rootPkg = {
|
|
518
|
+
name,
|
|
519
|
+
version: "0.0.1",
|
|
520
|
+
private: true,
|
|
521
|
+
type: "module",
|
|
522
|
+
scripts: {
|
|
523
|
+
dev: "turbo dev",
|
|
524
|
+
build: "turbo build",
|
|
525
|
+
typecheck: "turbo typecheck",
|
|
526
|
+
check: "ultracite check",
|
|
527
|
+
fix: "ultracite fix",
|
|
528
|
+
clean: "turbo clean",
|
|
529
|
+
},
|
|
530
|
+
devDependencies: {
|
|
531
|
+
turbo: "latest",
|
|
532
|
+
typescript: "^5.8.0",
|
|
533
|
+
ultracite: "latest",
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
writeFileSync(join(projectDir, "package.json"), JSON.stringify(rootPkg, null, 2) + "\n");
|
|
537
|
+
console.log(green(" ✓ package.json"));
|
|
538
|
+
|
|
539
|
+
// turbo.json
|
|
540
|
+
const turboConfig = {
|
|
541
|
+
$schema: "https://turbo.build/schema.json",
|
|
542
|
+
tasks: {
|
|
543
|
+
dev: { persistent: true, cache: false },
|
|
544
|
+
build: { dependsOn: ["^build"] },
|
|
545
|
+
typecheck: { dependsOn: ["^build"] },
|
|
546
|
+
clean: { cache: false },
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
writeFileSync(join(projectDir, "turbo.json"), JSON.stringify(turboConfig, null, 2) + "\n");
|
|
550
|
+
console.log(green(" ✓ turbo.json"));
|
|
551
|
+
|
|
552
|
+
// pnpm-workspace.yaml
|
|
553
|
+
const pnpmWorkspace = `packages:
|
|
554
|
+
- "apps/*"
|
|
555
|
+
- "packages/*"
|
|
556
|
+
- "docs"
|
|
557
|
+
`;
|
|
558
|
+
writeFileSync(join(projectDir, "pnpm-workspace.yaml"), pnpmWorkspace);
|
|
559
|
+
console.log(green(" ✓ pnpm-workspace.yaml"));
|
|
560
|
+
|
|
561
|
+
// tsconfig.json
|
|
562
|
+
const tsconfig = {
|
|
563
|
+
compilerOptions: {
|
|
564
|
+
target: "ES2022",
|
|
565
|
+
module: "ES2022",
|
|
566
|
+
moduleResolution: "bundler",
|
|
567
|
+
lib: ["ES2022"],
|
|
568
|
+
strict: true,
|
|
569
|
+
esModuleInterop: true,
|
|
570
|
+
skipLibCheck: true,
|
|
571
|
+
forceConsistentCasingInFileNames: true,
|
|
572
|
+
resolveJsonModule: true,
|
|
573
|
+
isolatedModules: true,
|
|
574
|
+
declaration: true,
|
|
575
|
+
declarationMap: true,
|
|
576
|
+
sourceMap: true,
|
|
577
|
+
outDir: "dist",
|
|
578
|
+
},
|
|
579
|
+
exclude: ["node_modules", "dist", ".turbo"],
|
|
580
|
+
};
|
|
581
|
+
writeFileSync(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
582
|
+
console.log(green(" ✓ tsconfig.json"));
|
|
583
|
+
|
|
584
|
+
// biome.json
|
|
585
|
+
const biomeConfig = {
|
|
586
|
+
$schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
587
|
+
extends: ["ultracite"],
|
|
588
|
+
};
|
|
589
|
+
writeFileSync(join(projectDir, "biome.json"), JSON.stringify(biomeConfig, null, 2) + "\n");
|
|
590
|
+
console.log(green(" ✓ biome.json"));
|
|
591
|
+
|
|
592
|
+
// ystack.config.json
|
|
593
|
+
const docsRoot = docsFramework === "fumadocs" ? "content/docs" : "docs/src/content";
|
|
594
|
+
const ystackConfig = {
|
|
595
|
+
project: name,
|
|
596
|
+
docs: {
|
|
597
|
+
root: docsRoot,
|
|
598
|
+
framework: docsFramework,
|
|
599
|
+
},
|
|
600
|
+
monorepo: {
|
|
601
|
+
enabled: true,
|
|
602
|
+
tool: "turborepo",
|
|
603
|
+
},
|
|
604
|
+
modules: {},
|
|
605
|
+
workflow: {
|
|
606
|
+
plan_checker: true,
|
|
607
|
+
fresh_context_per_task: true,
|
|
608
|
+
auto_docs_check: true,
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
writeFileSync(join(projectDir, "ystack.config.json"), JSON.stringify(ystackConfig, null, 2) + "\n");
|
|
612
|
+
console.log(green(" ✓ ystack.config.json"));
|
|
613
|
+
|
|
614
|
+
// CLAUDE.md
|
|
615
|
+
const claudeMd = `# ${name}
|
|
616
|
+
|
|
617
|
+
This project uses [ystack](https://github.com/yulonghe97/ystack) for doc-driven development.
|
|
618
|
+
|
|
619
|
+
## Structure
|
|
620
|
+
|
|
621
|
+
- \`apps/\` — Application packages
|
|
622
|
+
- \`packages/\` — Shared library packages
|
|
623
|
+
- \`docs/\` — Documentation site (${docsFramework})
|
|
624
|
+
|
|
625
|
+
## Module Registry
|
|
626
|
+
|
|
627
|
+
Modules are defined in \`ystack.config.json\`. Each module maps code directories to documentation pages.
|
|
628
|
+
|
|
629
|
+
## Available Commands
|
|
630
|
+
|
|
631
|
+
| Command | Description |
|
|
632
|
+
|---------|-------------|
|
|
633
|
+
| \`/import\` | Scan codebase and populate module registry |
|
|
634
|
+
| \`/build <feature>\` | Plan a feature (reads docs + code, surfaces assumptions) |
|
|
635
|
+
| \`/go\` | Execute the plan with fresh subagents |
|
|
636
|
+
| \`/review\` | Code review + goal-backward verification |
|
|
637
|
+
| \`/docs\` | Update documentation for completed work |
|
|
638
|
+
| \`/pr\` | Verify, docs check, create PR |
|
|
639
|
+
|
|
640
|
+
## Scripts
|
|
641
|
+
|
|
642
|
+
- \`pnpm dev\` — Start dev servers
|
|
643
|
+
- \`pnpm build\` — Build all packages
|
|
644
|
+
- \`pnpm typecheck\` — Type-check all packages
|
|
645
|
+
- \`pnpm check\` — Lint (ultracite/biome)
|
|
646
|
+
- \`pnpm fix\` — Auto-fix lint issues
|
|
647
|
+
- \`pnpm clean\` — Clean build artifacts
|
|
648
|
+
`;
|
|
649
|
+
writeFileSync(join(projectDir, "CLAUDE.md"), claudeMd);
|
|
650
|
+
console.log(green(" ✓ CLAUDE.md"));
|
|
651
|
+
|
|
652
|
+
// AGENTS.md — runtime-agnostic context for non-Claude agents
|
|
653
|
+
const agentsMd = `# ${name}
|
|
654
|
+
|
|
655
|
+
This project uses [ystack](https://github.com/yulonghe97/ystack) for doc-driven development.
|
|
656
|
+
|
|
657
|
+
## Structure
|
|
658
|
+
|
|
659
|
+
- \`apps/\` — Application packages
|
|
660
|
+
- \`packages/\` — Shared library packages
|
|
661
|
+
- \`docs/\` — Documentation site (${docsFramework})
|
|
662
|
+
|
|
663
|
+
## Module Registry
|
|
664
|
+
|
|
665
|
+
Modules are defined in \`ystack.config.json\`. Each module maps code directories to documentation pages.
|
|
666
|
+
|
|
667
|
+
## Workflow
|
|
668
|
+
|
|
669
|
+
1. Read the relevant doc page before making changes
|
|
670
|
+
2. Plan before executing — break work into small, verifiable tasks
|
|
671
|
+
3. Verify against success criteria after implementation
|
|
672
|
+
4. Update docs when done — only document completed, verified work
|
|
673
|
+
|
|
674
|
+
## Scripts
|
|
675
|
+
|
|
676
|
+
- \`pnpm dev\` — Start dev servers
|
|
677
|
+
- \`pnpm build\` — Build all packages
|
|
678
|
+
- \`pnpm typecheck\` — Type-check all packages
|
|
679
|
+
- \`pnpm check\` — Lint (ultracite/biome)
|
|
680
|
+
- \`pnpm fix\` — Auto-fix lint issues
|
|
681
|
+
- \`pnpm clean\` — Clean build artifacts
|
|
682
|
+
`;
|
|
683
|
+
writeFileSync(join(projectDir, "AGENTS.md"), agentsMd);
|
|
684
|
+
console.log(green(" ✓ AGENTS.md"));
|
|
685
|
+
|
|
686
|
+
// .gitignore
|
|
687
|
+
const gitignore = `node_modules/
|
|
688
|
+
dist/
|
|
689
|
+
.turbo/
|
|
690
|
+
.next/
|
|
691
|
+
.context/
|
|
692
|
+
.env
|
|
693
|
+
.env.local
|
|
694
|
+
*.tsbuildinfo
|
|
695
|
+
`;
|
|
696
|
+
writeFileSync(join(projectDir, ".gitignore"), gitignore);
|
|
697
|
+
console.log(green(" ✓ .gitignore"));
|
|
698
|
+
|
|
699
|
+
// .env.example
|
|
700
|
+
writeFileSync(join(projectDir, ".env.example"), "");
|
|
701
|
+
console.log(green(" ✓ .env.example"));
|
|
702
|
+
console.log();
|
|
703
|
+
|
|
704
|
+
// --- Docs app ---
|
|
705
|
+
console.log(bold("Setting up docs app..."));
|
|
706
|
+
|
|
707
|
+
if (docsFramework === "nextra") {
|
|
708
|
+
// docs/package.json
|
|
709
|
+
const docsPkg = {
|
|
710
|
+
name: `${name}-docs`,
|
|
711
|
+
version: "0.0.1",
|
|
712
|
+
private: true,
|
|
713
|
+
type: "module",
|
|
714
|
+
scripts: {
|
|
715
|
+
dev: "next dev",
|
|
716
|
+
build: "next build",
|
|
717
|
+
},
|
|
718
|
+
dependencies: {
|
|
719
|
+
next: "^15.0.0",
|
|
720
|
+
nextra: "^4.0.0",
|
|
721
|
+
"nextra-theme-docs": "^4.0.0",
|
|
722
|
+
react: "^19.0.0",
|
|
723
|
+
"react-dom": "^19.0.0",
|
|
724
|
+
},
|
|
725
|
+
devDependencies: {
|
|
726
|
+
typescript: "^5.8.0",
|
|
727
|
+
"@types/react": "^19.0.0",
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
writeFileSync(join(projectDir, "docs/package.json"), JSON.stringify(docsPkg, null, 2) + "\n");
|
|
731
|
+
console.log(green(" ✓ docs/package.json"));
|
|
732
|
+
|
|
733
|
+
// docs/next.config.ts
|
|
734
|
+
const nextConfig = `import nextra from "nextra";
|
|
735
|
+
|
|
736
|
+
const withNextra = nextra({});
|
|
737
|
+
|
|
738
|
+
export default withNextra({
|
|
739
|
+
output: "export",
|
|
740
|
+
images: { unoptimized: true },
|
|
741
|
+
});
|
|
742
|
+
`;
|
|
743
|
+
writeFileSync(join(projectDir, "docs/next.config.ts"), nextConfig);
|
|
744
|
+
console.log(green(" ✓ docs/next.config.ts"));
|
|
745
|
+
|
|
746
|
+
// docs/tsconfig.json
|
|
747
|
+
const docsTsconfig = {
|
|
748
|
+
extends: "../tsconfig.json",
|
|
749
|
+
compilerOptions: {
|
|
750
|
+
jsx: "preserve",
|
|
751
|
+
outDir: "dist",
|
|
752
|
+
noEmit: true,
|
|
753
|
+
},
|
|
754
|
+
include: ["src", "next.config.ts", "next-env.d.ts"],
|
|
755
|
+
exclude: ["node_modules", ".next"],
|
|
756
|
+
};
|
|
757
|
+
writeFileSync(join(projectDir, "docs/tsconfig.json"), JSON.stringify(docsTsconfig, null, 2) + "\n");
|
|
758
|
+
console.log(green(" ✓ docs/tsconfig.json"));
|
|
759
|
+
|
|
760
|
+
// docs/src/content/_meta.ts
|
|
761
|
+
const metaTs = `export default {
|
|
762
|
+
index: "Overview",
|
|
763
|
+
};
|
|
764
|
+
`;
|
|
765
|
+
writeFileSync(join(projectDir, "docs/src/content/_meta.ts"), metaTs);
|
|
766
|
+
console.log(green(" ✓ docs/src/content/_meta.ts"));
|
|
767
|
+
|
|
768
|
+
// docs/src/content/index.mdx
|
|
769
|
+
const indexMdx = `---
|
|
770
|
+
title: ${name}
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
# ${name}
|
|
774
|
+
|
|
775
|
+
Welcome to the ${name} documentation.
|
|
776
|
+
|
|
777
|
+
## Getting Started
|
|
778
|
+
|
|
779
|
+
This project is set up as a monorepo using Turborepo and pnpm workspaces.
|
|
780
|
+
|
|
781
|
+
\`\`\`bash
|
|
782
|
+
pnpm install
|
|
783
|
+
pnpm dev
|
|
784
|
+
\`\`\`
|
|
785
|
+
|
|
786
|
+
## Project Structure
|
|
787
|
+
|
|
788
|
+
- \`apps/\` — Application packages
|
|
789
|
+
- \`packages/\` — Shared library packages
|
|
790
|
+
- \`docs/\` — This documentation site
|
|
791
|
+
`;
|
|
792
|
+
writeFileSync(join(projectDir, "docs/src/content/index.mdx"), indexMdx);
|
|
793
|
+
console.log(green(" ✓ docs/src/content/index.mdx"));
|
|
794
|
+
} else {
|
|
795
|
+
// Fumadocs setup
|
|
796
|
+
// docs/package.json
|
|
797
|
+
const docsPkg = {
|
|
798
|
+
name: `${name}-docs`,
|
|
799
|
+
version: "0.0.1",
|
|
800
|
+
private: true,
|
|
801
|
+
type: "module",
|
|
802
|
+
scripts: {
|
|
803
|
+
dev: "next dev",
|
|
804
|
+
build: "next build",
|
|
805
|
+
},
|
|
806
|
+
dependencies: {
|
|
807
|
+
next: "^15.0.0",
|
|
808
|
+
"fumadocs-core": "latest",
|
|
809
|
+
"fumadocs-ui": "latest",
|
|
810
|
+
"fumadocs-mdx": "latest",
|
|
811
|
+
react: "^19.0.0",
|
|
812
|
+
"react-dom": "^19.0.0",
|
|
813
|
+
},
|
|
814
|
+
devDependencies: {
|
|
815
|
+
typescript: "^5.8.0",
|
|
816
|
+
"@types/react": "^19.0.0",
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
writeFileSync(join(projectDir, "docs/package.json"), JSON.stringify(docsPkg, null, 2) + "\n");
|
|
820
|
+
console.log(green(" ✓ docs/package.json"));
|
|
821
|
+
|
|
822
|
+
// docs/next.config.ts
|
|
823
|
+
const nextConfig = `import { createMDX } from "fumadocs-mdx/next";
|
|
824
|
+
|
|
825
|
+
const withMDX = createMDX();
|
|
826
|
+
|
|
827
|
+
export default withMDX({});
|
|
828
|
+
`;
|
|
829
|
+
writeFileSync(join(projectDir, "docs/next.config.ts"), nextConfig);
|
|
830
|
+
console.log(green(" ✓ docs/next.config.ts"));
|
|
831
|
+
|
|
832
|
+
// docs/tsconfig.json
|
|
833
|
+
const docsTsconfig = {
|
|
834
|
+
extends: "../tsconfig.json",
|
|
835
|
+
compilerOptions: {
|
|
836
|
+
jsx: "preserve",
|
|
837
|
+
outDir: "dist",
|
|
838
|
+
noEmit: true,
|
|
839
|
+
},
|
|
840
|
+
include: ["src", "next.config.ts", "next-env.d.ts", "content"],
|
|
841
|
+
exclude: ["node_modules", ".next"],
|
|
842
|
+
};
|
|
843
|
+
writeFileSync(join(projectDir, "docs/tsconfig.json"), JSON.stringify(docsTsconfig, null, 2) + "\n");
|
|
844
|
+
console.log(green(" ✓ docs/tsconfig.json"));
|
|
845
|
+
|
|
846
|
+
// For fumadocs, docs content goes under content/docs/ at project root
|
|
847
|
+
mkdirSync(join(projectDir, "content/docs"), { recursive: true });
|
|
848
|
+
|
|
849
|
+
// content/docs/meta.json
|
|
850
|
+
const metaJson = {
|
|
851
|
+
title: name,
|
|
852
|
+
pages: ["index"],
|
|
853
|
+
};
|
|
854
|
+
writeFileSync(join(projectDir, "content/docs/meta.json"), JSON.stringify(metaJson, null, 2) + "\n");
|
|
855
|
+
console.log(green(" ✓ content/docs/meta.json"));
|
|
856
|
+
|
|
857
|
+
// content/docs/index.mdx
|
|
858
|
+
const indexMdx = `---
|
|
859
|
+
title: ${name}
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
# ${name}
|
|
863
|
+
|
|
864
|
+
Welcome to the ${name} documentation.
|
|
865
|
+
|
|
866
|
+
## Getting Started
|
|
867
|
+
|
|
868
|
+
This project is set up as a monorepo using Turborepo and pnpm workspaces.
|
|
869
|
+
|
|
870
|
+
\`\`\`bash
|
|
871
|
+
pnpm install
|
|
872
|
+
pnpm dev
|
|
873
|
+
\`\`\`
|
|
874
|
+
|
|
875
|
+
## Project Structure
|
|
876
|
+
|
|
877
|
+
- \`apps/\` — Application packages
|
|
878
|
+
- \`packages/\` — Shared library packages
|
|
879
|
+
- \`docs/\` — This documentation site
|
|
880
|
+
`;
|
|
881
|
+
writeFileSync(join(projectDir, "content/docs/index.mdx"), indexMdx);
|
|
882
|
+
console.log(green(" ✓ content/docs/index.mdx"));
|
|
883
|
+
}
|
|
884
|
+
console.log();
|
|
885
|
+
|
|
886
|
+
// --- Skills & Hooks ---
|
|
887
|
+
console.log(bold("Installing skills..."));
|
|
888
|
+
const skillsResult = copySkills(projectDir, YSTACK_ROOT);
|
|
889
|
+
console.log(dim(` ${skillsResult.installed} installed, ${skillsResult.skipped} skipped\n`));
|
|
890
|
+
|
|
891
|
+
console.log(bold("Installing hooks..."));
|
|
892
|
+
installHooks(projectDir, YSTACK_ROOT);
|
|
893
|
+
console.log(green(" ✓ context-monitor (PostToolUse)"));
|
|
894
|
+
console.log(green(" ✓ workflow-nudge (PreToolUse on Edit)\n"));
|
|
895
|
+
|
|
896
|
+
// --- Git init ---
|
|
897
|
+
console.log(bold("Initializing git..."));
|
|
898
|
+
try {
|
|
899
|
+
execSync("git init", { cwd: projectDir, stdio: "ignore" });
|
|
900
|
+
console.log(green(" ✓ git init\n"));
|
|
901
|
+
} catch {
|
|
902
|
+
console.log(yellow(" ⚠ git init failed — initialize manually\n"));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// --- Summary ---
|
|
906
|
+
p.note(
|
|
907
|
+
[
|
|
908
|
+
`Monorepo: Turborepo + pnpm workspaces`,
|
|
909
|
+
`Linting: Ultracite (Biome)`,
|
|
910
|
+
`Docs: ${docsFramework === "nextra" ? "Nextra 4" : "Fumadocs"}`,
|
|
911
|
+
`TypeScript: Strict mode, ES2022`,
|
|
912
|
+
`Skills: ${skillsResult.installed} ystack skills`,
|
|
913
|
+
`Hooks: context-monitor, workflow-nudge`,
|
|
914
|
+
].join("\n"),
|
|
915
|
+
`Created ${name}`,
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
p.note(
|
|
919
|
+
[
|
|
920
|
+
`cd ${name}`,
|
|
921
|
+
"pnpm install",
|
|
922
|
+
"pnpm dev",
|
|
923
|
+
].join("\n"),
|
|
924
|
+
"Next steps",
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
p.outro("Done!");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function usage() {
|
|
931
|
+
console.log(`
|
|
932
|
+
${bold("ystack")} — agent harness for doc-driven development
|
|
933
|
+
|
|
934
|
+
${bold("Commands:")}
|
|
935
|
+
${green("init")} Interactive setup — configure docs, beads, runtime, hooks
|
|
936
|
+
${green("init --skills-only")} Install skills only, skip everything else
|
|
937
|
+
${green("update")} Update skills and hooks to latest version
|
|
938
|
+
${green("remove")} Remove ystack skills and hooks (keeps data)
|
|
939
|
+
${green("create <name>")} Scaffold a new project with opinionated defaults
|
|
940
|
+
${dim(" --docs nextra|fumadocs")} Choose docs framework (default: nextra)
|
|
941
|
+
${dim(" --from plan.md")} Scaffold integration (coming soon)
|
|
942
|
+
|
|
943
|
+
${bold("Docs:")}
|
|
944
|
+
https://github.com/yulonghe97/ystack
|
|
945
|
+
`);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// --- Router ---
|
|
949
|
+
|
|
950
|
+
switch (command) {
|
|
951
|
+
case "init":
|
|
952
|
+
cmdInit();
|
|
953
|
+
break;
|
|
954
|
+
case "update":
|
|
955
|
+
cmdUpdate();
|
|
956
|
+
break;
|
|
957
|
+
case "remove":
|
|
958
|
+
cmdRemove();
|
|
959
|
+
break;
|
|
960
|
+
case "create":
|
|
961
|
+
cmdCreate();
|
|
962
|
+
break;
|
|
963
|
+
case "--help":
|
|
964
|
+
case "-h":
|
|
965
|
+
case "help":
|
|
966
|
+
case undefined:
|
|
967
|
+
usage();
|
|
968
|
+
break;
|
|
969
|
+
default:
|
|
970
|
+
console.log(red(`Unknown command: ${command}\n`));
|
|
971
|
+
usage();
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|