squad-kit 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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/cli.js +607 -0
- package/dist/cli.js.map +1 -0
- package/package.json +60 -0
- package/templates/agents/claude-code/squad-new-story.md +18 -0
- package/templates/agents/claude-code/squad-plan.md +29 -0
- package/templates/agents/copilot/squad-plan.prompt.md +14 -0
- package/templates/agents/cursor/squad-new-story.md +9 -0
- package/templates/agents/cursor/squad-plan.md +26 -0
- package/templates/agents/gemini/squad-plan.toml +14 -0
- package/templates/index.md +7 -0
- package/templates/overview.md +13 -0
- package/templates/prompts/generate-plan.md +89 -0
- package/templates/prompts/intake.md +85 -0
- package/templates/prompts/story-skeleton.md +64 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import kleur7 from "kleur";
|
|
6
|
+
|
|
7
|
+
// src/commands/init.ts
|
|
8
|
+
import fs4 from "fs";
|
|
9
|
+
import path3 from "path";
|
|
10
|
+
import kleur from "kleur";
|
|
11
|
+
import { input, select, checkbox, confirm } from "@inquirer/prompts";
|
|
12
|
+
|
|
13
|
+
// src/core/paths.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
var SQUAD_DIR = ".squad";
|
|
17
|
+
function buildPaths(root) {
|
|
18
|
+
const squadDir = path.join(root, SQUAD_DIR);
|
|
19
|
+
return {
|
|
20
|
+
root,
|
|
21
|
+
squadDir,
|
|
22
|
+
configFile: path.join(squadDir, "config.yaml"),
|
|
23
|
+
promptsDir: path.join(squadDir, "prompts"),
|
|
24
|
+
storiesDir: path.join(squadDir, "stories"),
|
|
25
|
+
plansDir: path.join(squadDir, "plans"),
|
|
26
|
+
indexFile: path.join(squadDir, "plans", "00-index.md")
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function findSquadRoot(startDir = process.cwd()) {
|
|
30
|
+
let current = path.resolve(startDir);
|
|
31
|
+
const { root } = path.parse(current);
|
|
32
|
+
while (true) {
|
|
33
|
+
if (fs.existsSync(path.join(current, SQUAD_DIR, "config.yaml"))) {
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
if (current === root) return null;
|
|
37
|
+
current = path.dirname(current);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function requireSquadRoot(startDir = process.cwd()) {
|
|
41
|
+
const root = findSquadRoot(startDir);
|
|
42
|
+
if (!root) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`No .squad/ directory found (searched upward from ${startDir}). Run \`squad init\` first.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
function slugify(input3) {
|
|
50
|
+
return input3.toLowerCase().trim().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/core/config.ts
|
|
54
|
+
import fs2 from "fs";
|
|
55
|
+
import yaml from "js-yaml";
|
|
56
|
+
var DEFAULT_CONFIG = {
|
|
57
|
+
version: 1,
|
|
58
|
+
project: {
|
|
59
|
+
name: "my-project",
|
|
60
|
+
primaryLanguage: "typescript",
|
|
61
|
+
projectRoots: ["."]
|
|
62
|
+
},
|
|
63
|
+
tracker: { type: "none" },
|
|
64
|
+
naming: { includeTrackerId: false, globalSequence: true },
|
|
65
|
+
agents: []
|
|
66
|
+
};
|
|
67
|
+
function loadConfig(configFile) {
|
|
68
|
+
const raw = fs2.readFileSync(configFile, "utf8");
|
|
69
|
+
const parsed = yaml.load(raw);
|
|
70
|
+
if (!parsed || typeof parsed !== "object") {
|
|
71
|
+
throw new Error(`Invalid config at ${configFile}: expected a YAML object.`);
|
|
72
|
+
}
|
|
73
|
+
return mergeConfig(DEFAULT_CONFIG, parsed);
|
|
74
|
+
}
|
|
75
|
+
function saveConfig(configFile, config) {
|
|
76
|
+
const body = yaml.dump(config, { lineWidth: 100, noRefs: true, sortKeys: false });
|
|
77
|
+
fs2.writeFileSync(configFile, body, "utf8");
|
|
78
|
+
}
|
|
79
|
+
function mergeConfig(base, override) {
|
|
80
|
+
return {
|
|
81
|
+
version: override.version ?? base.version,
|
|
82
|
+
project: { ...base.project, ...override.project ?? {} },
|
|
83
|
+
tracker: { ...base.tracker, ...override.tracker ?? {} },
|
|
84
|
+
naming: { ...base.naming, ...override.naming ?? {} },
|
|
85
|
+
agents: override.agents ?? base.agents
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/utils/fs.ts
|
|
90
|
+
import fs3 from "fs";
|
|
91
|
+
import path2 from "path";
|
|
92
|
+
import { fileURLToPath } from "url";
|
|
93
|
+
function readFile(file) {
|
|
94
|
+
return fs3.readFileSync(file, "utf8");
|
|
95
|
+
}
|
|
96
|
+
function writeFileSafe(file, content, overwrite = false) {
|
|
97
|
+
if (!overwrite && fs3.existsSync(file)) return false;
|
|
98
|
+
fs3.mkdirSync(path2.dirname(file), { recursive: true });
|
|
99
|
+
fs3.writeFileSync(file, content, "utf8");
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function copyTree(srcDir, destDir, overwrite = false) {
|
|
103
|
+
fs3.mkdirSync(destDir, { recursive: true });
|
|
104
|
+
for (const entry of fs3.readdirSync(srcDir, { withFileTypes: true })) {
|
|
105
|
+
const src = path2.join(srcDir, entry.name);
|
|
106
|
+
const dest = path2.join(destDir, entry.name);
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
copyTree(src, dest, overwrite);
|
|
109
|
+
} else if (entry.isFile()) {
|
|
110
|
+
if (!overwrite && fs3.existsSync(dest)) continue;
|
|
111
|
+
fs3.copyFileSync(src, dest);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function packageRoot() {
|
|
116
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
117
|
+
let dir = here;
|
|
118
|
+
for (let i = 0; i < 6; i++) {
|
|
119
|
+
if (fs3.existsSync(path2.join(dir, "package.json")) && fs3.existsSync(path2.join(dir, "templates"))) {
|
|
120
|
+
return dir;
|
|
121
|
+
}
|
|
122
|
+
dir = path2.dirname(dir);
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Could not locate squad-kit package root from ${here}`);
|
|
125
|
+
}
|
|
126
|
+
function templatesDir() {
|
|
127
|
+
return path2.join(packageRoot(), "templates");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/core/template.ts
|
|
131
|
+
var TOKEN = /\{\{\s*([\w.-]+)\s*\}\}/g;
|
|
132
|
+
function render(template, vars) {
|
|
133
|
+
return template.replace(TOKEN, (_, key) => {
|
|
134
|
+
const value = vars[key];
|
|
135
|
+
return value === void 0 || value === null ? "" : String(value);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/commands/init.ts
|
|
140
|
+
var SUPPORTED_AGENTS = ["claude-code", "cursor", "copilot", "gemini"];
|
|
141
|
+
var AGENT_INSTALL = {
|
|
142
|
+
"claude-code": { subdir: path3.join(".claude", "commands"), srcDir: "claude-code" },
|
|
143
|
+
cursor: { subdir: path3.join(".cursor", "commands"), srcDir: "cursor" },
|
|
144
|
+
copilot: { subdir: path3.join(".github", "prompts"), srcDir: "copilot" },
|
|
145
|
+
gemini: { subdir: path3.join(".gemini", "commands"), srcDir: "gemini" }
|
|
146
|
+
};
|
|
147
|
+
async function runInit(opts) {
|
|
148
|
+
const root = process.cwd();
|
|
149
|
+
const paths = buildPaths(root);
|
|
150
|
+
if (fs4.existsSync(paths.configFile) && !opts.force) {
|
|
151
|
+
console.log(kleur.yellow(`A ${SQUAD_DIR}/config.yaml already exists.`));
|
|
152
|
+
console.log(` To add agents or change settings, re-run with ${kleur.bold("--force")} or edit the file directly.`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const defaults = {
|
|
156
|
+
name: opts.name ?? path3.basename(root),
|
|
157
|
+
language: opts.language ?? "typescript",
|
|
158
|
+
tracker: opts.tracker ?? "none",
|
|
159
|
+
agents: parseAgentsFlag(opts.agents)
|
|
160
|
+
};
|
|
161
|
+
const answers = opts.yes ? defaults : {
|
|
162
|
+
name: await input({ message: "Project name", default: defaults.name }),
|
|
163
|
+
language: await input({ message: "Primary language", default: defaults.language }),
|
|
164
|
+
tracker: await select({
|
|
165
|
+
message: "Issue tracker",
|
|
166
|
+
choices: [
|
|
167
|
+
{ name: "None", value: "none" },
|
|
168
|
+
{ name: "GitHub Issues", value: "github" },
|
|
169
|
+
{ name: "Linear", value: "linear" },
|
|
170
|
+
{ name: "Jira", value: "jira" },
|
|
171
|
+
{ name: "Azure DevOps", value: "azure" }
|
|
172
|
+
],
|
|
173
|
+
default: defaults.tracker
|
|
174
|
+
}),
|
|
175
|
+
agents: await checkbox({
|
|
176
|
+
message: "Install slash commands for which agents?",
|
|
177
|
+
choices: SUPPORTED_AGENTS.map((a) => ({ name: a, value: a, checked: defaults.agents.includes(a) }))
|
|
178
|
+
})
|
|
179
|
+
};
|
|
180
|
+
const includeTrackerId = answers.tracker !== "none" ? await confirmSafe("Include tracker id in plan filenames (NN-story-<slug>-<id>.md)?", true, !!opts.yes) : false;
|
|
181
|
+
const config = {
|
|
182
|
+
version: 1,
|
|
183
|
+
project: { name: answers.name, primaryLanguage: answers.language, projectRoots: ["."] },
|
|
184
|
+
tracker: { type: answers.tracker },
|
|
185
|
+
naming: { includeTrackerId, globalSequence: true },
|
|
186
|
+
agents: answers.agents
|
|
187
|
+
};
|
|
188
|
+
fs4.mkdirSync(paths.squadDir, { recursive: true });
|
|
189
|
+
fs4.mkdirSync(paths.promptsDir, { recursive: true });
|
|
190
|
+
fs4.mkdirSync(paths.storiesDir, { recursive: true });
|
|
191
|
+
fs4.mkdirSync(paths.plansDir, { recursive: true });
|
|
192
|
+
copyTree(path3.join(templatesDir(), "prompts"), paths.promptsDir, !!opts.force);
|
|
193
|
+
writeFileSafe(
|
|
194
|
+
paths.indexFile,
|
|
195
|
+
readFile(path3.join(templatesDir(), "index.md")),
|
|
196
|
+
!!opts.force
|
|
197
|
+
);
|
|
198
|
+
saveConfig(paths.configFile, config);
|
|
199
|
+
writeFileSafe(
|
|
200
|
+
path3.join(paths.squadDir, "README.md"),
|
|
201
|
+
renderSquadReadme(config),
|
|
202
|
+
!!opts.force
|
|
203
|
+
);
|
|
204
|
+
for (const agent of answers.agents) {
|
|
205
|
+
installAgent(root, agent, !!opts.force);
|
|
206
|
+
}
|
|
207
|
+
console.log(kleur.green(`
|
|
208
|
+
\u2713 Initialized ${SQUAD_DIR}/ at ${root}`));
|
|
209
|
+
console.log(` tracker: ${config.tracker.type}`);
|
|
210
|
+
console.log(` agents: ${answers.agents.length ? answers.agents.join(", ") : "(none)"}`);
|
|
211
|
+
console.log(`
|
|
212
|
+
Next:`);
|
|
213
|
+
console.log(` 1) ${kleur.bold("squad new-story <feature-slug>")}`);
|
|
214
|
+
console.log(` 2) Fill the generated intake.md, then run ${kleur.bold("/squad-plan")} in your agent (or ${kleur.bold("squad new-plan")}).`);
|
|
215
|
+
}
|
|
216
|
+
function parseAgentsFlag(flag) {
|
|
217
|
+
if (!flag) return [];
|
|
218
|
+
return flag.split(",").map((s) => s.trim()).filter((s) => SUPPORTED_AGENTS.includes(s));
|
|
219
|
+
}
|
|
220
|
+
async function confirmSafe(message, defaultValue, nonInteractive) {
|
|
221
|
+
if (nonInteractive) return defaultValue;
|
|
222
|
+
return confirm({ message, default: defaultValue });
|
|
223
|
+
}
|
|
224
|
+
function installAgent(root, agent, overwrite) {
|
|
225
|
+
const config = AGENT_INSTALL[agent];
|
|
226
|
+
const src = path3.join(templatesDir(), "agents", config.srcDir);
|
|
227
|
+
const dest = path3.join(root, config.subdir);
|
|
228
|
+
if (!fs4.existsSync(src)) return;
|
|
229
|
+
copyTree(src, dest, overwrite);
|
|
230
|
+
}
|
|
231
|
+
function renderSquadReadme(config) {
|
|
232
|
+
return render(
|
|
233
|
+
`# squad-kit workspace
|
|
234
|
+
|
|
235
|
+
This folder is managed by [squad-kit](https://github.com/AzmSquad/squad-kit).
|
|
236
|
+
|
|
237
|
+
- **Project:** {{name}}
|
|
238
|
+
- **Language:** {{language}}
|
|
239
|
+
- **Tracker:** {{tracker}}
|
|
240
|
+
|
|
241
|
+
## Workflow
|
|
242
|
+
|
|
243
|
+
1. **Intake** \u2014 \`squad new-story <feature-slug>\` scaffolds \`stories/<feature>/<id>/intake.md\`. Paste the tracker title, description, and acceptance criteria.
|
|
244
|
+
2. **Plan** \u2014 Run \`/squad-plan <intake-path>\` in your agent (or \`squad new-plan <intake-path>\` to get the composed prompt on stdout).
|
|
245
|
+
3. **Implement** \u2014 Open a new, scoped agent session and attach **only** the generated \`NN-story-*.md\` file. Let a cheap model execute it.
|
|
246
|
+
|
|
247
|
+
See \`prompts/generate-plan.md\` for the plan-generation meta-prompt and \`prompts/story-skeleton.md\` for the target plan structure.
|
|
248
|
+
`,
|
|
249
|
+
{ name: config.project.name, language: config.project.primaryLanguage ?? "", tracker: config.tracker.type }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/commands/new-story.ts
|
|
254
|
+
import fs5 from "fs";
|
|
255
|
+
import path4 from "path";
|
|
256
|
+
import kleur2 from "kleur";
|
|
257
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
258
|
+
|
|
259
|
+
// src/core/tracker.ts
|
|
260
|
+
var VALIDATORS = {
|
|
261
|
+
github: /^(?:[\w.-]+\/[\w.-]+#)?\d+$/,
|
|
262
|
+
linear: /^[A-Z][A-Z0-9]+-\d+$/i,
|
|
263
|
+
jira: /^[A-Z][A-Z0-9]+-\d+$/,
|
|
264
|
+
azure: /^\d+$/
|
|
265
|
+
};
|
|
266
|
+
function validateTrackerId(type, id) {
|
|
267
|
+
if (type === "none") return true;
|
|
268
|
+
const pattern = VALIDATORS[type];
|
|
269
|
+
return pattern.test(id);
|
|
270
|
+
}
|
|
271
|
+
function trackerIdForFilename(type, id) {
|
|
272
|
+
if (type === "github") return id.replace(/^[\w.-]+\/[\w.-]+#/, "");
|
|
273
|
+
return id;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/commands/new-story.ts
|
|
277
|
+
async function runNewStory(featureSlug, opts) {
|
|
278
|
+
if (!featureSlug) {
|
|
279
|
+
throw new Error('feature-slug is required. Usage: squad new-story <feature-slug> [--id <tracker-id>] [--title "..."]');
|
|
280
|
+
}
|
|
281
|
+
const slug = slugify(featureSlug);
|
|
282
|
+
if (!slug) throw new Error(`Invalid feature slug: "${featureSlug}"`);
|
|
283
|
+
const root = requireSquadRoot();
|
|
284
|
+
const paths = buildPaths(root);
|
|
285
|
+
const config = loadConfig(paths.configFile);
|
|
286
|
+
const interactive = !opts.yes && Boolean(process.stdin.isTTY);
|
|
287
|
+
const trackerNeedsId = config.tracker.type !== "none" && config.naming.includeTrackerId;
|
|
288
|
+
let id = opts.id;
|
|
289
|
+
let title = opts.title;
|
|
290
|
+
if (!id && trackerNeedsId) {
|
|
291
|
+
if (!interactive) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Tracker "${config.tracker.type}" is configured and naming.includeTrackerId is true. Pass --id <tracker-id>.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
id = await input2({
|
|
297
|
+
message: `${config.tracker.type} work-item id:`,
|
|
298
|
+
validate: (v) => validateTrackerId(config.tracker.type, v.trim()) ? true : `Invalid ${config.tracker.type} id format.`
|
|
299
|
+
});
|
|
300
|
+
id = id.trim();
|
|
301
|
+
}
|
|
302
|
+
if (id && !validateTrackerId(config.tracker.type, id)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Invalid tracker id "${id}" for tracker type "${config.tracker.type}". Check .squad/config.yaml or pass --id in the right format.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (!id && !title) {
|
|
308
|
+
if (!interactive) {
|
|
309
|
+
throw new Error('Provide either --id <tracker-id> or --title "..." so the story folder has a distinct name.');
|
|
310
|
+
}
|
|
311
|
+
title = (await input2({ message: "Short story title:" })).trim();
|
|
312
|
+
if (!title) throw new Error("Title cannot be empty.");
|
|
313
|
+
}
|
|
314
|
+
if (interactive && !title) {
|
|
315
|
+
const maybe = (await input2({ message: "Short title (optional, press Enter to skip):", default: "" })).trim();
|
|
316
|
+
if (maybe) title = maybe;
|
|
317
|
+
}
|
|
318
|
+
const storyFolderName = id ? trackerIdForFilename(config.tracker.type, id) : slugify(title);
|
|
319
|
+
if (!storyFolderName) throw new Error("Could not derive a story folder name from --id or --title.");
|
|
320
|
+
const storyDir = path4.join(paths.storiesDir, slug, storyFolderName);
|
|
321
|
+
const intakeFile = path4.join(storyDir, "intake.md");
|
|
322
|
+
const attachmentsDir = path4.join(storyDir, "attachments");
|
|
323
|
+
if (fs5.existsSync(intakeFile)) {
|
|
324
|
+
throw new Error(`Intake already exists: ${intakeFile}`);
|
|
325
|
+
}
|
|
326
|
+
fs5.mkdirSync(attachmentsDir, { recursive: true });
|
|
327
|
+
const template = readFile(path4.join(templatesDir(), "prompts", "intake.md"));
|
|
328
|
+
const rendered = render(template, {
|
|
329
|
+
featureSlug: slug,
|
|
330
|
+
storyId: storyFolderName,
|
|
331
|
+
trackerType: config.tracker.type,
|
|
332
|
+
projectRoots: (config.project.projectRoots ?? ["."]).join(", "),
|
|
333
|
+
primaryLanguage: config.project.primaryLanguage ?? ""
|
|
334
|
+
});
|
|
335
|
+
const preamble = title ? `> **Title hint (from CLI):** ${title}
|
|
336
|
+
|
|
337
|
+
` : "";
|
|
338
|
+
writeFileSafe(intakeFile, preamble + rendered, false);
|
|
339
|
+
ensureFeatureOverview(path4.join(paths.plansDir, slug), slug);
|
|
340
|
+
console.log(kleur2.green(`\u2713 Created intake:`));
|
|
341
|
+
console.log(` ${path4.relative(root, intakeFile)}`);
|
|
342
|
+
console.log(` ${path4.relative(root, attachmentsDir)}${path4.sep}`);
|
|
343
|
+
console.log(`
|
|
344
|
+
Next: paste the tracker title, description, and acceptance criteria into the intake, then:`);
|
|
345
|
+
console.log(` ${kleur2.bold(`squad new-plan ${path4.relative(root, intakeFile)}`)}`);
|
|
346
|
+
console.log(` (or run ${kleur2.bold("/squad-plan")} in your agent)`);
|
|
347
|
+
}
|
|
348
|
+
function ensureFeatureOverview(featurePlanDir, slug) {
|
|
349
|
+
fs5.mkdirSync(featurePlanDir, { recursive: true });
|
|
350
|
+
const overviewFile = path4.join(featurePlanDir, "00-overview.md");
|
|
351
|
+
if (fs5.existsSync(overviewFile)) return;
|
|
352
|
+
const template = readFile(path4.join(templatesDir(), "overview.md"));
|
|
353
|
+
writeFileSafe(overviewFile, render(template, { featureSlug: slug }), false);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/new-plan.ts
|
|
357
|
+
import fs6 from "fs";
|
|
358
|
+
import path5 from "path";
|
|
359
|
+
import kleur3 from "kleur";
|
|
360
|
+
|
|
361
|
+
// src/utils/clipboard.ts
|
|
362
|
+
import { spawn } from "child_process";
|
|
363
|
+
async function copyToClipboard(text) {
|
|
364
|
+
const cmd = clipboardCommand();
|
|
365
|
+
if (!cmd) return false;
|
|
366
|
+
return new Promise((resolve) => {
|
|
367
|
+
const child = spawn(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
368
|
+
child.on("error", () => resolve(false));
|
|
369
|
+
child.on("close", (code) => resolve(code === 0));
|
|
370
|
+
child.stdin.write(text);
|
|
371
|
+
child.stdin.end();
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function clipboardCommand() {
|
|
375
|
+
if (process.platform === "darwin") return { command: "pbcopy", args: [] };
|
|
376
|
+
if (process.platform === "win32") return { command: "clip", args: [] };
|
|
377
|
+
if (process.env.WAYLAND_DISPLAY) return { command: "wl-copy", args: [] };
|
|
378
|
+
if (process.env.DISPLAY) return { command: "xclip", args: ["-selection", "clipboard"] };
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/commands/new-plan.ts
|
|
383
|
+
async function runNewPlan(intakePath, opts) {
|
|
384
|
+
if (!intakePath) {
|
|
385
|
+
throw new Error("intake-path is required. Usage: squad new-plan <path-to-intake.md>");
|
|
386
|
+
}
|
|
387
|
+
const root = requireSquadRoot();
|
|
388
|
+
const paths = buildPaths(root);
|
|
389
|
+
const config = loadConfig(paths.configFile);
|
|
390
|
+
const resolved = path5.resolve(intakePath);
|
|
391
|
+
if (!fs6.existsSync(resolved)) throw new Error(`Intake not found: ${resolved}`);
|
|
392
|
+
const intakeContent = readFile(resolved);
|
|
393
|
+
const metaPromptFile = path5.join(paths.promptsDir, "generate-plan.md");
|
|
394
|
+
if (!fs6.existsSync(metaPromptFile)) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Missing meta-prompt at ${metaPromptFile}. Re-run \`squad init --force\` to restore templates.`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const metaPrompt = readFile(metaPromptFile);
|
|
400
|
+
const composed = render(metaPrompt, {
|
|
401
|
+
projectRoots: (config.project.projectRoots ?? ["."]).join(", "),
|
|
402
|
+
primaryLanguage: config.project.primaryLanguage ?? "",
|
|
403
|
+
trackerType: config.tracker.type,
|
|
404
|
+
intakeContent
|
|
405
|
+
});
|
|
406
|
+
process.stdout.write(composed);
|
|
407
|
+
if (!composed.endsWith("\n")) process.stdout.write("\n");
|
|
408
|
+
if (opts.copy !== false) {
|
|
409
|
+
const ok = await copyToClipboard(composed);
|
|
410
|
+
if (ok) console.error(kleur3.dim("\n(copied to clipboard)"));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/commands/status.ts
|
|
415
|
+
import fs8 from "fs";
|
|
416
|
+
import path7 from "path";
|
|
417
|
+
import kleur4 from "kleur";
|
|
418
|
+
|
|
419
|
+
// src/core/sequence.ts
|
|
420
|
+
import fs7 from "fs";
|
|
421
|
+
import path6 from "path";
|
|
422
|
+
var STORY_FILE = /^(\d{2,})-story-.+\.md$/;
|
|
423
|
+
function scanPlans(plansDir) {
|
|
424
|
+
const used = [];
|
|
425
|
+
const seen = /* @__PURE__ */ new Map();
|
|
426
|
+
if (fs7.existsSync(plansDir)) {
|
|
427
|
+
for (const feature of fs7.readdirSync(plansDir, { withFileTypes: true })) {
|
|
428
|
+
if (!feature.isDirectory()) continue;
|
|
429
|
+
const featureDir = path6.join(plansDir, feature.name);
|
|
430
|
+
for (const entry of fs7.readdirSync(featureDir)) {
|
|
431
|
+
const match = entry.match(STORY_FILE);
|
|
432
|
+
if (!match) continue;
|
|
433
|
+
const n = parseInt(match[1], 10);
|
|
434
|
+
used.push(n);
|
|
435
|
+
seen.set(n, (seen.get(n) ?? 0) + 1);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const duplicates = [...seen.entries()].filter(([, c]) => c > 1).map(([n]) => n);
|
|
440
|
+
const maxNumber = used.length ? Math.max(...used) : 0;
|
|
441
|
+
return { usedNumbers: used.sort((a, b) => a - b), maxNumber, duplicates, nextGlobal: maxNumber + 1 };
|
|
442
|
+
}
|
|
443
|
+
function formatSequence(n) {
|
|
444
|
+
return String(n).padStart(2, "0");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/commands/status.ts
|
|
448
|
+
async function runStatus() {
|
|
449
|
+
const root = requireSquadRoot();
|
|
450
|
+
const paths = buildPaths(root);
|
|
451
|
+
const config = loadConfig(paths.configFile);
|
|
452
|
+
const scan = scanPlans(paths.plansDir);
|
|
453
|
+
const storyCount = countStories(paths.storiesDir);
|
|
454
|
+
const planCount = scan.usedNumbers.length;
|
|
455
|
+
console.log(kleur4.bold("squad-kit status"));
|
|
456
|
+
console.log(` project: ${config.project.name}`);
|
|
457
|
+
console.log(` tracker: ${config.tracker.type}`);
|
|
458
|
+
console.log(` agents: ${config.agents.length ? config.agents.join(", ") : "(none)"}`);
|
|
459
|
+
console.log(` stories (drafts + planned): ${storyCount}`);
|
|
460
|
+
console.log(` plan files: ${planCount}`);
|
|
461
|
+
console.log(` next NN: ${formatSequence(scan.nextGlobal)}`);
|
|
462
|
+
if (scan.duplicates.length > 0) {
|
|
463
|
+
console.log(kleur4.yellow(` \u26A0 duplicate NN numbers: ${scan.duplicates.map(formatSequence).join(", ")}`));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function countStories(storiesDir) {
|
|
467
|
+
if (!fs8.existsSync(storiesDir)) return 0;
|
|
468
|
+
let total = 0;
|
|
469
|
+
for (const feature of fs8.readdirSync(storiesDir, { withFileTypes: true })) {
|
|
470
|
+
if (!feature.isDirectory()) continue;
|
|
471
|
+
const featureDir = path7.join(storiesDir, feature.name);
|
|
472
|
+
for (const story of fs8.readdirSync(featureDir, { withFileTypes: true })) {
|
|
473
|
+
if (!story.isDirectory()) continue;
|
|
474
|
+
if (fs8.existsSync(path7.join(featureDir, story.name, "intake.md"))) total++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return total;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/commands/list.ts
|
|
481
|
+
import fs9 from "fs";
|
|
482
|
+
import path8 from "path";
|
|
483
|
+
import kleur5 from "kleur";
|
|
484
|
+
async function runList(opts) {
|
|
485
|
+
const root = requireSquadRoot();
|
|
486
|
+
const paths = buildPaths(root);
|
|
487
|
+
const rows = [];
|
|
488
|
+
if (fs9.existsSync(paths.storiesDir)) {
|
|
489
|
+
for (const feature of fs9.readdirSync(paths.storiesDir, { withFileTypes: true })) {
|
|
490
|
+
if (!feature.isDirectory()) continue;
|
|
491
|
+
if (opts.feature && feature.name !== opts.feature) continue;
|
|
492
|
+
const featureDir = path8.join(paths.storiesDir, feature.name);
|
|
493
|
+
for (const story of fs9.readdirSync(featureDir, { withFileTypes: true })) {
|
|
494
|
+
if (!story.isDirectory()) continue;
|
|
495
|
+
const intake = path8.join(featureDir, story.name, "intake.md");
|
|
496
|
+
rows.push({
|
|
497
|
+
feature: feature.name,
|
|
498
|
+
id: story.name,
|
|
499
|
+
hasIntake: fs9.existsSync(intake),
|
|
500
|
+
planFile: findPlan(paths.plansDir, feature.name, story.name)
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (rows.length === 0) {
|
|
506
|
+
console.log(kleur5.dim("(no stories yet \u2014 try `squad new-story <feature-slug>`)"));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const header = ["feature", "story", "intake", "plan"];
|
|
510
|
+
const table = rows.map((r) => [
|
|
511
|
+
r.feature,
|
|
512
|
+
r.id,
|
|
513
|
+
r.hasIntake ? "yes" : "\u2014",
|
|
514
|
+
r.planFile ?? kleur5.dim("\u2014")
|
|
515
|
+
]);
|
|
516
|
+
printTable([header, ...table]);
|
|
517
|
+
}
|
|
518
|
+
function findPlan(plansDir, feature, storyId) {
|
|
519
|
+
const featureDir = path8.join(plansDir, feature);
|
|
520
|
+
if (!fs9.existsSync(featureDir)) return void 0;
|
|
521
|
+
for (const entry of fs9.readdirSync(featureDir)) {
|
|
522
|
+
if (entry.includes(storyId) && entry.endsWith(".md")) return entry;
|
|
523
|
+
}
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
function printTable(rows) {
|
|
527
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => stripAnsi(r[i] ?? "").length)));
|
|
528
|
+
for (const row of rows) {
|
|
529
|
+
console.log(row.map((cell, i) => pad(cell, widths[i])).join(" "));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function pad(s, width) {
|
|
533
|
+
const visible = stripAnsi(s).length;
|
|
534
|
+
return s + " ".repeat(Math.max(0, width - visible));
|
|
535
|
+
}
|
|
536
|
+
function stripAnsi(s) {
|
|
537
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/commands/tracker-link.ts
|
|
541
|
+
import fs10 from "fs";
|
|
542
|
+
import path9 from "path";
|
|
543
|
+
import kleur6 from "kleur";
|
|
544
|
+
async function runTrackerLink(storyPath, trackerId) {
|
|
545
|
+
if (!storyPath || !trackerId) {
|
|
546
|
+
throw new Error("Usage: squad tracker link <story-folder-or-intake> <tracker-id>");
|
|
547
|
+
}
|
|
548
|
+
const root = requireSquadRoot();
|
|
549
|
+
const paths = buildPaths(root);
|
|
550
|
+
const config = loadConfig(paths.configFile);
|
|
551
|
+
if (!validateTrackerId(config.tracker.type, trackerId)) {
|
|
552
|
+
throw new Error(`Invalid tracker id "${trackerId}" for tracker type "${config.tracker.type}".`);
|
|
553
|
+
}
|
|
554
|
+
const resolved = path9.resolve(storyPath);
|
|
555
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path9.join(resolved, "intake.md");
|
|
556
|
+
if (!fs10.existsSync(intakeFile)) throw new Error(`intake.md not found at ${intakeFile}`);
|
|
557
|
+
const content = fs10.readFileSync(intakeFile, "utf8");
|
|
558
|
+
const updated = upsertTrackerId(content, trackerId);
|
|
559
|
+
fs10.writeFileSync(intakeFile, updated, "utf8");
|
|
560
|
+
console.log(kleur6.green(`\u2713 Linked tracker id ${trackerId} \u2192 ${path9.relative(root, intakeFile)}`));
|
|
561
|
+
}
|
|
562
|
+
function upsertTrackerId(content, id) {
|
|
563
|
+
const pattern = /(-\s*\*\*Work item id:\*\*)[^\n]*/;
|
|
564
|
+
if (pattern.test(content)) {
|
|
565
|
+
return content.replace(pattern, `$1 ${id}`);
|
|
566
|
+
}
|
|
567
|
+
return content + `
|
|
568
|
+
|
|
569
|
+
- **Work item id:** ${id}
|
|
570
|
+
`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/cli.ts
|
|
574
|
+
var program = new Command();
|
|
575
|
+
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.1.0");
|
|
576
|
+
program.command("init").description("Bootstrap .squad/ in the current directory").option("--agents <list>", "Comma-separated agent list: claude-code,cursor,copilot,gemini").option("--tracker <type>", "Tracker type: none|github|linear|jira|azure").option("--name <name>", "Project name").option("--language <lang>", "Primary language").option("--force", "Overwrite existing files", false).option("-y, --yes", "Accept defaults (non-interactive)", false).action(wrap(runInit));
|
|
577
|
+
program.command("new-story <feature-slug>").description("Scaffold a new story intake under .squad/stories/<feature>/<id>/").option("--id <id>", "Tracker work-item id (required when tracker is not none and naming.includeTrackerId is true)").option("--title <title>", "Short title hint (placed at top of intake)").option("-y, --yes", "Fail fast instead of prompting for missing values", false).action(wrapArgs(runNewStory));
|
|
578
|
+
program.command("new-plan <intake-path>").description("Compose the plan-generation meta-prompt with intake content; prints to stdout and copies to clipboard").option("--no-copy", "Do not copy the composed prompt to clipboard").action(wrapArgs(runNewPlan));
|
|
579
|
+
program.command("status").description("Show squad-kit workspace status").action(wrap(runStatus));
|
|
580
|
+
program.command("list").description("List stories and their plan state").option("--feature <slug>", "Filter by feature slug").action(wrap(runList));
|
|
581
|
+
var tracker = program.command("tracker").description("Tracker id helpers");
|
|
582
|
+
tracker.command("link <story-path> <tracker-id>").description("Attach/update a tracker id on an existing story intake").action(wrapArgs(runTrackerLink));
|
|
583
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
584
|
+
console.error(kleur7.red(`error: ${err.message}`));
|
|
585
|
+
process.exit(1);
|
|
586
|
+
});
|
|
587
|
+
function wrap(fn) {
|
|
588
|
+
return async (opts) => {
|
|
589
|
+
try {
|
|
590
|
+
await fn(opts);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
console.error(kleur7.red(`error: ${err.message}`));
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function wrapArgs(fn) {
|
|
598
|
+
return async (...args) => {
|
|
599
|
+
try {
|
|
600
|
+
await fn(...args);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
console.error(kleur7.red(`error: ${err.message}`));
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
//# sourceMappingURL=cli.js.map
|