@sduck/sduck-cli 0.1.3 → 0.1.5
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/.sduck/sduck-assets/agent-rules/antigravity.md +1 -0
- package/.sduck/sduck-assets/agent-rules/claude-code.md +1 -0
- package/.sduck/sduck-assets/agent-rules/codex.md +1 -0
- package/.sduck/sduck-assets/agent-rules/core.md +15 -0
- package/.sduck/sduck-assets/agent-rules/cursor.mdc +1 -0
- package/.sduck/sduck-assets/agent-rules/gemini-cli.md +1 -0
- package/.sduck/sduck-assets/agent-rules/hooks/sdd-guard.sh +125 -0
- package/.sduck/sduck-assets/agent-rules/opencode.md +1 -0
- package/README.md +40 -0
- package/dist/cli.js +266 -72
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/core/
|
|
7
|
-
import { readFile as readFile2,
|
|
6
|
+
// src/core/archive.ts
|
|
7
|
+
import { readFile as readFile2, rename } from "fs/promises";
|
|
8
8
|
import { join as join3 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/core/fs.ts
|
|
@@ -39,9 +39,11 @@ import { join, relative } from "path";
|
|
|
39
39
|
var SDUCK_HOME_DIR = ".sduck";
|
|
40
40
|
var SDUCK_ASSETS_DIR = "sduck-assets";
|
|
41
41
|
var SDUCK_WORKSPACE_DIR = "sduck-workspace";
|
|
42
|
+
var SDUCK_ARCHIVE_DIR = "sduck-archive";
|
|
42
43
|
var PROJECT_SDUCK_HOME_RELATIVE_PATH = SDUCK_HOME_DIR;
|
|
43
44
|
var PROJECT_SDUCK_ASSETS_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_ASSETS_DIR);
|
|
44
45
|
var PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_WORKSPACE_DIR);
|
|
46
|
+
var PROJECT_SDUCK_ARCHIVE_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_ARCHIVE_DIR);
|
|
45
47
|
function getProjectSduckHomePath(projectRoot) {
|
|
46
48
|
return join(projectRoot, PROJECT_SDUCK_HOME_RELATIVE_PATH);
|
|
47
49
|
}
|
|
@@ -51,6 +53,9 @@ function getProjectSduckAssetsPath(projectRoot) {
|
|
|
51
53
|
function getProjectSduckWorkspacePath(projectRoot) {
|
|
52
54
|
return join(projectRoot, PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH);
|
|
53
55
|
}
|
|
56
|
+
function getProjectSduckArchivePath(projectRoot) {
|
|
57
|
+
return join(projectRoot, PROJECT_SDUCK_ARCHIVE_RELATIVE_PATH);
|
|
58
|
+
}
|
|
54
59
|
function getProjectRelativeSduckAssetPath(...segments) {
|
|
55
60
|
return join(PROJECT_SDUCK_ASSETS_RELATIVE_PATH, ...segments);
|
|
56
61
|
}
|
|
@@ -141,6 +146,128 @@ async function findActiveTask(projectRoot) {
|
|
|
141
146
|
return null;
|
|
142
147
|
}
|
|
143
148
|
|
|
149
|
+
// src/core/archive.ts
|
|
150
|
+
function extractCompletedAt(metaContent) {
|
|
151
|
+
const match = /^completed_at:\s+(.+)$/m.exec(metaContent);
|
|
152
|
+
const value = match?.[1]?.trim();
|
|
153
|
+
if (value === void 0 || value === "null") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
function deriveArchiveMonth(completedAt) {
|
|
159
|
+
return completedAt.slice(0, 7);
|
|
160
|
+
}
|
|
161
|
+
function filterArchiveCandidates(tasks) {
|
|
162
|
+
return tasks.filter((task) => task.status === "DONE");
|
|
163
|
+
}
|
|
164
|
+
async function isAlreadyArchived(archivePath, taskDirName) {
|
|
165
|
+
const targetPath = join3(archivePath, taskDirName);
|
|
166
|
+
return await getFsEntryKind(targetPath) === "directory";
|
|
167
|
+
}
|
|
168
|
+
async function loadArchiveTargets(projectRoot, input) {
|
|
169
|
+
const tasks = await listWorkspaceTasks(projectRoot);
|
|
170
|
+
const doneTasks = filterArchiveCandidates(tasks);
|
|
171
|
+
const targets = [];
|
|
172
|
+
for (const task of doneTasks) {
|
|
173
|
+
const metaPath = join3(projectRoot, task.path, "meta.yml");
|
|
174
|
+
const metaContent = await readFile2(metaPath, "utf8");
|
|
175
|
+
const completedAt = extractCompletedAt(metaContent);
|
|
176
|
+
if (completedAt === null) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
targets.push({
|
|
180
|
+
completedAt,
|
|
181
|
+
id: task.id,
|
|
182
|
+
month: deriveArchiveMonth(completedAt),
|
|
183
|
+
path: task.path,
|
|
184
|
+
slug: task.slug
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
targets.sort((a, b) => a.completedAt.localeCompare(b.completedAt));
|
|
188
|
+
const keep = input.keep ?? 0;
|
|
189
|
+
if (keep > 0 && targets.length > keep) {
|
|
190
|
+
return targets.slice(0, targets.length - keep);
|
|
191
|
+
}
|
|
192
|
+
return targets;
|
|
193
|
+
}
|
|
194
|
+
async function runArchiveWorkflow(projectRoot, targets) {
|
|
195
|
+
const archiveRoot = getProjectSduckArchivePath(projectRoot);
|
|
196
|
+
const archived = [];
|
|
197
|
+
const skipped = [];
|
|
198
|
+
for (const target of targets) {
|
|
199
|
+
const monthDir = join3(archiveRoot, target.month);
|
|
200
|
+
await ensureDirectory(monthDir);
|
|
201
|
+
const segments = target.path.split("/");
|
|
202
|
+
const taskDirName = segments.at(-1) ?? target.id;
|
|
203
|
+
if (await isAlreadyArchived(monthDir, taskDirName)) {
|
|
204
|
+
skipped.push({ reason: "already archived", taskId: target.id });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const sourcePath = join3(projectRoot, target.path);
|
|
208
|
+
const destPath = join3(monthDir, taskDirName);
|
|
209
|
+
await rename(sourcePath, destPath);
|
|
210
|
+
archived.push({ month: target.month, taskId: target.id });
|
|
211
|
+
}
|
|
212
|
+
return { archived, skipped };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/commands/archive.ts
|
|
216
|
+
function padCell(value, width) {
|
|
217
|
+
return value.padEnd(width, " ");
|
|
218
|
+
}
|
|
219
|
+
function buildResultTable(result) {
|
|
220
|
+
const rows = [
|
|
221
|
+
...result.archived.map((row) => ({
|
|
222
|
+
month: row.month,
|
|
223
|
+
result: "archived",
|
|
224
|
+
task: row.taskId
|
|
225
|
+
})),
|
|
226
|
+
...result.skipped.map((row) => ({
|
|
227
|
+
month: "",
|
|
228
|
+
result: "skipped",
|
|
229
|
+
task: row.taskId
|
|
230
|
+
}))
|
|
231
|
+
];
|
|
232
|
+
const resultWidth = Math.max("Result".length, ...rows.map((row) => row.result.length));
|
|
233
|
+
const taskWidth = Math.max("Task".length, ...rows.map((row) => row.task.length));
|
|
234
|
+
const monthWidth = Math.max("Month".length, ...rows.map((row) => row.month.length));
|
|
235
|
+
const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(monthWidth)}-+`;
|
|
236
|
+
const header = `| ${padCell("Result", resultWidth)} | ${padCell("Task", taskWidth)} | ${padCell("Month", monthWidth)} |`;
|
|
237
|
+
const body = rows.map(
|
|
238
|
+
(row) => `| ${padCell(row.result, resultWidth)} | ${padCell(row.task, taskWidth)} | ${padCell(row.month, monthWidth)} |`
|
|
239
|
+
);
|
|
240
|
+
return [border, header, border, ...body, border].join("\n");
|
|
241
|
+
}
|
|
242
|
+
async function runArchiveCommand(input, projectRoot) {
|
|
243
|
+
try {
|
|
244
|
+
const targets = await loadArchiveTargets(projectRoot, input);
|
|
245
|
+
if (targets.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
exitCode: 0,
|
|
248
|
+
stderr: "",
|
|
249
|
+
stdout: "\uC544\uCE74\uC774\uBE0C \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const result = await runArchiveWorkflow(projectRoot, targets);
|
|
253
|
+
return {
|
|
254
|
+
exitCode: 0,
|
|
255
|
+
stderr: "",
|
|
256
|
+
stdout: buildResultTable(result)
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
exitCode: 1,
|
|
261
|
+
stderr: error instanceof Error ? error.message : "Unknown archive failure.",
|
|
262
|
+
stdout: ""
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/core/done.ts
|
|
268
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
269
|
+
import { join as join4 } from "path";
|
|
270
|
+
|
|
144
271
|
// src/utils/utc-date.ts
|
|
145
272
|
function pad2(value) {
|
|
146
273
|
return String(value).padStart(2, "0");
|
|
@@ -243,11 +370,11 @@ function validateDoneTarget(task) {
|
|
|
243
370
|
}
|
|
244
371
|
}
|
|
245
372
|
async function loadTaskEvalCriteria(projectRoot) {
|
|
246
|
-
const taskEvalPath =
|
|
373
|
+
const taskEvalPath = join4(projectRoot, TASK_EVAL_ASSET_PATH);
|
|
247
374
|
if (await getFsEntryKind(taskEvalPath) !== "file") {
|
|
248
375
|
throw new Error(`Missing task evaluation asset at ${TASK_EVAL_ASSET_PATH}.`);
|
|
249
376
|
}
|
|
250
|
-
const taskEvalContent = await
|
|
377
|
+
const taskEvalContent = await readFile3(taskEvalPath, "utf8");
|
|
251
378
|
const labels = extractTaskEvalCriteriaLabels(taskEvalContent);
|
|
252
379
|
if (labels.length === 0) {
|
|
253
380
|
throw new Error(`Task evaluation asset has no criteria labels: ${TASK_EVAL_ASSET_PATH}.`);
|
|
@@ -256,17 +383,17 @@ async function loadTaskEvalCriteria(projectRoot) {
|
|
|
256
383
|
}
|
|
257
384
|
async function completeTask(projectRoot, task, completedAt, taskEvalCriteria) {
|
|
258
385
|
validateDoneTarget(task);
|
|
259
|
-
const metaPath =
|
|
260
|
-
const specPath =
|
|
386
|
+
const metaPath = join4(projectRoot, task.path, "meta.yml");
|
|
387
|
+
const specPath = join4(projectRoot, task.path, "spec.md");
|
|
261
388
|
if (await getFsEntryKind(metaPath) !== "file") {
|
|
262
389
|
throw new Error(`Missing meta.yml for task ${task.id}.`);
|
|
263
390
|
}
|
|
264
391
|
if (await getFsEntryKind(specPath) !== "file") {
|
|
265
392
|
throw new Error(`Missing spec.md for task ${task.id}.`);
|
|
266
393
|
}
|
|
267
|
-
const metaContent = await
|
|
394
|
+
const metaContent = await readFile3(metaPath, "utf8");
|
|
268
395
|
validateDoneMetaContent(metaContent);
|
|
269
|
-
const specContent = await
|
|
396
|
+
const specContent = await readFile3(specPath, "utf8");
|
|
270
397
|
const uncheckedItems = extractUncheckedChecklistItems(specContent);
|
|
271
398
|
if (uncheckedItems.length > 0) {
|
|
272
399
|
throw new Error(`Spec checklist is incomplete: ${uncheckedItems.join("; ")}`);
|
|
@@ -332,10 +459,10 @@ function createTaskCompletedAt(date = /* @__PURE__ */ new Date()) {
|
|
|
332
459
|
}
|
|
333
460
|
|
|
334
461
|
// src/commands/done.ts
|
|
335
|
-
function
|
|
462
|
+
function padCell2(value, width) {
|
|
336
463
|
return value.padEnd(width, " ");
|
|
337
464
|
}
|
|
338
|
-
function
|
|
465
|
+
function buildResultTable2(result) {
|
|
339
466
|
const rows = [
|
|
340
467
|
...result.succeeded.map((row) => ({
|
|
341
468
|
note: row.note,
|
|
@@ -352,9 +479,9 @@ function buildResultTable(result) {
|
|
|
352
479
|
const taskWidth = Math.max("Task".length, ...rows.map((row) => row.task.length));
|
|
353
480
|
const noteWidth = Math.max("Note".length, ...rows.map((row) => row.note.length));
|
|
354
481
|
const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(noteWidth)}-+`;
|
|
355
|
-
const header = `| ${
|
|
482
|
+
const header = `| ${padCell2("Result", resultWidth)} | ${padCell2("Task", taskWidth)} | ${padCell2("Note", noteWidth)} |`;
|
|
356
483
|
const body = rows.map(
|
|
357
|
-
(row) => `| ${
|
|
484
|
+
(row) => `| ${padCell2(row.result, resultWidth)} | ${padCell2(row.task, taskWidth)} | ${padCell2(row.note, noteWidth)} |`
|
|
358
485
|
);
|
|
359
486
|
return [border, header, border, ...body, border].join("\n");
|
|
360
487
|
}
|
|
@@ -372,7 +499,7 @@ function formatFailureDetails(failed) {
|
|
|
372
499
|
return lines;
|
|
373
500
|
}
|
|
374
501
|
function formatSuccess(result) {
|
|
375
|
-
const lines = [
|
|
502
|
+
const lines = [buildResultTable2(result)];
|
|
376
503
|
if (result.succeeded.length > 0) {
|
|
377
504
|
const criteriaLabels = result.succeeded[0]?.taskEvalCriteria ?? [];
|
|
378
505
|
lines.push("", "\uC0C1\uD0DC: DONE");
|
|
@@ -413,10 +540,10 @@ import { confirm } from "@inquirer/prompts";
|
|
|
413
540
|
|
|
414
541
|
// src/core/fast-track.ts
|
|
415
542
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
416
|
-
import { join as
|
|
543
|
+
import { join as join9 } from "path";
|
|
417
544
|
|
|
418
545
|
// src/core/assets.ts
|
|
419
|
-
import { dirname, join as
|
|
546
|
+
import { dirname, join as join5 } from "path";
|
|
420
547
|
import { fileURLToPath } from "url";
|
|
421
548
|
var SUPPORTED_TASK_TYPES = [
|
|
422
549
|
"build",
|
|
@@ -426,16 +553,16 @@ var SUPPORTED_TASK_TYPES = [
|
|
|
426
553
|
"chore"
|
|
427
554
|
];
|
|
428
555
|
var EVAL_ASSET_RELATIVE_PATHS = {
|
|
429
|
-
task:
|
|
430
|
-
plan:
|
|
431
|
-
spec:
|
|
556
|
+
task: join5("eval", "task.yml"),
|
|
557
|
+
plan: join5("eval", "plan.yml"),
|
|
558
|
+
spec: join5("eval", "spec.yml")
|
|
432
559
|
};
|
|
433
560
|
var SPEC_TEMPLATE_RELATIVE_PATHS = {
|
|
434
|
-
build:
|
|
435
|
-
feature:
|
|
436
|
-
fix:
|
|
437
|
-
refactor:
|
|
438
|
-
chore:
|
|
561
|
+
build: join5("types", "build.md"),
|
|
562
|
+
feature: join5("types", "feature.md"),
|
|
563
|
+
fix: join5("types", "fix.md"),
|
|
564
|
+
refactor: join5("types", "refactor.md"),
|
|
565
|
+
chore: join5("types", "chore.md")
|
|
439
566
|
};
|
|
440
567
|
var INIT_ASSET_RELATIVE_PATHS = [
|
|
441
568
|
EVAL_ASSET_RELATIVE_PATHS.spec,
|
|
@@ -446,8 +573,8 @@ var INIT_ASSET_RELATIVE_PATHS = [
|
|
|
446
573
|
async function getBundledAssetsRoot() {
|
|
447
574
|
const currentDirectoryPath = dirname(fileURLToPath(import.meta.url));
|
|
448
575
|
const candidatePaths = [
|
|
449
|
-
|
|
450
|
-
|
|
576
|
+
join5(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets"),
|
|
577
|
+
join5(currentDirectoryPath, "..", ".sduck", "sduck-assets")
|
|
451
578
|
];
|
|
452
579
|
for (const candidatePath of candidatePaths) {
|
|
453
580
|
if (await getFsEntryKind(candidatePath) === "directory") {
|
|
@@ -464,8 +591,8 @@ function resolveSpecTemplateRelativePath(type) {
|
|
|
464
591
|
}
|
|
465
592
|
|
|
466
593
|
// src/core/plan-approve.ts
|
|
467
|
-
import { readFile as
|
|
468
|
-
import { join as
|
|
594
|
+
import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
595
|
+
import { join as join6 } from "path";
|
|
469
596
|
function filterPlanApprovalCandidates(tasks) {
|
|
470
597
|
return tasks.filter((task) => task.status === "SPEC_APPROVED");
|
|
471
598
|
}
|
|
@@ -507,8 +634,8 @@ async function approvePlans(projectRoot, tasks, approvedAt) {
|
|
|
507
634
|
});
|
|
508
635
|
continue;
|
|
509
636
|
}
|
|
510
|
-
const metaPath =
|
|
511
|
-
const planPath =
|
|
637
|
+
const metaPath = join6(projectRoot, task.path, "meta.yml");
|
|
638
|
+
const planPath = join6(projectRoot, task.path, "plan.md");
|
|
512
639
|
if (await getFsEntryKind(metaPath) !== "file") {
|
|
513
640
|
failed.push({ note: "missing meta.yml", taskId: task.id });
|
|
514
641
|
continue;
|
|
@@ -517,14 +644,14 @@ async function approvePlans(projectRoot, tasks, approvedAt) {
|
|
|
517
644
|
failed.push({ note: "missing plan.md", taskId: task.id });
|
|
518
645
|
continue;
|
|
519
646
|
}
|
|
520
|
-
const planContent = await
|
|
647
|
+
const planContent = await readFile4(planPath, "utf8");
|
|
521
648
|
const totalSteps = countPlanSteps(planContent);
|
|
522
649
|
if (totalSteps === 0) {
|
|
523
650
|
failed.push({ note: "missing valid Step headers", taskId: task.id });
|
|
524
651
|
continue;
|
|
525
652
|
}
|
|
526
653
|
const updatedMeta = updatePlanApprovalBlock(
|
|
527
|
-
await
|
|
654
|
+
await readFile4(metaPath, "utf8"),
|
|
528
655
|
approvedAt,
|
|
529
656
|
totalSteps
|
|
530
657
|
);
|
|
@@ -547,8 +674,8 @@ function createPlanApprovedAt(date = /* @__PURE__ */ new Date()) {
|
|
|
547
674
|
}
|
|
548
675
|
|
|
549
676
|
// src/core/spec-approve.ts
|
|
550
|
-
import { readFile as
|
|
551
|
-
import { join as
|
|
677
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
678
|
+
import { join as join7 } from "path";
|
|
552
679
|
function filterApprovalCandidates(tasks) {
|
|
553
680
|
return tasks.filter((task) => task.status === "PENDING_SPEC_APPROVAL");
|
|
554
681
|
}
|
|
@@ -583,11 +710,11 @@ function updateSpecApprovalBlock(metaContent, approvedAt) {
|
|
|
583
710
|
async function approveSpecs(projectRoot, tasks, approvedAt) {
|
|
584
711
|
validateSpecApprovalTargets(tasks);
|
|
585
712
|
for (const task of tasks) {
|
|
586
|
-
const metaPath =
|
|
713
|
+
const metaPath = join7(projectRoot, task.path, "meta.yml");
|
|
587
714
|
if (await getFsEntryKind(metaPath) !== "file") {
|
|
588
715
|
throw new Error(`Missing meta.yml for task ${task.id}.`);
|
|
589
716
|
}
|
|
590
|
-
const updatedContent = updateSpecApprovalBlock(await
|
|
717
|
+
const updatedContent = updateSpecApprovalBlock(await readFile5(metaPath, "utf8"), approvedAt);
|
|
591
718
|
await writeFile3(metaPath, updatedContent, "utf8");
|
|
592
719
|
}
|
|
593
720
|
return {
|
|
@@ -605,8 +732,8 @@ function createSpecApprovedAt(date = /* @__PURE__ */ new Date()) {
|
|
|
605
732
|
}
|
|
606
733
|
|
|
607
734
|
// src/core/start.ts
|
|
608
|
-
import { mkdir as mkdir2, readFile as
|
|
609
|
-
import { join as
|
|
735
|
+
import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
736
|
+
import { join as join8 } from "path";
|
|
610
737
|
function normalizeSlug(input) {
|
|
611
738
|
return input.trim().toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
612
739
|
}
|
|
@@ -653,7 +780,7 @@ function renderInitialMeta(input) {
|
|
|
653
780
|
}
|
|
654
781
|
async function resolveSpecTemplatePath(type) {
|
|
655
782
|
const assetsRoot = await getBundledAssetsRoot();
|
|
656
|
-
return
|
|
783
|
+
return join8(assetsRoot, resolveSpecTemplateRelativePath(type));
|
|
657
784
|
}
|
|
658
785
|
function applyTemplateDefaults(template, type, slug, currentDate) {
|
|
659
786
|
const displayName = slug.replace(/-/g, " ");
|
|
@@ -673,7 +800,7 @@ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE
|
|
|
673
800
|
}
|
|
674
801
|
const workspaceId = createWorkspaceId(currentDate, rawType, slug);
|
|
675
802
|
const workspacePath = getProjectRelativeSduckWorkspacePath(workspaceId);
|
|
676
|
-
const absoluteWorkspacePath =
|
|
803
|
+
const absoluteWorkspacePath = join8(projectRoot, workspacePath);
|
|
677
804
|
if (await getFsEntryKind(absoluteWorkspacePath) !== "missing") {
|
|
678
805
|
throw new Error(`Workspace already exists: ${workspacePath}`);
|
|
679
806
|
}
|
|
@@ -684,7 +811,7 @@ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE
|
|
|
684
811
|
if (await getFsEntryKind(templatePath) !== "file") {
|
|
685
812
|
throw new Error(`Missing spec template for type '${rawType}' at ${templatePath}`);
|
|
686
813
|
}
|
|
687
|
-
const specTemplate = await
|
|
814
|
+
const specTemplate = await readFile6(templatePath, "utf8");
|
|
688
815
|
const specContent = applyTemplateDefaults(specTemplate, rawType, slug, currentDate);
|
|
689
816
|
const metaContent = renderInitialMeta({
|
|
690
817
|
createdAt: formatUtcTimestamp(currentDate),
|
|
@@ -692,9 +819,9 @@ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE
|
|
|
692
819
|
slug,
|
|
693
820
|
type: rawType
|
|
694
821
|
});
|
|
695
|
-
await writeFile4(
|
|
696
|
-
await writeFile4(
|
|
697
|
-
await writeFile4(
|
|
822
|
+
await writeFile4(join8(absoluteWorkspacePath, "meta.yml"), metaContent, "utf8");
|
|
823
|
+
await writeFile4(join8(absoluteWorkspacePath, "spec.md"), specContent, "utf8");
|
|
824
|
+
await writeFile4(join8(absoluteWorkspacePath, "plan.md"), "", "utf8");
|
|
698
825
|
return {
|
|
699
826
|
workspaceId,
|
|
700
827
|
workspacePath,
|
|
@@ -761,9 +888,9 @@ async function createFastTrackTask(input, projectRoot) {
|
|
|
761
888
|
throw new Error(`Unsupported type: ${input.type}`);
|
|
762
889
|
}
|
|
763
890
|
const startedTask = await startTask(input.type, input.slug, projectRoot);
|
|
764
|
-
const taskPath =
|
|
765
|
-
const specPath =
|
|
766
|
-
const planPath =
|
|
891
|
+
const taskPath = join9(projectRoot, startedTask.workspacePath);
|
|
892
|
+
const specPath = join9(taskPath, "spec.md");
|
|
893
|
+
const planPath = join9(taskPath, "plan.md");
|
|
767
894
|
if (await getFsEntryKind(specPath) !== "file") {
|
|
768
895
|
throw new Error(`Missing spec.md for fast-track task ${startedTask.workspaceId}.`);
|
|
769
896
|
}
|
|
@@ -900,9 +1027,15 @@ async function runFastTrackCommand(input, projectRoot) {
|
|
|
900
1027
|
import { checkbox } from "@inquirer/prompts";
|
|
901
1028
|
|
|
902
1029
|
// src/core/agent-rules.ts
|
|
903
|
-
import { readFile as
|
|
904
|
-
import { dirname as dirname2, join as
|
|
1030
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1031
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
905
1032
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1033
|
+
var CLAUDE_CODE_HOOK_SETTINGS_PATH = join10(".claude", "settings.json");
|
|
1034
|
+
var CLAUDE_CODE_HOOK_SCRIPT_PATH = join10(".claude", "hooks", "sdd-guard.sh");
|
|
1035
|
+
var CLAUDE_CODE_HOOK_SOURCE_PATH = join10("hooks", "sdd-guard.sh");
|
|
1036
|
+
function needsClaudeCodeHook(agents) {
|
|
1037
|
+
return agents.includes("claude-code");
|
|
1038
|
+
}
|
|
906
1039
|
var SDD_RULES_BEGIN = "<!-- sduck:begin -->";
|
|
907
1040
|
var SDD_RULES_END = "<!-- sduck:end -->";
|
|
908
1041
|
var SUPPORTED_AGENTS = [
|
|
@@ -920,12 +1053,12 @@ var AGENT_RULE_TARGETS = [
|
|
|
920
1053
|
{ agentId: "gemini-cli", outputPath: "GEMINI.md", kind: "root-file" },
|
|
921
1054
|
{
|
|
922
1055
|
agentId: "cursor",
|
|
923
|
-
outputPath:
|
|
1056
|
+
outputPath: join10(".cursor", "rules", "sduck-core.mdc"),
|
|
924
1057
|
kind: "managed-file"
|
|
925
1058
|
},
|
|
926
1059
|
{
|
|
927
1060
|
agentId: "antigravity",
|
|
928
|
-
outputPath:
|
|
1061
|
+
outputPath: join10(".agents", "rules", "sduck-core.md"),
|
|
929
1062
|
kind: "managed-file"
|
|
930
1063
|
}
|
|
931
1064
|
];
|
|
@@ -986,8 +1119,8 @@ function renderManagedBlock(lines) {
|
|
|
986
1119
|
async function getAgentRulesAssetRoot() {
|
|
987
1120
|
const currentDirectoryPath = dirname2(fileURLToPath2(import.meta.url));
|
|
988
1121
|
const candidatePaths = [
|
|
989
|
-
|
|
990
|
-
|
|
1122
|
+
join10(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets", "agent-rules"),
|
|
1123
|
+
join10(currentDirectoryPath, "..", ".sduck", "sduck-assets", "agent-rules")
|
|
991
1124
|
];
|
|
992
1125
|
for (const candidatePath of candidatePaths) {
|
|
993
1126
|
if (await getFsEntryKind(candidatePath) === "directory") {
|
|
@@ -997,7 +1130,7 @@ async function getAgentRulesAssetRoot() {
|
|
|
997
1130
|
throw new Error("Unable to locate bundled sduck agent rule assets.");
|
|
998
1131
|
}
|
|
999
1132
|
async function readAssetFile(assetRoot, fileName) {
|
|
1000
|
-
return await
|
|
1133
|
+
return await readFile7(join10(assetRoot, fileName), "utf8");
|
|
1001
1134
|
}
|
|
1002
1135
|
function buildRootFileLines(agentIds, agentSpecificContent) {
|
|
1003
1136
|
const labels = SUPPORTED_AGENTS.filter((agent) => agentIds.includes(agent.id)).map(
|
|
@@ -1059,8 +1192,8 @@ function planAgentRuleActions(mode, targets, existingEntries, existingContents)
|
|
|
1059
1192
|
}
|
|
1060
1193
|
|
|
1061
1194
|
// src/core/init.ts
|
|
1062
|
-
import { mkdir as mkdir3, readFile as
|
|
1063
|
-
import { dirname as dirname3, join as
|
|
1195
|
+
import { chmod, mkdir as mkdir3, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
1196
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
1064
1197
|
var ASSET_TEMPLATE_DEFINITIONS = [
|
|
1065
1198
|
{
|
|
1066
1199
|
key: "eval-spec",
|
|
@@ -1173,7 +1306,7 @@ async function collectExistingEntries(projectRoot) {
|
|
|
1173
1306
|
for (const definition of ASSET_TEMPLATE_DEFINITIONS) {
|
|
1174
1307
|
existingEntries.set(
|
|
1175
1308
|
definition.relativePath,
|
|
1176
|
-
await getFsEntryKind(
|
|
1309
|
+
await getFsEntryKind(join11(projectRoot, definition.relativePath))
|
|
1177
1310
|
);
|
|
1178
1311
|
}
|
|
1179
1312
|
return existingEntries;
|
|
@@ -1181,9 +1314,9 @@ async function collectExistingEntries(projectRoot) {
|
|
|
1181
1314
|
async function collectExistingFileContents(projectRoot, targets) {
|
|
1182
1315
|
const contents = /* @__PURE__ */ new Map();
|
|
1183
1316
|
for (const target of targets) {
|
|
1184
|
-
const targetPath =
|
|
1317
|
+
const targetPath = join11(projectRoot, target.outputPath);
|
|
1185
1318
|
if (await getFsEntryKind(targetPath) === "file") {
|
|
1186
|
-
contents.set(target.outputPath, await
|
|
1319
|
+
contents.set(target.outputPath, await readFile8(targetPath, "utf8"));
|
|
1187
1320
|
}
|
|
1188
1321
|
}
|
|
1189
1322
|
return contents;
|
|
@@ -1246,8 +1379,8 @@ async function initProject(options, projectRoot) {
|
|
|
1246
1379
|
continue;
|
|
1247
1380
|
}
|
|
1248
1381
|
const definition = ASSET_TEMPLATE_MAP[action.key];
|
|
1249
|
-
const sourcePath =
|
|
1250
|
-
const targetPath =
|
|
1382
|
+
const sourcePath = join11(assetSourceRoot, toBundledAssetRelativePath(definition.relativePath));
|
|
1383
|
+
const targetPath = join11(projectRoot, definition.relativePath);
|
|
1251
1384
|
await ensureReadableFile(sourcePath);
|
|
1252
1385
|
await mkdir3(dirname3(targetPath), { recursive: true });
|
|
1253
1386
|
await copyFileIntoPlace(sourcePath, targetPath);
|
|
@@ -1257,7 +1390,7 @@ async function initProject(options, projectRoot) {
|
|
|
1257
1390
|
for (const target of agentTargets) {
|
|
1258
1391
|
agentEntryKinds.set(
|
|
1259
1392
|
target.outputPath,
|
|
1260
|
-
await getFsEntryKind(
|
|
1393
|
+
await getFsEntryKind(join11(projectRoot, target.outputPath))
|
|
1261
1394
|
);
|
|
1262
1395
|
}
|
|
1263
1396
|
const existingContents = await collectExistingFileContents(projectRoot, agentTargets);
|
|
@@ -1269,6 +1402,9 @@ async function initProject(options, projectRoot) {
|
|
|
1269
1402
|
summary,
|
|
1270
1403
|
resolvedOptions.agents
|
|
1271
1404
|
);
|
|
1405
|
+
if (needsClaudeCodeHook(resolvedOptions.agents)) {
|
|
1406
|
+
await installClaudeCodeHook(projectRoot, summary);
|
|
1407
|
+
}
|
|
1272
1408
|
return {
|
|
1273
1409
|
mode,
|
|
1274
1410
|
agents: resolvedOptions.agents,
|
|
@@ -1276,9 +1412,54 @@ async function initProject(options, projectRoot) {
|
|
|
1276
1412
|
didChange: summary.created.length > 0 || summary.prepended.length > 0 || summary.overwritten.length > 0
|
|
1277
1413
|
};
|
|
1278
1414
|
}
|
|
1415
|
+
async function installClaudeCodeHook(projectRoot, summary) {
|
|
1416
|
+
const assetRoot = await getBundledAssetsRoot();
|
|
1417
|
+
const hookSourcePath = join11(assetRoot, "agent-rules", CLAUDE_CODE_HOOK_SOURCE_PATH);
|
|
1418
|
+
const hookTargetPath = join11(projectRoot, CLAUDE_CODE_HOOK_SCRIPT_PATH);
|
|
1419
|
+
const settingsPath = join11(projectRoot, CLAUDE_CODE_HOOK_SETTINGS_PATH);
|
|
1420
|
+
await mkdir3(dirname3(hookTargetPath), { recursive: true });
|
|
1421
|
+
await copyFileIntoPlace(hookSourcePath, hookTargetPath);
|
|
1422
|
+
await chmod(hookTargetPath, 493);
|
|
1423
|
+
summary.created.push(CLAUDE_CODE_HOOK_SCRIPT_PATH);
|
|
1424
|
+
summary.rows.push({ path: CLAUDE_CODE_HOOK_SCRIPT_PATH, status: "created" });
|
|
1425
|
+
const hookConfig = {
|
|
1426
|
+
hooks: {
|
|
1427
|
+
PreToolUse: [
|
|
1428
|
+
{
|
|
1429
|
+
matcher: "Edit|Write",
|
|
1430
|
+
hooks: [
|
|
1431
|
+
{
|
|
1432
|
+
type: "command",
|
|
1433
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/sdd-guard.sh'
|
|
1434
|
+
}
|
|
1435
|
+
]
|
|
1436
|
+
}
|
|
1437
|
+
]
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
if (await getFsEntryKind(settingsPath) === "file") {
|
|
1441
|
+
const existingContent = await readFile8(settingsPath, "utf8");
|
|
1442
|
+
try {
|
|
1443
|
+
const existing = JSON.parse(existingContent);
|
|
1444
|
+
existing["hooks"] = hookConfig.hooks;
|
|
1445
|
+
await writeFile6(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
1446
|
+
summary.overwritten.push(CLAUDE_CODE_HOOK_SETTINGS_PATH);
|
|
1447
|
+
summary.rows.push({ path: CLAUDE_CODE_HOOK_SETTINGS_PATH, status: "overwritten" });
|
|
1448
|
+
} catch {
|
|
1449
|
+
await writeFile6(settingsPath, JSON.stringify(hookConfig, null, 2) + "\n", "utf8");
|
|
1450
|
+
summary.overwritten.push(CLAUDE_CODE_HOOK_SETTINGS_PATH);
|
|
1451
|
+
summary.rows.push({ path: CLAUDE_CODE_HOOK_SETTINGS_PATH, status: "overwritten" });
|
|
1452
|
+
}
|
|
1453
|
+
} else {
|
|
1454
|
+
await mkdir3(dirname3(settingsPath), { recursive: true });
|
|
1455
|
+
await writeFile6(settingsPath, JSON.stringify(hookConfig, null, 2) + "\n", "utf8");
|
|
1456
|
+
summary.created.push(CLAUDE_CODE_HOOK_SETTINGS_PATH);
|
|
1457
|
+
summary.rows.push({ path: CLAUDE_CODE_HOOK_SETTINGS_PATH, status: "created" });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1279
1460
|
async function applyAgentRuleActions(projectRoot, actions, existingContents, summary, selectedAgents) {
|
|
1280
1461
|
for (const action of actions) {
|
|
1281
|
-
const targetPath =
|
|
1462
|
+
const targetPath = join11(projectRoot, action.outputPath);
|
|
1282
1463
|
const content = await renderAgentRuleContent(action, selectedAgents);
|
|
1283
1464
|
if (action.mergeMode === "create") {
|
|
1284
1465
|
await mkdir3(dirname3(targetPath), { recursive: true });
|
|
@@ -1323,16 +1504,16 @@ async function applyAgentRuleActions(projectRoot, actions, existingContents, sum
|
|
|
1323
1504
|
var AGENT_PROMPT_MESSAGE = "Select AI agents to generate repository rule files for";
|
|
1324
1505
|
var AGENT_PROMPT_INSTRUCTIONS = "Use space to toggle agents, arrow keys to move, and enter to submit.";
|
|
1325
1506
|
var AGENT_PROMPT_REQUIRED_MESSAGE = "Select at least one agent. Use space to toggle and enter to submit.";
|
|
1326
|
-
function
|
|
1507
|
+
function padCell3(value, width) {
|
|
1327
1508
|
return value.padEnd(width, " ");
|
|
1328
1509
|
}
|
|
1329
1510
|
function buildSummaryTable(rows) {
|
|
1330
1511
|
const statusWidth = Math.max("Status".length, ...rows.map((row) => row.status.length));
|
|
1331
1512
|
const pathWidth = Math.max("Path".length, ...rows.map((row) => row.path.length));
|
|
1332
1513
|
const border = `+-${"-".repeat(statusWidth)}-+-${"-".repeat(pathWidth)}-+`;
|
|
1333
|
-
const header = `| ${
|
|
1514
|
+
const header = `| ${padCell3("Status", statusWidth)} | ${padCell3("Path", pathWidth)} |`;
|
|
1334
1515
|
const body = rows.map(
|
|
1335
|
-
(row) => `| ${
|
|
1516
|
+
(row) => `| ${padCell3(row.status, statusWidth)} | ${padCell3(row.path, pathWidth)} |`
|
|
1336
1517
|
);
|
|
1337
1518
|
return [border, header, border, ...body, border].join("\n");
|
|
1338
1519
|
}
|
|
@@ -1399,10 +1580,10 @@ async function runInitCommand(options, projectRoot) {
|
|
|
1399
1580
|
|
|
1400
1581
|
// src/commands/plan-approve.ts
|
|
1401
1582
|
import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
1402
|
-
function
|
|
1583
|
+
function padCell4(value, width) {
|
|
1403
1584
|
return value.padEnd(width, " ");
|
|
1404
1585
|
}
|
|
1405
|
-
function
|
|
1586
|
+
function buildResultTable3(result) {
|
|
1406
1587
|
const rows = [
|
|
1407
1588
|
...result.succeeded.map((row) => ({
|
|
1408
1589
|
note: row.note,
|
|
@@ -1422,9 +1603,9 @@ function buildResultTable2(result) {
|
|
|
1422
1603
|
const stepsWidth = Math.max("Steps".length, ...rows.map((row) => row.steps.length));
|
|
1423
1604
|
const noteWidth = Math.max("Note".length, ...rows.map((row) => row.note.length));
|
|
1424
1605
|
const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(stepsWidth)}-+-${"-".repeat(noteWidth)}-+`;
|
|
1425
|
-
const header = `| ${
|
|
1606
|
+
const header = `| ${padCell4("Result", resultWidth)} | ${padCell4("Task", taskWidth)} | ${padCell4("Steps", stepsWidth)} | ${padCell4("Note", noteWidth)} |`;
|
|
1426
1607
|
const body = rows.map(
|
|
1427
|
-
(row) => `| ${
|
|
1608
|
+
(row) => `| ${padCell4(row.result, resultWidth)} | ${padCell4(row.task, taskWidth)} | ${padCell4(row.steps, stepsWidth)} | ${padCell4(row.note, noteWidth)} |`
|
|
1428
1609
|
);
|
|
1429
1610
|
return [border, header, border, ...body, border].join("\n");
|
|
1430
1611
|
}
|
|
@@ -1432,7 +1613,7 @@ function formatTaskLabel(task) {
|
|
|
1432
1613
|
return `${task.id} (${task.status})`;
|
|
1433
1614
|
}
|
|
1434
1615
|
function formatSuccess2(result) {
|
|
1435
|
-
const lines = [
|
|
1616
|
+
const lines = [buildResultTable3(result)];
|
|
1436
1617
|
if (result.succeeded.length > 0) {
|
|
1437
1618
|
lines.push("", "\uC0C1\uD0DC: IN_PROGRESS \u2192 \uC791\uC5C5\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.");
|
|
1438
1619
|
}
|
|
@@ -1467,7 +1648,7 @@ async function runPlanApproveCommand(input, projectRoot) {
|
|
|
1467
1648
|
if (result.succeeded.length === 0) {
|
|
1468
1649
|
return {
|
|
1469
1650
|
exitCode: 1,
|
|
1470
|
-
stderr:
|
|
1651
|
+
stderr: buildResultTable3(result),
|
|
1471
1652
|
stdout: ""
|
|
1472
1653
|
};
|
|
1473
1654
|
}
|
|
@@ -1567,7 +1748,7 @@ async function runStartCommand(type, slug, projectRoot) {
|
|
|
1567
1748
|
// package.json
|
|
1568
1749
|
var package_default = {
|
|
1569
1750
|
name: "@sduck/sduck-cli",
|
|
1570
|
-
version: "0.1.
|
|
1751
|
+
version: "0.1.5",
|
|
1571
1752
|
description: "Spec-Driven Development CLI bootstrap",
|
|
1572
1753
|
type: "module",
|
|
1573
1754
|
bin: {
|
|
@@ -1719,6 +1900,19 @@ program.command("done [target]").description("Complete an in-progress task after
|
|
|
1719
1900
|
process.exitCode = result.exitCode;
|
|
1720
1901
|
}
|
|
1721
1902
|
});
|
|
1903
|
+
program.command("archive").description("Archive completed tasks into monthly directories").option("--keep <n>", "Keep the N most recently completed tasks in workspace", "0").action(async (options) => {
|
|
1904
|
+
const keep = Number(options.keep);
|
|
1905
|
+
const result = await runArchiveCommand({ keep }, process.cwd());
|
|
1906
|
+
if (result.stdout !== "") {
|
|
1907
|
+
console.log(result.stdout);
|
|
1908
|
+
}
|
|
1909
|
+
if (result.stderr !== "") {
|
|
1910
|
+
console.error(result.stderr);
|
|
1911
|
+
}
|
|
1912
|
+
if (result.exitCode !== 0) {
|
|
1913
|
+
process.exitCode = result.exitCode;
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1722
1916
|
program.command("roadmap").description("Show the current bootstrap status").action(() => {
|
|
1723
1917
|
console.log(PLACEHOLDER_MESSAGE);
|
|
1724
1918
|
});
|