@sduck/sduck-cli 0.1.0 → 0.1.2

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/dist/cli.js CHANGED
@@ -3,42 +3,906 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/commands/init.ts
7
- import { checkbox } from "@inquirer/prompts";
6
+ // src/core/done.ts
7
+ import { readFile as readFile2, writeFile } from "fs/promises";
8
+ import { join as join3 } from "path";
9
+
10
+ // src/core/fs.ts
11
+ import { constants } from "fs";
12
+ import { access, copyFile, mkdir, stat } from "fs/promises";
13
+ async function getFsEntryKind(targetPath) {
14
+ try {
15
+ const stats = await stat(targetPath);
16
+ if (stats.isDirectory()) {
17
+ return "directory";
18
+ }
19
+ if (stats.isFile()) {
20
+ return "file";
21
+ }
22
+ return "file";
23
+ } catch {
24
+ return "missing";
25
+ }
26
+ }
27
+ async function ensureDirectory(targetPath) {
28
+ await mkdir(targetPath, { recursive: true });
29
+ }
30
+ async function ensureReadableFile(targetPath) {
31
+ await access(targetPath, constants.R_OK);
32
+ }
33
+ async function copyFileIntoPlace(sourcePath, targetPath) {
34
+ await copyFile(sourcePath, targetPath);
35
+ }
36
+
37
+ // src/core/project-paths.ts
38
+ import { join, relative } from "path";
39
+ var SDUCK_HOME_DIR = ".sduck";
40
+ var SDUCK_ASSETS_DIR = "sduck-assets";
41
+ var SDUCK_WORKSPACE_DIR = "sduck-workspace";
42
+ var PROJECT_SDUCK_HOME_RELATIVE_PATH = SDUCK_HOME_DIR;
43
+ var PROJECT_SDUCK_ASSETS_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_ASSETS_DIR);
44
+ var PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_WORKSPACE_DIR);
45
+ function getProjectSduckHomePath(projectRoot) {
46
+ return join(projectRoot, PROJECT_SDUCK_HOME_RELATIVE_PATH);
47
+ }
48
+ function getProjectSduckAssetsPath(projectRoot) {
49
+ return join(projectRoot, PROJECT_SDUCK_ASSETS_RELATIVE_PATH);
50
+ }
51
+ function getProjectSduckWorkspacePath(projectRoot) {
52
+ return join(projectRoot, PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH);
53
+ }
54
+ function getProjectRelativeSduckAssetPath(...segments) {
55
+ return join(PROJECT_SDUCK_ASSETS_RELATIVE_PATH, ...segments);
56
+ }
57
+ function getProjectRelativeSduckWorkspacePath(...segments) {
58
+ return join(PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH, ...segments);
59
+ }
60
+ function toBundledAssetRelativePath(projectRelativeAssetPath) {
61
+ return relative(PROJECT_SDUCK_ASSETS_RELATIVE_PATH, projectRelativeAssetPath);
62
+ }
63
+
64
+ // src/core/workspace.ts
65
+ import { readFile } from "fs/promises";
66
+ import { join as join2 } from "path";
67
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "PENDING_SPEC_APPROVAL", "PENDING_PLAN_APPROVAL"]);
68
+ function parseMetaText(content) {
69
+ const createdAtMatch = /^created_at:\s+(.+)$/m.exec(content);
70
+ const idMatch = /^id:\s+(.+)$/m.exec(content);
71
+ const slugMatch = /^slug:\s+(.+)$/m.exec(content);
72
+ const statusMatch = /^status:\s+(.+)$/m.exec(content);
73
+ const parsedMeta = {};
74
+ if (createdAtMatch?.[1] !== void 0) {
75
+ parsedMeta.createdAt = createdAtMatch[1].trim();
76
+ }
77
+ if (idMatch?.[1] !== void 0) {
78
+ parsedMeta.id = idMatch[1].trim();
79
+ }
80
+ if (slugMatch?.[1] !== void 0) {
81
+ parsedMeta.slug = slugMatch[1].trim();
82
+ }
83
+ if (statusMatch?.[1] !== void 0) {
84
+ parsedMeta.status = statusMatch[1].trim();
85
+ }
86
+ return parsedMeta;
87
+ }
88
+ function sortTasksByRecency(tasks) {
89
+ return [...tasks].sort((left, right) => {
90
+ const leftValue = left.createdAt ?? "";
91
+ const rightValue = right.createdAt ?? "";
92
+ return rightValue.localeCompare(leftValue);
93
+ });
94
+ }
95
+ async function listWorkspaceTasks(projectRoot) {
96
+ const workspaceRoot = getProjectSduckWorkspacePath(projectRoot);
97
+ if (await getFsEntryKind(workspaceRoot) !== "directory") {
98
+ return [];
99
+ }
100
+ const { readdir } = await import("fs/promises");
101
+ const entries = await readdir(workspaceRoot, { withFileTypes: true });
102
+ const tasks = [];
103
+ for (const entry of entries) {
104
+ if (!entry.isDirectory()) {
105
+ continue;
106
+ }
107
+ const relativePath = getProjectRelativeSduckWorkspacePath(entry.name);
108
+ const metaPath = join2(projectRoot, relativePath, "meta.yml");
109
+ if (await getFsEntryKind(metaPath) !== "file") {
110
+ continue;
111
+ }
112
+ const parsedMeta = parseMetaText(await readFile(metaPath, "utf8"));
113
+ if (parsedMeta.id !== void 0 && parsedMeta.status !== void 0) {
114
+ const task = {
115
+ id: parsedMeta.id,
116
+ path: relativePath,
117
+ status: parsedMeta.status
118
+ };
119
+ if (parsedMeta.createdAt !== void 0) {
120
+ task.createdAt = parsedMeta.createdAt;
121
+ }
122
+ if (parsedMeta.slug !== void 0) {
123
+ task.slug = parsedMeta.slug;
124
+ }
125
+ tasks.push(task);
126
+ }
127
+ }
128
+ return sortTasksByRecency(tasks);
129
+ }
130
+ async function findActiveTask(projectRoot) {
131
+ const tasks = await listWorkspaceTasks(projectRoot);
132
+ for (const task of tasks) {
133
+ if (ACTIVE_STATUSES.has(task.status)) {
134
+ return {
135
+ id: task.id,
136
+ path: task.path,
137
+ status: task.status
138
+ };
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+
144
+ // src/utils/utc-date.ts
145
+ function pad2(value) {
146
+ return String(value).padStart(2, "0");
147
+ }
148
+ function formatUtcDate(date) {
149
+ const year = String(date.getUTCFullYear());
150
+ const month = pad2(date.getUTCMonth() + 1);
151
+ const day = pad2(date.getUTCDate());
152
+ return `${year}-${month}-${day}`;
153
+ }
154
+ function formatUtcTimestamp(date) {
155
+ const year = String(date.getUTCFullYear());
156
+ const month = pad2(date.getUTCMonth() + 1);
157
+ const day = pad2(date.getUTCDate());
158
+ const hour = pad2(date.getUTCHours());
159
+ const minute = pad2(date.getUTCMinutes());
160
+ const second = pad2(date.getUTCSeconds());
161
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}Z`;
162
+ }
163
+
164
+ // src/core/done.ts
165
+ var TASK_EVAL_ASSET_PATH = getProjectRelativeSduckAssetPath("eval", "task.yml");
166
+ function filterDoneCandidates(tasks) {
167
+ return tasks.filter((task) => task.status === "IN_PROGRESS");
168
+ }
169
+ function resolveDoneTargetMatches(tasks, target) {
170
+ if (target === void 0 || target.trim() === "") {
171
+ return filterDoneCandidates(tasks);
172
+ }
173
+ const trimmedTarget = target.trim();
174
+ return tasks.filter((task) => task.id === trimmedTarget || task.slug === trimmedTarget);
175
+ }
176
+ function extractUncheckedChecklistItems(specContent) {
177
+ const uncheckedMatches = specContent.matchAll(/^\s*- \[ \] (.+)$/gm);
178
+ return [...uncheckedMatches].map((match) => match[1]).filter((item) => item !== void 0).map((item) => item.trim());
179
+ }
180
+ function extractTaskEvalCriteriaLabels(taskEvalContent) {
181
+ const labels = [...taskEvalContent.matchAll(/^\s{6}label:\s+(.+)$/gm)].map((match) => match[1]).filter((item) => item !== void 0).map((item) => item.trim());
182
+ return [...new Set(labels)];
183
+ }
184
+ function parseCompletedStepNumbers(value) {
185
+ const trimmedValue = value.trim();
186
+ if (trimmedValue === "") {
187
+ return [];
188
+ }
189
+ return trimmedValue.split(",").map((segment) => {
190
+ const parsedValue = Number.parseInt(segment.trim(), 10);
191
+ if (!Number.isInteger(parsedValue)) {
192
+ throw new Error(`Invalid completed step value: ${segment.trim()}`);
193
+ }
194
+ return parsedValue;
195
+ });
196
+ }
197
+ function validateDoneMetaContent(metaContent) {
198
+ const totalMatch = /^ {2}total:\s+(.+)$/m.exec(metaContent);
199
+ const completedMatch = /^ {2}completed:\s+\[(.*)\]$/m.exec(metaContent);
200
+ if (totalMatch?.[1] === void 0 || completedMatch?.[1] === void 0) {
201
+ throw new Error("Task meta is missing a valid steps block.");
202
+ }
203
+ if (totalMatch[1].trim() === "null") {
204
+ throw new Error("Task steps are not initialized yet (steps.total is null).");
205
+ }
206
+ const totalSteps = Number.parseInt(totalMatch[1].trim(), 10);
207
+ if (!Number.isInteger(totalSteps) || totalSteps <= 0) {
208
+ throw new Error(`Task has an invalid steps.total value: ${totalMatch[1].trim()}`);
209
+ }
210
+ const completedSteps = parseCompletedStepNumbers(completedMatch[1]);
211
+ const uniqueSteps = new Set(completedSteps);
212
+ if (uniqueSteps.size !== completedSteps.length) {
213
+ throw new Error("Task has duplicate completed step numbers.");
214
+ }
215
+ const invalidStep = completedSteps.find((step) => step < 1 || step > totalSteps);
216
+ if (invalidStep !== void 0) {
217
+ throw new Error(`Task has an out-of-range completed step number: ${String(invalidStep)}`);
218
+ }
219
+ if (completedSteps.length !== totalSteps) {
220
+ const missingSteps = [];
221
+ for (let step = 1; step <= totalSteps; step += 1) {
222
+ if (!uniqueSteps.has(step)) {
223
+ missingSteps.push(step);
224
+ }
225
+ }
226
+ throw new Error(`Task steps are incomplete. Missing steps: ${missingSteps.join(", ")}`);
227
+ }
228
+ return {
229
+ completedSteps,
230
+ totalSteps
231
+ };
232
+ }
233
+ function updateDoneBlock(metaContent, completedAt) {
234
+ const withStatus = metaContent.replace(/^status:\s+.+$/m, "status: DONE");
235
+ return withStatus.replace(/^completed_at:\s+.+$/m, `completed_at: ${completedAt}`);
236
+ }
237
+ function validateDoneTarget(task) {
238
+ if (task.status === "DONE") {
239
+ throw new Error(`Task ${task.id} is already DONE.`);
240
+ }
241
+ if (task.status !== "IN_PROGRESS") {
242
+ throw new Error(`Task ${task.id} is not in progress (${task.status}).`);
243
+ }
244
+ }
245
+ async function loadTaskEvalCriteria(projectRoot) {
246
+ const taskEvalPath = join3(projectRoot, TASK_EVAL_ASSET_PATH);
247
+ if (await getFsEntryKind(taskEvalPath) !== "file") {
248
+ throw new Error(`Missing task evaluation asset at ${TASK_EVAL_ASSET_PATH}.`);
249
+ }
250
+ const taskEvalContent = await readFile2(taskEvalPath, "utf8");
251
+ const labels = extractTaskEvalCriteriaLabels(taskEvalContent);
252
+ if (labels.length === 0) {
253
+ throw new Error(`Task evaluation asset has no criteria labels: ${TASK_EVAL_ASSET_PATH}.`);
254
+ }
255
+ return labels;
256
+ }
257
+ async function completeTask(projectRoot, task, completedAt, taskEvalCriteria) {
258
+ validateDoneTarget(task);
259
+ const metaPath = join3(projectRoot, task.path, "meta.yml");
260
+ const specPath = join3(projectRoot, task.path, "spec.md");
261
+ if (await getFsEntryKind(metaPath) !== "file") {
262
+ throw new Error(`Missing meta.yml for task ${task.id}.`);
263
+ }
264
+ if (await getFsEntryKind(specPath) !== "file") {
265
+ throw new Error(`Missing spec.md for task ${task.id}.`);
266
+ }
267
+ const metaContent = await readFile2(metaPath, "utf8");
268
+ validateDoneMetaContent(metaContent);
269
+ const specContent = await readFile2(specPath, "utf8");
270
+ const uncheckedItems = extractUncheckedChecklistItems(specContent);
271
+ if (uncheckedItems.length > 0) {
272
+ throw new Error(`Spec checklist is incomplete: ${uncheckedItems.join("; ")}`);
273
+ }
274
+ await writeFile(metaPath, updateDoneBlock(metaContent, completedAt), "utf8");
275
+ return {
276
+ completedAt,
277
+ note: `task eval checked (${String(taskEvalCriteria.length)} criteria)`,
278
+ taskEvalCriteria: [...taskEvalCriteria],
279
+ taskId: task.id
280
+ };
281
+ }
282
+ async function runDoneWorkflow(projectRoot, tasks, completedAt) {
283
+ const taskEvalCriteria = await loadTaskEvalCriteria(projectRoot);
284
+ const succeeded = [];
285
+ const failed = [];
286
+ for (const task of tasks) {
287
+ try {
288
+ succeeded.push(await completeTask(projectRoot, task, completedAt, taskEvalCriteria));
289
+ } catch (error) {
290
+ const message = error instanceof Error ? error.message : "Unknown done failure.";
291
+ const pendingChecklistItems = message.startsWith("Spec checklist is incomplete: ") ? message.replace("Spec checklist is incomplete: ", "").split("; ").filter((item) => item !== "") : [];
292
+ failed.push({
293
+ note: message,
294
+ pendingChecklistItems,
295
+ taskId: task.id
296
+ });
297
+ }
298
+ }
299
+ return {
300
+ completedAt,
301
+ failed,
302
+ nextStatus: "DONE",
303
+ succeeded
304
+ };
305
+ }
306
+ async function loadDoneTargets(projectRoot, input) {
307
+ const tasks = await listWorkspaceTasks(projectRoot);
308
+ const matches = resolveDoneTargetMatches(tasks, input.target);
309
+ if (input.target === void 0 || input.target.trim() === "") {
310
+ if (matches.length === 0) {
311
+ throw new Error("No IN_PROGRESS task found. Run `sduck done <slug>` after choosing a task.");
312
+ }
313
+ if (matches.length > 1) {
314
+ const labels = matches.map((task) => task.slug ?? task.id).join(", ");
315
+ throw new Error(
316
+ `Multiple IN_PROGRESS tasks found: ${labels}. Rerun with \`sduck done <slug>\` or \`sduck done <id>\`.`
317
+ );
318
+ }
319
+ return matches;
320
+ }
321
+ if (matches.length === 0) {
322
+ throw new Error(`No task matches target '${input.target.trim()}'.`);
323
+ }
324
+ if (matches.length > 1) {
325
+ const ids = matches.map((task) => task.id).join(", ");
326
+ throw new Error(`Multiple tasks match '${input.target.trim()}': ${ids}. Use an exact task id.`);
327
+ }
328
+ return matches;
329
+ }
330
+ function createTaskCompletedAt(date = /* @__PURE__ */ new Date()) {
331
+ return formatUtcTimestamp(date);
332
+ }
333
+
334
+ // src/commands/done.ts
335
+ function padCell(value, width) {
336
+ return value.padEnd(width, " ");
337
+ }
338
+ function buildResultTable(result) {
339
+ const rows = [
340
+ ...result.succeeded.map((row) => ({
341
+ note: row.note,
342
+ result: "success",
343
+ task: row.taskId
344
+ })),
345
+ ...result.failed.map((row) => ({
346
+ note: row.note,
347
+ result: "failed",
348
+ task: row.taskId
349
+ }))
350
+ ];
351
+ const resultWidth = Math.max("Result".length, ...rows.map((row) => row.result.length));
352
+ const taskWidth = Math.max("Task".length, ...rows.map((row) => row.task.length));
353
+ const noteWidth = Math.max("Note".length, ...rows.map((row) => row.note.length));
354
+ const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(noteWidth)}-+`;
355
+ const header = `| ${padCell("Result", resultWidth)} | ${padCell("Task", taskWidth)} | ${padCell("Note", noteWidth)} |`;
356
+ const body = rows.map(
357
+ (row) => `| ${padCell(row.result, resultWidth)} | ${padCell(row.task, taskWidth)} | ${padCell(row.note, noteWidth)} |`
358
+ );
359
+ return [border, header, border, ...body, border].join("\n");
360
+ }
361
+ function formatFailureDetails(failed) {
362
+ const lines = [];
363
+ for (const row of failed) {
364
+ if (row.pendingChecklistItems.length === 0) {
365
+ continue;
366
+ }
367
+ lines.push("", `\uBBF8\uC644\uB8CC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 (${row.taskId})`);
368
+ for (const item of row.pendingChecklistItems) {
369
+ lines.push(`- [ ] ${item}`);
370
+ }
371
+ }
372
+ return lines;
373
+ }
374
+ function formatSuccess(result) {
375
+ const lines = [buildResultTable(result)];
376
+ if (result.succeeded.length > 0) {
377
+ const criteriaLabels = result.succeeded[0]?.taskEvalCriteria ?? [];
378
+ lines.push("", "\uC0C1\uD0DC: DONE");
379
+ if (criteriaLabels.length > 0) {
380
+ lines.push(`task eval \uAE30\uC900: ${criteriaLabels.join(", ")}`);
381
+ }
382
+ }
383
+ lines.push(...formatFailureDetails(result.failed));
384
+ return lines.join("\n");
385
+ }
386
+ async function runDoneCommand(input, projectRoot) {
387
+ try {
388
+ const tasks = await loadDoneTargets(projectRoot, input);
389
+ const result = await runDoneWorkflow(projectRoot, tasks, createTaskCompletedAt());
390
+ if (result.succeeded.length === 0) {
391
+ return {
392
+ exitCode: 1,
393
+ stderr: formatSuccess(result),
394
+ stdout: ""
395
+ };
396
+ }
397
+ return {
398
+ exitCode: result.failed.length > 0 ? 1 : 0,
399
+ stderr: "",
400
+ stdout: formatSuccess(result)
401
+ };
402
+ } catch (error) {
403
+ return {
404
+ exitCode: 1,
405
+ stderr: error instanceof Error ? error.message : "Unknown done failure.",
406
+ stdout: ""
407
+ };
408
+ }
409
+ }
410
+
411
+ // src/commands/fast-track.ts
412
+ import { confirm } from "@inquirer/prompts";
413
+
414
+ // src/core/fast-track.ts
415
+ import { writeFile as writeFile5 } from "fs/promises";
416
+ import { join as join8 } from "path";
417
+
418
+ // src/core/assets.ts
419
+ import { dirname, join as join4 } from "path";
420
+ import { fileURLToPath } from "url";
421
+ var SUPPORTED_TASK_TYPES = [
422
+ "build",
423
+ "feature",
424
+ "fix",
425
+ "refactor",
426
+ "chore"
427
+ ];
428
+ var EVAL_ASSET_RELATIVE_PATHS = {
429
+ task: join4("eval", "task.yml"),
430
+ plan: join4("eval", "plan.yml"),
431
+ spec: join4("eval", "spec.yml")
432
+ };
433
+ var SPEC_TEMPLATE_RELATIVE_PATHS = {
434
+ build: join4("types", "build.md"),
435
+ feature: join4("types", "feature.md"),
436
+ fix: join4("types", "fix.md"),
437
+ refactor: join4("types", "refactor.md"),
438
+ chore: join4("types", "chore.md")
439
+ };
440
+ var INIT_ASSET_RELATIVE_PATHS = [
441
+ EVAL_ASSET_RELATIVE_PATHS.spec,
442
+ EVAL_ASSET_RELATIVE_PATHS.plan,
443
+ EVAL_ASSET_RELATIVE_PATHS.task,
444
+ ...Object.values(SPEC_TEMPLATE_RELATIVE_PATHS)
445
+ ];
446
+ async function getBundledAssetsRoot() {
447
+ const currentDirectoryPath = dirname(fileURLToPath(import.meta.url));
448
+ const candidatePaths = [
449
+ join4(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets"),
450
+ join4(currentDirectoryPath, "..", ".sduck", "sduck-assets")
451
+ ];
452
+ for (const candidatePath of candidatePaths) {
453
+ if (await getFsEntryKind(candidatePath) === "directory") {
454
+ return candidatePath;
455
+ }
456
+ }
457
+ throw new Error("Unable to locate bundled .sduck/sduck-assets directory.");
458
+ }
459
+ function isSupportedTaskType(value) {
460
+ return SUPPORTED_TASK_TYPES.includes(value);
461
+ }
462
+ function resolveSpecTemplateRelativePath(type) {
463
+ return SPEC_TEMPLATE_RELATIVE_PATHS[type];
464
+ }
465
+
466
+ // src/core/plan-approve.ts
467
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
468
+ import { join as join5 } from "path";
469
+ function filterPlanApprovalCandidates(tasks) {
470
+ return tasks.filter((task) => task.status === "SPEC_APPROVED");
471
+ }
472
+ function resolvePlanApprovalCandidates(tasks, target) {
473
+ const candidates = filterPlanApprovalCandidates(tasks);
474
+ if (target === void 0 || target.trim() === "") {
475
+ return candidates;
476
+ }
477
+ const trimmedTarget = target.trim();
478
+ return candidates.filter((task) => task.id === trimmedTarget || task.slug === trimmedTarget);
479
+ }
480
+ function countPlanSteps(planContent) {
481
+ const matches = planContent.match(/^## Step \d+\. .+$/gm);
482
+ return matches?.length ?? 0;
483
+ }
484
+ function updatePlanApprovalBlock(metaContent, approvedAt, totalSteps) {
485
+ const withStatus = metaContent.replace(/^status:\s+.+$/m, "status: IN_PROGRESS");
486
+ const withPlan = withStatus.replace(
487
+ /plan:\n {2}approved:\s+false\n {2}approved_at:\s+null/m,
488
+ `plan:
489
+ approved: true
490
+ approved_at: ${approvedAt}`
491
+ );
492
+ return withPlan.replace(
493
+ /steps:\n {2}total:\s+null\n {2}completed:\s+\[\]/m,
494
+ `steps:
495
+ total: ${String(totalSteps)}
496
+ completed: []`
497
+ );
498
+ }
499
+ async function approvePlans(projectRoot, tasks, approvedAt) {
500
+ const succeeded = [];
501
+ const failed = [];
502
+ for (const task of tasks) {
503
+ if (task.status !== "SPEC_APPROVED") {
504
+ failed.push({
505
+ note: `task is not awaiting plan approval (${task.status})`,
506
+ taskId: task.id
507
+ });
508
+ continue;
509
+ }
510
+ const metaPath = join5(projectRoot, task.path, "meta.yml");
511
+ const planPath = join5(projectRoot, task.path, "plan.md");
512
+ if (await getFsEntryKind(metaPath) !== "file") {
513
+ failed.push({ note: "missing meta.yml", taskId: task.id });
514
+ continue;
515
+ }
516
+ if (await getFsEntryKind(planPath) !== "file") {
517
+ failed.push({ note: "missing plan.md", taskId: task.id });
518
+ continue;
519
+ }
520
+ const planContent = await readFile3(planPath, "utf8");
521
+ const totalSteps = countPlanSteps(planContent);
522
+ if (totalSteps === 0) {
523
+ failed.push({ note: "missing valid Step headers", taskId: task.id });
524
+ continue;
525
+ }
526
+ const updatedMeta = updatePlanApprovalBlock(
527
+ await readFile3(metaPath, "utf8"),
528
+ approvedAt,
529
+ totalSteps
530
+ );
531
+ await writeFile2(metaPath, updatedMeta, "utf8");
532
+ succeeded.push({ note: "moved to IN_PROGRESS", steps: totalSteps, taskId: task.id });
533
+ }
534
+ return {
535
+ approvedAt,
536
+ failed,
537
+ nextStatus: "IN_PROGRESS",
538
+ succeeded
539
+ };
540
+ }
541
+ async function loadPlanApprovalCandidates(projectRoot, input) {
542
+ const tasks = await listWorkspaceTasks(projectRoot);
543
+ return resolvePlanApprovalCandidates(tasks, input.target);
544
+ }
545
+ function createPlanApprovedAt(date = /* @__PURE__ */ new Date()) {
546
+ return formatUtcTimestamp(date);
547
+ }
548
+
549
+ // src/core/spec-approve.ts
550
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
551
+ import { join as join6 } from "path";
552
+ function filterApprovalCandidates(tasks) {
553
+ return tasks.filter((task) => task.status === "PENDING_SPEC_APPROVAL");
554
+ }
555
+ function resolveTargetCandidates(tasks, target) {
556
+ const candidates = filterApprovalCandidates(tasks);
557
+ if (target === void 0 || target.trim() === "") {
558
+ return candidates;
559
+ }
560
+ const trimmedTarget = target.trim();
561
+ return candidates.filter((task) => task.id === trimmedTarget || task.slug === trimmedTarget);
562
+ }
563
+ function validateSpecApprovalTargets(tasks) {
564
+ if (tasks.length === 0) {
565
+ throw new Error("No approvable spec tasks found.");
566
+ }
567
+ const invalidTask = tasks.find((task) => task.status !== "PENDING_SPEC_APPROVAL");
568
+ if (invalidTask !== void 0) {
569
+ throw new Error(
570
+ `Task ${invalidTask.id} is not awaiting spec approval (${invalidTask.status}).`
571
+ );
572
+ }
573
+ }
574
+ function updateSpecApprovalBlock(metaContent, approvedAt) {
575
+ const withStatus = metaContent.replace(/^status:\s+.+$/m, "status: SPEC_APPROVED");
576
+ return withStatus.replace(
577
+ /spec:\n {2}approved:\s+false\n {2}approved_at:\s+null/m,
578
+ `spec:
579
+ approved: true
580
+ approved_at: ${approvedAt}`
581
+ );
582
+ }
583
+ async function approveSpecs(projectRoot, tasks, approvedAt) {
584
+ validateSpecApprovalTargets(tasks);
585
+ for (const task of tasks) {
586
+ const metaPath = join6(projectRoot, task.path, "meta.yml");
587
+ if (await getFsEntryKind(metaPath) !== "file") {
588
+ throw new Error(`Missing meta.yml for task ${task.id}.`);
589
+ }
590
+ const updatedContent = updateSpecApprovalBlock(await readFile4(metaPath, "utf8"), approvedAt);
591
+ await writeFile3(metaPath, updatedContent, "utf8");
592
+ }
593
+ return {
594
+ approvedAt,
595
+ approvedTaskIds: tasks.map((task) => task.id),
596
+ nextStatus: "SPEC_APPROVED"
597
+ };
598
+ }
599
+ async function loadSpecApprovalCandidates(projectRoot, input) {
600
+ const tasks = await listWorkspaceTasks(projectRoot);
601
+ return resolveTargetCandidates(tasks, input.target);
602
+ }
603
+ function createSpecApprovedAt(date = /* @__PURE__ */ new Date()) {
604
+ return formatUtcTimestamp(date);
605
+ }
8
606
 
9
- // src/core/agent-rules.ts
10
- import { readFile } from "fs/promises";
11
- import { dirname, join } from "path";
12
- import { fileURLToPath } from "url";
607
+ // src/core/start.ts
608
+ import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
609
+ import { join as join7 } from "path";
610
+ function normalizeSlug(input) {
611
+ return input.trim().toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
612
+ }
613
+ function validateSlug(slug) {
614
+ if (slug === "") {
615
+ throw new Error("Invalid slug: slug cannot be empty after normalization.");
616
+ }
617
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
618
+ throw new Error("Invalid slug: use lowercase kebab-case only.");
619
+ }
620
+ }
621
+ function createWorkspaceId(date, type, slug) {
622
+ const year = String(date.getUTCFullYear());
623
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
624
+ const day = String(date.getUTCDate()).padStart(2, "0");
625
+ const hour = String(date.getUTCHours()).padStart(2, "0");
626
+ const minute = String(date.getUTCMinutes()).padStart(2, "0");
627
+ return `${year}${month}${day}-${hour}${minute}-${type}-${slug}`;
628
+ }
629
+ function renderInitialMeta(input) {
630
+ return [
631
+ `id: ${input.id}`,
632
+ `type: ${input.type}`,
633
+ `slug: ${input.slug}`,
634
+ `created_at: ${input.createdAt}`,
635
+ "",
636
+ "status: PENDING_SPEC_APPROVAL",
637
+ "",
638
+ "spec:",
639
+ " approved: false",
640
+ " approved_at: null",
641
+ "",
642
+ "plan:",
643
+ " approved: false",
644
+ " approved_at: null",
645
+ "",
646
+ "steps:",
647
+ " total: null",
648
+ " completed: []",
649
+ "",
650
+ "completed_at: null",
651
+ ""
652
+ ].join("\n");
653
+ }
654
+ async function resolveSpecTemplatePath(type) {
655
+ const assetsRoot = await getBundledAssetsRoot();
656
+ return join7(assetsRoot, resolveSpecTemplateRelativePath(type));
657
+ }
658
+ function applyTemplateDefaults(template, type, slug, currentDate) {
659
+ const displayName = slug.replace(/-/g, " ");
660
+ return template.replace(/\{기능명\}/g, displayName).replace(/\{버그 요약 한 줄\}/g, displayName).replace(/YYYY-MM-DD/g, formatUtcDate(currentDate)).replace(/> \*\*작성자:\*\*\s*$/m, "> **\uC791\uC131\uC790:** taehee").replace(/> \*\*연관 티켓:\*\*\s*$/m, "> **\uC5F0\uAD00 \uD2F0\uCF13:** -").replace(/^# \[(feature|fix|refactor|chore|build)\] .*/m, `# [${type}] ${displayName}`);
661
+ }
662
+ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE__ */ new Date()) {
663
+ if (!isSupportedTaskType(rawType)) {
664
+ throw new Error(`Unsupported type: ${rawType}`);
665
+ }
666
+ const slug = normalizeSlug(rawSlug);
667
+ validateSlug(slug);
668
+ const activeTask = await findActiveTask(projectRoot);
669
+ if (activeTask !== null) {
670
+ throw new Error(
671
+ `Active task exists: ${activeTask.id} (${activeTask.status}) at ${activeTask.path}. Finish or approve it before starting a new task.`
672
+ );
673
+ }
674
+ const workspaceId = createWorkspaceId(currentDate, rawType, slug);
675
+ const workspacePath = getProjectRelativeSduckWorkspacePath(workspaceId);
676
+ const absoluteWorkspacePath = join7(projectRoot, workspacePath);
677
+ if (await getFsEntryKind(absoluteWorkspacePath) !== "missing") {
678
+ throw new Error(`Workspace already exists: ${workspacePath}`);
679
+ }
680
+ const workspaceRoot = getProjectSduckWorkspacePath(projectRoot);
681
+ await mkdir2(workspaceRoot, { recursive: true });
682
+ await mkdir2(absoluteWorkspacePath, { recursive: false });
683
+ const templatePath = await resolveSpecTemplatePath(rawType);
684
+ if (await getFsEntryKind(templatePath) !== "file") {
685
+ throw new Error(`Missing spec template for type '${rawType}' at ${templatePath}`);
686
+ }
687
+ const specTemplate = await readFile5(templatePath, "utf8");
688
+ const specContent = applyTemplateDefaults(specTemplate, rawType, slug, currentDate);
689
+ const metaContent = renderInitialMeta({
690
+ createdAt: formatUtcTimestamp(currentDate),
691
+ id: workspaceId,
692
+ slug,
693
+ type: rawType
694
+ });
695
+ await writeFile4(join7(absoluteWorkspacePath, "meta.yml"), metaContent, "utf8");
696
+ await writeFile4(join7(absoluteWorkspacePath, "spec.md"), specContent, "utf8");
697
+ await writeFile4(join7(absoluteWorkspacePath, "plan.md"), "", "utf8");
698
+ return {
699
+ workspaceId,
700
+ workspacePath,
701
+ status: "PENDING_SPEC_APPROVAL"
702
+ };
703
+ }
13
704
 
14
- // src/core/fs.ts
15
- import { constants } from "fs";
16
- import { access, copyFile, mkdir, stat } from "fs/promises";
17
- async function getFsEntryKind(targetPath) {
18
- try {
19
- const stats = await stat(targetPath);
20
- if (stats.isDirectory()) {
21
- return "directory";
22
- }
23
- if (stats.isFile()) {
24
- return "file";
705
+ // src/core/fast-track.ts
706
+ function toSpecApprovalTarget(target) {
707
+ return target;
708
+ }
709
+ function toPlanApprovalTarget(target) {
710
+ return {
711
+ ...target,
712
+ status: "SPEC_APPROVED"
713
+ };
714
+ }
715
+ function renderMinimalSpec(type, slug) {
716
+ const displayName = slug.replace(/-/g, " ");
717
+ return [
718
+ `# [${type}] ${displayName}`,
719
+ "",
720
+ "## \uBAA9\uD45C",
721
+ "",
722
+ `- ${displayName} \uC791\uC5C5\uC744 \uBE60\uB974\uAC8C \uC2DC\uC791\uD560 \uC218 \uC788\uB294 \uCD5C\uC18C \uC2A4\uD399\uC744 \uC815\uC758\uD55C\uB2E4`,
723
+ "",
724
+ "## \uBC94\uC704",
725
+ "",
726
+ `- ${displayName} \uAD6C\uD604\uC5D0 \uD544\uC694\uD55C \uD575\uC2EC \uBCC0\uACBD\uB9CC \uD3EC\uD568\uD55C\uB2E4`,
727
+ "",
728
+ "## \uC81C\uC678 \uBC94\uC704",
729
+ "",
730
+ "- \uC694\uAD6C\uC0AC\uD56D\uACFC \uC9C1\uC811 \uAD00\uB828 \uC5C6\uB294 \uB9AC\uD329\uD130\uB9C1\uC740 \uD3EC\uD568\uD558\uC9C0 \uC54A\uB294\uB2E4",
731
+ "",
732
+ "## \uC644\uB8CC \uC870\uAC74",
733
+ "",
734
+ "- [ ] \uD575\uC2EC \uB3D9\uC791\uC774 \uAD6C\uD604\uB41C\uB2E4",
735
+ "- [ ] \uAD00\uB828 \uD14C\uC2A4\uD2B8\uAC00 \uD1B5\uACFC\uD55C\uB2E4",
736
+ "- [ ] \uBB38\uC11C \uB610\uB294 \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC601\uD5A5\uC774 \uBC18\uC601\uB41C\uB2E4",
737
+ ""
738
+ ].join("\n");
739
+ }
740
+ function renderMinimalPlan(slug) {
741
+ const displayName = slug.replace(/-/g, " ");
742
+ return [
743
+ "# Plan",
744
+ "",
745
+ `## Step 1. ${displayName} \uC694\uAD6C\uC0AC\uD56D \uBC18\uC601`,
746
+ "",
747
+ "- \uD575\uC2EC \uAD6C\uD604 \uD30C\uC77C\uC744 \uC218\uC815\uD574 \uC694\uAD6C\uC0AC\uD56D\uC744 \uBC18\uC601\uD55C\uB2E4.",
748
+ "",
749
+ "## Step 2. \uAC80\uC99D\uACFC \uB9C8\uBB34\uB9AC",
750
+ "",
751
+ "- \uAD00\uB828 \uD14C\uC2A4\uD2B8\uC640 \uBB38\uC11C\uB97C \uC5C5\uB370\uC774\uD2B8\uD55C\uB2E4.",
752
+ "- `npm run lint`, `npm run typecheck`, `npm test`, `npm run build`\uB85C \uAC80\uC99D\uD55C\uB2E4.",
753
+ ""
754
+ ].join("\n");
755
+ }
756
+ function isInteractiveApprovalAvailable() {
757
+ return process.stdin.isTTY && process.stdout.isTTY;
758
+ }
759
+ async function createFastTrackTask(input, projectRoot) {
760
+ if (!isSupportedTaskType(input.type)) {
761
+ throw new Error(`Unsupported type: ${input.type}`);
762
+ }
763
+ const startedTask = await startTask(input.type, input.slug, projectRoot);
764
+ const taskPath = join8(projectRoot, startedTask.workspacePath);
765
+ const specPath = join8(taskPath, "spec.md");
766
+ const planPath = join8(taskPath, "plan.md");
767
+ if (await getFsEntryKind(specPath) !== "file") {
768
+ throw new Error(`Missing spec.md for fast-track task ${startedTask.workspaceId}.`);
769
+ }
770
+ if (await getFsEntryKind(planPath) !== "file") {
771
+ throw new Error(`Missing plan.md for fast-track task ${startedTask.workspaceId}.`);
772
+ }
773
+ await writeFile5(specPath, renderMinimalSpec(input.type, input.slug), "utf8");
774
+ await writeFile5(planPath, renderMinimalPlan(input.slug), "utf8");
775
+ return {
776
+ failed: [],
777
+ nextStatus: "PENDING_SPEC_APPROVAL",
778
+ path: startedTask.workspacePath,
779
+ planCreated: true,
780
+ specCreated: true,
781
+ succeeded: [{ note: "created minimal spec and plan", taskId: startedTask.workspaceId }],
782
+ taskId: startedTask.workspaceId
783
+ };
784
+ }
785
+ function buildFailureRows(taskId, failedRows, fallbackNote) {
786
+ if (failedRows.length === 0) {
787
+ if (fallbackNote === "") {
788
+ return [];
25
789
  }
26
- return "file";
27
- } catch {
28
- return "missing";
790
+ return [{ note: fallbackNote, taskId }];
29
791
  }
792
+ return failedRows.map((row) => ({ note: row.note, taskId: row.taskId }));
30
793
  }
31
- async function ensureDirectory(targetPath) {
32
- await mkdir(targetPath, { recursive: true });
794
+ function buildSuccessRows(succeededRows) {
795
+ return succeededRows.map((row) => ({
796
+ note: `${row.note} (${String(row.steps)} steps)`,
797
+ taskId: row.taskId
798
+ }));
33
799
  }
34
- async function ensureReadableFile(targetPath) {
35
- await access(targetPath, constants.R_OK);
800
+ async function approveFastTrackTask(target, projectRoot) {
801
+ await approveSpecs(projectRoot, [toSpecApprovalTarget(target)], createSpecApprovedAt());
802
+ const planResult = await approvePlans(
803
+ projectRoot,
804
+ [toPlanApprovalTarget(target)],
805
+ createPlanApprovedAt()
806
+ );
807
+ if (planResult.succeeded.length === 0) {
808
+ return {
809
+ approved: false,
810
+ failed: buildFailureRows(target.id, planResult.failed, "plan approval failed"),
811
+ nextStatus: "SPEC_APPROVED",
812
+ succeeded: [{ note: "approved minimal spec", taskId: target.id }],
813
+ taskId: target.id
814
+ };
815
+ }
816
+ return {
817
+ approved: true,
818
+ failed: buildFailureRows(target.id, planResult.failed, ""),
819
+ nextStatus: "IN_PROGRESS",
820
+ succeeded: [
821
+ { note: "approved minimal spec", taskId: target.id },
822
+ ...buildSuccessRows(planResult.succeeded)
823
+ ],
824
+ taskId: target.id
825
+ };
36
826
  }
37
- async function copyFileIntoPlace(sourcePath, targetPath) {
38
- await copyFile(sourcePath, targetPath);
827
+
828
+ // src/commands/fast-track.ts
829
+ function formatFailures(rows) {
830
+ return rows.map((row) => `- ${row.taskId}: ${row.note}`);
831
+ }
832
+ async function runFastTrackCommand(input, projectRoot) {
833
+ try {
834
+ const createdTask = await createFastTrackTask(input, projectRoot);
835
+ const lines = [
836
+ "fast-track task created",
837
+ `\uACBD\uB85C: ${createdTask.path}/`,
838
+ "minimal spec: created",
839
+ "minimal plan: created"
840
+ ];
841
+ if (!isInteractiveApprovalAvailable()) {
842
+ lines.push(
843
+ "\uC0C1\uD0DC: PENDING_SPEC_APPROVAL",
844
+ "\uB2E4\uC74C \uB2E8\uACC4: `sduck spec approve <slug>` \uD6C4 `sduck plan approve <slug>`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
845
+ );
846
+ return {
847
+ exitCode: 0,
848
+ stderr: "",
849
+ stdout: lines.join("\n")
850
+ };
851
+ }
852
+ const shouldApprove = await confirm({
853
+ default: true,
854
+ message: "Approve the minimal spec and minimal plan now?"
855
+ });
856
+ if (!shouldApprove) {
857
+ lines.push(
858
+ "\uC0C1\uD0DC: PENDING_SPEC_APPROVAL",
859
+ "\uB2E4\uC74C \uB2E8\uACC4: `sduck spec approve <slug>` \uD6C4 `sduck plan approve <slug>`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
860
+ );
861
+ return {
862
+ exitCode: 0,
863
+ stderr: "",
864
+ stdout: lines.join("\n")
865
+ };
866
+ }
867
+ const approvalResult = await approveFastTrackTask(
868
+ {
869
+ id: createdTask.taskId,
870
+ path: createdTask.path,
871
+ slug: input.slug,
872
+ status: "PENDING_SPEC_APPROVAL"
873
+ },
874
+ projectRoot
875
+ );
876
+ lines.push(`\uC0C1\uD0DC: ${approvalResult.nextStatus}`);
877
+ if (approvalResult.failed.length > 0) {
878
+ lines.push("\uC2B9\uC778 \uACB0\uACFC:", ...formatFailures(approvalResult.failed));
879
+ }
880
+ if (approvalResult.approved) {
881
+ lines.push("fast-track \uC2B9\uC778 \uC644\uB8CC \u2192 \uBC14\uB85C \uC791\uC5C5\uC744 \uC2DC\uC791\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
882
+ } else {
883
+ lines.push("\uC77C\uBD80 \uC2B9\uC778 \uB2E8\uACC4\uAC00 \uC644\uB8CC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC77C\uBC18 \uC2B9\uC778 \uBA85\uB839\uC73C\uB85C \uC774\uC5B4\uC11C \uC9C4\uD589\uD558\uC138\uC694.");
884
+ }
885
+ return {
886
+ exitCode: approvalResult.failed.length > 0 ? 1 : 0,
887
+ stderr: "",
888
+ stdout: lines.join("\n")
889
+ };
890
+ } catch (error) {
891
+ return {
892
+ exitCode: 1,
893
+ stderr: error instanceof Error ? error.message : "Unknown fast-track failure.",
894
+ stdout: ""
895
+ };
896
+ }
39
897
  }
40
898
 
899
+ // src/commands/init.ts
900
+ import { checkbox } from "@inquirer/prompts";
901
+
41
902
  // src/core/agent-rules.ts
903
+ import { readFile as readFile6 } from "fs/promises";
904
+ import { dirname as dirname2, join as join9 } from "path";
905
+ import { fileURLToPath as fileURLToPath2 } from "url";
42
906
  var SDD_RULES_BEGIN = "<!-- sduck:begin -->";
43
907
  var SDD_RULES_END = "<!-- sduck:end -->";
44
908
  var SUPPORTED_AGENTS = [
@@ -51,17 +915,17 @@ var SUPPORTED_AGENTS = [
51
915
  ];
52
916
  var AGENT_RULE_TARGETS = [
53
917
  { agentId: "claude-code", outputPath: "CLAUDE.md", kind: "root-file" },
54
- { agentId: "codex", outputPath: "AGENTS.md", kind: "root-file" },
55
- { agentId: "opencode", outputPath: "AGENTS.md", kind: "root-file" },
918
+ { agentId: "codex", outputPath: "AGENT.md", kind: "root-file" },
919
+ { agentId: "opencode", outputPath: "AGENT.md", kind: "root-file" },
56
920
  { agentId: "gemini-cli", outputPath: "GEMINI.md", kind: "root-file" },
57
921
  {
58
922
  agentId: "cursor",
59
- outputPath: join(".cursor", "rules", "sduck-core.mdc"),
923
+ outputPath: join9(".cursor", "rules", "sduck-core.mdc"),
60
924
  kind: "managed-file"
61
925
  },
62
926
  {
63
927
  agentId: "antigravity",
64
- outputPath: join(".agents", "rules", "sduck-core.md"),
928
+ outputPath: join9(".agents", "rules", "sduck-core.md"),
65
929
  kind: "managed-file"
66
930
  }
67
931
  ];
@@ -120,10 +984,10 @@ function renderManagedBlock(lines) {
120
984
  return [SDD_RULES_BEGIN, ...lines, SDD_RULES_END].join("\n");
121
985
  }
122
986
  async function getAgentRulesAssetRoot() {
123
- const currentDirectoryPath = dirname(fileURLToPath(import.meta.url));
987
+ const currentDirectoryPath = dirname2(fileURLToPath2(import.meta.url));
124
988
  const candidatePaths = [
125
- join(currentDirectoryPath, "..", "..", "sduck-assets", "agent-rules"),
126
- join(currentDirectoryPath, "..", "sduck-assets", "agent-rules")
989
+ join9(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets", "agent-rules"),
990
+ join9(currentDirectoryPath, "..", ".sduck", "sduck-assets", "agent-rules")
127
991
  ];
128
992
  for (const candidatePath of candidatePaths) {
129
993
  if (await getFsEntryKind(candidatePath) === "directory") {
@@ -133,7 +997,7 @@ async function getAgentRulesAssetRoot() {
133
997
  throw new Error("Unable to locate bundled sduck agent rule assets.");
134
998
  }
135
999
  async function readAssetFile(assetRoot, fileName) {
136
- return await readFile(join(assetRoot, fileName), "utf8");
1000
+ return await readFile6(join9(assetRoot, fileName), "utf8");
137
1001
  }
138
1002
  function buildRootFileLines(agentIds, agentSpecificContent) {
139
1003
  const labels = SUPPORTED_AGENTS.filter((agent) => agentIds.includes(agent.id)).map(
@@ -165,111 +1029,67 @@ ${coreContent.trim()}
165
1029
  for (const agentId of relatedAgents) {
166
1030
  const templateFileName = AGENT_TEMPLATE_FILES[agentId];
167
1031
  specificSections.push((await readAssetFile(assetRoot, templateFileName)).trim());
168
- }
169
- const lines = buildRootFileLines(relatedAgents, [...specificSections, coreContent.trim()]);
170
- return `${renderManagedBlock(lines)}
171
- `;
172
- }
173
- function planAgentRuleActions(mode, targets, existingEntries, existingContents) {
174
- return targets.map((target) => {
175
- const currentKind = existingEntries.get(target.outputPath) ?? "missing";
176
- if (currentKind === "missing") {
177
- return { ...target, mergeMode: "create", currentKind };
178
- }
179
- if (currentKind !== "file") {
180
- return { ...target, mergeMode: "overwrite", currentKind };
181
- }
182
- if (target.kind === "managed-file") {
183
- return { ...target, mergeMode: mode === "force" ? "overwrite" : "keep", currentKind };
184
- }
185
- const content = existingContents.get(target.outputPath) ?? "";
186
- if (mode === "safe") {
187
- return { ...target, mergeMode: hasManagedBlock(content) ? "keep" : "prepend", currentKind };
188
- }
189
- return {
190
- ...target,
191
- mergeMode: hasManagedBlock(content) ? "replace-block" : "prepend",
192
- currentKind
193
- };
194
- });
195
- }
196
-
197
- // src/core/init.ts
198
- import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
199
- import { dirname as dirname3, join as join3 } from "path";
200
-
201
- // src/core/assets.ts
202
- import { dirname as dirname2, join as join2 } from "path";
203
- import { fileURLToPath as fileURLToPath2 } from "url";
204
- var SUPPORTED_TASK_TYPES = [
205
- "build",
206
- "feature",
207
- "fix",
208
- "refactor",
209
- "chore"
210
- ];
211
- var EVAL_ASSET_RELATIVE_PATHS = {
212
- plan: join2("eval", "plan.yml"),
213
- spec: join2("eval", "spec.yml")
214
- };
215
- var SPEC_TEMPLATE_RELATIVE_PATHS = {
216
- build: join2("types", "build.md"),
217
- feature: join2("types", "feature.md"),
218
- fix: join2("types", "fix.md"),
219
- refactor: join2("types", "refactor.md"),
220
- chore: join2("types", "chore.md")
221
- };
222
- var INIT_ASSET_RELATIVE_PATHS = [
223
- EVAL_ASSET_RELATIVE_PATHS.spec,
224
- EVAL_ASSET_RELATIVE_PATHS.plan,
225
- ...Object.values(SPEC_TEMPLATE_RELATIVE_PATHS)
226
- ];
227
- async function getBundledAssetsRoot() {
228
- const currentDirectoryPath = dirname2(fileURLToPath2(import.meta.url));
229
- const candidatePaths = [
230
- join2(currentDirectoryPath, "..", "..", "sduck-assets"),
231
- join2(currentDirectoryPath, "..", "sduck-assets")
232
- ];
233
- for (const candidatePath of candidatePaths) {
234
- if (await getFsEntryKind(candidatePath) === "directory") {
235
- return candidatePath;
236
- }
237
- }
238
- throw new Error("Unable to locate bundled sduck-assets directory.");
239
- }
240
- function isSupportedTaskType(value) {
241
- return SUPPORTED_TASK_TYPES.includes(value);
1032
+ }
1033
+ const lines = buildRootFileLines(relatedAgents, [...specificSections, coreContent.trim()]);
1034
+ return `${renderManagedBlock(lines)}
1035
+ `;
242
1036
  }
243
- function resolveSpecTemplateRelativePath(type) {
244
- return SPEC_TEMPLATE_RELATIVE_PATHS[type];
1037
+ function planAgentRuleActions(mode, targets, existingEntries, existingContents) {
1038
+ return targets.map((target) => {
1039
+ const currentKind = existingEntries.get(target.outputPath) ?? "missing";
1040
+ if (currentKind === "missing") {
1041
+ return { ...target, mergeMode: "create", currentKind };
1042
+ }
1043
+ if (currentKind !== "file") {
1044
+ return { ...target, mergeMode: "overwrite", currentKind };
1045
+ }
1046
+ if (target.kind === "managed-file") {
1047
+ return { ...target, mergeMode: mode === "force" ? "overwrite" : "keep", currentKind };
1048
+ }
1049
+ const content = existingContents.get(target.outputPath) ?? "";
1050
+ if (mode === "safe") {
1051
+ return { ...target, mergeMode: hasManagedBlock(content) ? "keep" : "prepend", currentKind };
1052
+ }
1053
+ return {
1054
+ ...target,
1055
+ mergeMode: hasManagedBlock(content) ? "replace-block" : "prepend",
1056
+ currentKind
1057
+ };
1058
+ });
245
1059
  }
246
1060
 
247
1061
  // src/core/init.ts
1062
+ import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
1063
+ import { dirname as dirname3, join as join10 } from "path";
248
1064
  var ASSET_TEMPLATE_DEFINITIONS = [
249
1065
  {
250
1066
  key: "eval-spec",
251
- relativePath: join3("sduck-assets", EVAL_ASSET_RELATIVE_PATHS.spec)
1067
+ relativePath: getProjectRelativeSduckAssetPath(EVAL_ASSET_RELATIVE_PATHS.spec)
252
1068
  },
253
1069
  {
254
1070
  key: "eval-plan",
255
- relativePath: join3("sduck-assets", EVAL_ASSET_RELATIVE_PATHS.plan)
1071
+ relativePath: getProjectRelativeSduckAssetPath(EVAL_ASSET_RELATIVE_PATHS.plan)
1072
+ },
1073
+ {
1074
+ key: "eval-task",
1075
+ relativePath: getProjectRelativeSduckAssetPath(EVAL_ASSET_RELATIVE_PATHS.task)
256
1076
  },
257
1077
  {
258
1078
  key: "type-build",
259
- relativePath: join3("sduck-assets", "types", "build.md")
1079
+ relativePath: getProjectRelativeSduckAssetPath("types", "build.md")
260
1080
  },
261
1081
  {
262
1082
  key: "type-feature",
263
- relativePath: join3("sduck-assets", "types", "feature.md")
1083
+ relativePath: getProjectRelativeSduckAssetPath("types", "feature.md")
264
1084
  },
265
- { key: "type-fix", relativePath: join3("sduck-assets", "types", "fix.md") },
1085
+ { key: "type-fix", relativePath: getProjectRelativeSduckAssetPath("types", "fix.md") },
266
1086
  {
267
1087
  key: "type-refactor",
268
- relativePath: join3("sduck-assets", "types", "refactor.md")
1088
+ relativePath: getProjectRelativeSduckAssetPath("types", "refactor.md")
269
1089
  },
270
1090
  {
271
1091
  key: "type-chore",
272
- relativePath: join3("sduck-assets", "types", "chore.md")
1092
+ relativePath: getProjectRelativeSduckAssetPath("types", "chore.md")
273
1093
  }
274
1094
  ];
275
1095
  var ASSET_TEMPLATE_MAP = Object.fromEntries(
@@ -353,7 +1173,7 @@ async function collectExistingEntries(projectRoot) {
353
1173
  for (const definition of ASSET_TEMPLATE_DEFINITIONS) {
354
1174
  existingEntries.set(
355
1175
  definition.relativePath,
356
- await getFsEntryKind(join3(projectRoot, definition.relativePath))
1176
+ await getFsEntryKind(join10(projectRoot, definition.relativePath))
357
1177
  );
358
1178
  }
359
1179
  return existingEntries;
@@ -361,9 +1181,9 @@ async function collectExistingEntries(projectRoot) {
361
1181
  async function collectExistingFileContents(projectRoot, targets) {
362
1182
  const contents = /* @__PURE__ */ new Map();
363
1183
  for (const target of targets) {
364
- const targetPath = join3(projectRoot, target.outputPath);
1184
+ const targetPath = join10(projectRoot, target.outputPath);
365
1185
  if (await getFsEntryKind(targetPath) === "file") {
366
- contents.set(target.outputPath, await readFile2(targetPath, "utf8"));
1186
+ contents.set(target.outputPath, await readFile7(targetPath, "utf8"));
367
1187
  }
368
1188
  }
369
1189
  return contents;
@@ -383,8 +1203,9 @@ async function initProject(options, projectRoot) {
383
1203
  const resolvedOptions = resolveInitOptions(options);
384
1204
  const { mode } = resolvedOptions;
385
1205
  const assetSourceRoot = await getBundledAssetsRoot();
386
- const assetsRoot = join3(projectRoot, "sduck-assets");
387
- const workspaceRoot = join3(projectRoot, "sduck-workspace");
1206
+ const sduckHomeRoot = getProjectSduckHomePath(projectRoot);
1207
+ const assetsRoot = getProjectSduckAssetsPath(projectRoot);
1208
+ const workspaceRoot = getProjectSduckWorkspacePath(projectRoot);
388
1209
  const summary = {
389
1210
  created: [],
390
1211
  prepended: [],
@@ -394,12 +1215,18 @@ async function initProject(options, projectRoot) {
394
1215
  errors: [],
395
1216
  rows: []
396
1217
  };
1218
+ const sduckHomeStatus = await ensureRootDirectory(sduckHomeRoot, "asset-root-conflict");
1219
+ summary[sduckHomeStatus].push(`${PROJECT_SDUCK_HOME_RELATIVE_PATH}/`);
1220
+ summary.rows.push({ path: `${PROJECT_SDUCK_HOME_RELATIVE_PATH}/`, status: sduckHomeStatus });
397
1221
  const assetsRootStatus = await ensureRootDirectory(assetsRoot, "asset-root-conflict");
398
- summary[assetsRootStatus].push("sduck-assets/");
399
- summary.rows.push({ path: "sduck-assets/", status: assetsRootStatus });
1222
+ summary[assetsRootStatus].push(`${PROJECT_SDUCK_ASSETS_RELATIVE_PATH}/`);
1223
+ summary.rows.push({ path: `${PROJECT_SDUCK_ASSETS_RELATIVE_PATH}/`, status: assetsRootStatus });
400
1224
  const workspaceRootStatus = await ensureRootDirectory(workspaceRoot, "workspace-root-conflict");
401
- summary[workspaceRootStatus].push("sduck-workspace/");
402
- summary.rows.push({ path: "sduck-workspace/", status: workspaceRootStatus });
1225
+ summary[workspaceRootStatus].push(`${PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH}/`);
1226
+ summary.rows.push({
1227
+ path: `${PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH}/`,
1228
+ status: workspaceRootStatus
1229
+ });
403
1230
  const actions = planInitActions(mode, await collectExistingEntries(projectRoot));
404
1231
  const actionSummary = summarizeInitActions(actions);
405
1232
  summary.created.push(...actionSummary.created);
@@ -419,13 +1246,10 @@ async function initProject(options, projectRoot) {
419
1246
  continue;
420
1247
  }
421
1248
  const definition = ASSET_TEMPLATE_MAP[action.key];
422
- const sourcePath = join3(
423
- assetSourceRoot,
424
- definition.relativePath.replace(/^sduck-assets[\\/]/, "")
425
- );
426
- const targetPath = join3(projectRoot, definition.relativePath);
1249
+ const sourcePath = join10(assetSourceRoot, toBundledAssetRelativePath(definition.relativePath));
1250
+ const targetPath = join10(projectRoot, definition.relativePath);
427
1251
  await ensureReadableFile(sourcePath);
428
- await mkdir2(dirname3(targetPath), { recursive: true });
1252
+ await mkdir3(dirname3(targetPath), { recursive: true });
429
1253
  await copyFileIntoPlace(sourcePath, targetPath);
430
1254
  }
431
1255
  const agentTargets = listAgentRuleTargets(resolvedOptions.agents);
@@ -433,7 +1257,7 @@ async function initProject(options, projectRoot) {
433
1257
  for (const target of agentTargets) {
434
1258
  agentEntryKinds.set(
435
1259
  target.outputPath,
436
- await getFsEntryKind(join3(projectRoot, target.outputPath))
1260
+ await getFsEntryKind(join10(projectRoot, target.outputPath))
437
1261
  );
438
1262
  }
439
1263
  const existingContents = await collectExistingFileContents(projectRoot, agentTargets);
@@ -454,11 +1278,11 @@ async function initProject(options, projectRoot) {
454
1278
  }
455
1279
  async function applyAgentRuleActions(projectRoot, actions, existingContents, summary, selectedAgents) {
456
1280
  for (const action of actions) {
457
- const targetPath = join3(projectRoot, action.outputPath);
1281
+ const targetPath = join10(projectRoot, action.outputPath);
458
1282
  const content = await renderAgentRuleContent(action, selectedAgents);
459
1283
  if (action.mergeMode === "create") {
460
- await mkdir2(dirname3(targetPath), { recursive: true });
461
- await writeFile(targetPath, content, "utf8");
1284
+ await mkdir3(dirname3(targetPath), { recursive: true });
1285
+ await writeFile6(targetPath, content, "utf8");
462
1286
  summary.created.push(action.outputPath);
463
1287
  summary.rows.push({ path: action.outputPath, status: "created" });
464
1288
  continue;
@@ -469,290 +1293,116 @@ async function applyAgentRuleActions(projectRoot, actions, existingContents, sum
469
1293
  summary.warnings.push(`Kept existing rule file: ${action.outputPath}`);
470
1294
  continue;
471
1295
  }
472
- await mkdir2(dirname3(targetPath), { recursive: true });
1296
+ await mkdir3(dirname3(targetPath), { recursive: true });
473
1297
  if (action.mergeMode === "prepend") {
474
1298
  const currentContent = existingContents.get(action.outputPath) ?? "";
475
- await writeFile(targetPath, prependManagedBlock(currentContent, content.trimEnd()), "utf8");
1299
+ await writeFile6(targetPath, prependManagedBlock(currentContent, content.trimEnd()), "utf8");
476
1300
  summary.prepended.push(action.outputPath);
477
1301
  summary.rows.push({ path: action.outputPath, status: "prepended" });
478
1302
  continue;
479
1303
  }
480
1304
  if (action.mergeMode === "replace-block") {
481
1305
  const currentContent = existingContents.get(action.outputPath) ?? "";
482
- await writeFile(targetPath, replaceManagedBlock(currentContent, content.trimEnd()), "utf8");
1306
+ await writeFile6(targetPath, replaceManagedBlock(currentContent, content.trimEnd()), "utf8");
483
1307
  summary.overwritten.push(action.outputPath);
484
1308
  summary.rows.push({ path: action.outputPath, status: "overwritten" });
485
1309
  continue;
486
1310
  }
487
- await writeFile(targetPath, content, "utf8");
1311
+ await writeFile6(targetPath, content, "utf8");
488
1312
  summary.overwritten.push(action.outputPath);
489
1313
  summary.rows.push({ path: action.outputPath, status: "overwritten" });
490
1314
  }
491
1315
  if (summary.kept.some((path) => path.endsWith(".md") || path.endsWith(".mdc"))) {
492
- summary.warnings.push(
493
- "Run `sduck init --force` to refresh managed rule content for selected agents."
494
- );
495
- }
496
- }
497
-
498
- // src/commands/init.ts
499
- function padCell(value, width) {
500
- return value.padEnd(width, " ");
501
- }
502
- function buildSummaryTable(rows) {
503
- const statusWidth = Math.max("Status".length, ...rows.map((row) => row.status.length));
504
- const pathWidth = Math.max("Path".length, ...rows.map((row) => row.path.length));
505
- const border = `+-${"-".repeat(statusWidth)}-+-${"-".repeat(pathWidth)}-+`;
506
- const header = `| ${padCell("Status", statusWidth)} | ${padCell("Path", pathWidth)} |`;
507
- const body = rows.map(
508
- (row) => `| ${padCell(row.status, statusWidth)} | ${padCell(row.path, pathWidth)} |`
509
- );
510
- return [border, header, border, ...body, border].join("\n");
511
- }
512
- function formatResult(result) {
513
- const lines = [
514
- result.didChange ? "sduck init completed." : "sduck init completed with no file changes."
515
- ];
516
- if (result.agents.length > 0) {
517
- lines.push(`Selected agents: ${result.agents.join(", ")}`);
518
- }
519
- lines.push("", buildSummaryTable(result.summary.rows));
520
- if (result.summary.warnings.length > 0) {
521
- lines.push("", "Warnings:");
522
- lines.push(...result.summary.warnings.map((warning) => `- ${warning}`));
523
- }
524
- return lines.join("\n");
525
- }
526
- async function resolveSelectedAgents(options) {
527
- const parsedAgents = parseAgentsOption(options.agents);
528
- if (parsedAgents.length > 0 || !process.stdin.isTTY || !process.stdout.isTTY) {
529
- return parsedAgents;
530
- }
531
- return await checkbox({
532
- message: "Select AI agents to generate repository rule files for",
533
- choices: SUPPORTED_AGENTS.map((agent) => ({
534
- name: agent.label,
535
- value: agent.id
536
- }))
537
- });
538
- }
539
- async function runInitCommand(options, projectRoot) {
540
- try {
541
- const resolvedOptions = {
542
- force: options.force,
543
- agents: await resolveSelectedAgents(options)
544
- };
545
- const result = await initProject(resolvedOptions, projectRoot);
546
- return {
547
- exitCode: 0,
548
- stderr: "",
549
- stdout: formatResult(result)
550
- };
551
- } catch (error) {
552
- const message = error instanceof Error ? error.message : "Unknown init failure.";
553
- return {
554
- exitCode: 1,
555
- stderr: message,
556
- stdout: ""
557
- };
558
- }
559
- }
560
-
561
- // src/commands/plan-approve.ts
562
- import { checkbox as checkbox2 } from "@inquirer/prompts";
563
-
564
- // src/core/plan-approve.ts
565
- import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
566
- import { join as join5 } from "path";
567
-
568
- // src/core/workspace.ts
569
- import { readFile as readFile3 } from "fs/promises";
570
- import { join as join4 } from "path";
571
- var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "PENDING_SPEC_APPROVAL", "PENDING_PLAN_APPROVAL"]);
572
- function parseMetaText(content) {
573
- const createdAtMatch = /^created_at:\s+(.+)$/m.exec(content);
574
- const idMatch = /^id:\s+(.+)$/m.exec(content);
575
- const slugMatch = /^slug:\s+(.+)$/m.exec(content);
576
- const statusMatch = /^status:\s+(.+)$/m.exec(content);
577
- const parsedMeta = {};
578
- if (createdAtMatch?.[1] !== void 0) {
579
- parsedMeta.createdAt = createdAtMatch[1].trim();
580
- }
581
- if (idMatch?.[1] !== void 0) {
582
- parsedMeta.id = idMatch[1].trim();
583
- }
584
- if (slugMatch?.[1] !== void 0) {
585
- parsedMeta.slug = slugMatch[1].trim();
586
- }
587
- if (statusMatch?.[1] !== void 0) {
588
- parsedMeta.status = statusMatch[1].trim();
589
- }
590
- return parsedMeta;
591
- }
592
- function sortTasksByRecency(tasks) {
593
- return [...tasks].sort((left, right) => {
594
- const leftValue = left.createdAt ?? "";
595
- const rightValue = right.createdAt ?? "";
596
- return rightValue.localeCompare(leftValue);
597
- });
598
- }
599
- async function listWorkspaceTasks(projectRoot) {
600
- const workspaceRoot = join4(projectRoot, "sduck-workspace");
601
- if (await getFsEntryKind(workspaceRoot) !== "directory") {
602
- return [];
603
- }
604
- const { readdir } = await import("fs/promises");
605
- const entries = await readdir(workspaceRoot, { withFileTypes: true });
606
- const tasks = [];
607
- for (const entry of entries) {
608
- if (!entry.isDirectory()) {
609
- continue;
610
- }
611
- const relativePath = join4("sduck-workspace", entry.name);
612
- const metaPath = join4(projectRoot, relativePath, "meta.yml");
613
- if (await getFsEntryKind(metaPath) !== "file") {
614
- continue;
615
- }
616
- const parsedMeta = parseMetaText(await readFile3(metaPath, "utf8"));
617
- if (parsedMeta.id !== void 0 && parsedMeta.status !== void 0) {
618
- const task = {
619
- id: parsedMeta.id,
620
- path: relativePath,
621
- status: parsedMeta.status
622
- };
623
- if (parsedMeta.createdAt !== void 0) {
624
- task.createdAt = parsedMeta.createdAt;
625
- }
626
- if (parsedMeta.slug !== void 0) {
627
- task.slug = parsedMeta.slug;
628
- }
629
- tasks.push(task);
630
- }
631
- }
632
- return sortTasksByRecency(tasks);
633
- }
634
- async function findActiveTask(projectRoot) {
635
- const tasks = await listWorkspaceTasks(projectRoot);
636
- for (const task of tasks) {
637
- if (ACTIVE_STATUSES.has(task.status)) {
638
- return {
639
- id: task.id,
640
- path: task.path,
641
- status: task.status
642
- };
643
- }
644
- }
645
- return null;
646
- }
647
-
648
- // src/utils/utc-date.ts
649
- function pad2(value) {
650
- return String(value).padStart(2, "0");
651
- }
652
- function formatUtcDate(date) {
653
- const year = String(date.getUTCFullYear());
654
- const month = pad2(date.getUTCMonth() + 1);
655
- const day = pad2(date.getUTCDate());
656
- return `${year}-${month}-${day}`;
657
- }
658
- function formatUtcTimestamp(date) {
659
- const year = String(date.getUTCFullYear());
660
- const month = pad2(date.getUTCMonth() + 1);
661
- const day = pad2(date.getUTCDate());
662
- const hour = pad2(date.getUTCHours());
663
- const minute = pad2(date.getUTCMinutes());
664
- const second = pad2(date.getUTCSeconds());
665
- return `${year}-${month}-${day}T${hour}:${minute}:${second}Z`;
666
- }
667
-
668
- // src/core/plan-approve.ts
669
- function filterPlanApprovalCandidates(tasks) {
670
- return tasks.filter((task) => task.status === "SPEC_APPROVED");
671
- }
672
- function resolvePlanApprovalCandidates(tasks, target) {
673
- const candidates = filterPlanApprovalCandidates(tasks);
674
- if (target === void 0 || target.trim() === "") {
675
- return candidates;
676
- }
677
- const trimmedTarget = target.trim();
678
- return candidates.filter(
679
- (task) => task.id === trimmedTarget || task.slug === trimmedTarget || task.id.endsWith(trimmedTarget)
680
- );
681
- }
682
- function countPlanSteps(planContent) {
683
- const matches = planContent.match(/^## Step \d+\. .+$/gm);
684
- return matches?.length ?? 0;
685
- }
686
- function updatePlanApprovalBlock(metaContent, approvedAt, totalSteps) {
687
- const withStatus = metaContent.replace(/^status:\s+.+$/m, "status: IN_PROGRESS");
688
- const withPlan = withStatus.replace(
689
- /plan:\n {2}approved:\s+false\n {2}approved_at:\s+null/m,
690
- `plan:
691
- approved: true
692
- approved_at: ${approvedAt}`
693
- );
694
- return withPlan.replace(
695
- /steps:\n {2}total:\s+null\n {2}completed:\s+\[\]/m,
696
- `steps:
697
- total: ${String(totalSteps)}
698
- completed: []`
699
- );
700
- }
701
- async function approvePlans(projectRoot, tasks, approvedAt) {
702
- const succeeded = [];
703
- const failed = [];
704
- for (const task of tasks) {
705
- if (task.status !== "SPEC_APPROVED") {
706
- failed.push({
707
- note: `task is not awaiting plan approval (${task.status})`,
708
- taskId: task.id
709
- });
710
- continue;
711
- }
712
- const metaPath = join5(projectRoot, task.path, "meta.yml");
713
- const planPath = join5(projectRoot, task.path, "plan.md");
714
- if (await getFsEntryKind(metaPath) !== "file") {
715
- failed.push({ note: "missing meta.yml", taskId: task.id });
716
- continue;
717
- }
718
- if (await getFsEntryKind(planPath) !== "file") {
719
- failed.push({ note: "missing plan.md", taskId: task.id });
720
- continue;
721
- }
722
- const planContent = await readFile4(planPath, "utf8");
723
- const totalSteps = countPlanSteps(planContent);
724
- if (totalSteps === 0) {
725
- failed.push({ note: "missing valid Step headers", taskId: task.id });
726
- continue;
727
- }
728
- const updatedMeta = updatePlanApprovalBlock(
729
- await readFile4(metaPath, "utf8"),
730
- approvedAt,
731
- totalSteps
1316
+ summary.warnings.push(
1317
+ "Run `sduck init --force` to refresh managed rule content for selected agents."
732
1318
  );
733
- await writeFile2(metaPath, updatedMeta, "utf8");
734
- succeeded.push({ note: "moved to IN_PROGRESS", steps: totalSteps, taskId: task.id });
735
1319
  }
1320
+ }
1321
+
1322
+ // src/commands/init.ts
1323
+ var AGENT_PROMPT_MESSAGE = "Select AI agents to generate repository rule files for";
1324
+ var AGENT_PROMPT_INSTRUCTIONS = "Use space to toggle agents, arrow keys to move, and enter to submit.";
1325
+ var AGENT_PROMPT_REQUIRED_MESSAGE = "Select at least one agent. Use space to toggle and enter to submit.";
1326
+ function padCell2(value, width) {
1327
+ return value.padEnd(width, " ");
1328
+ }
1329
+ function buildSummaryTable(rows) {
1330
+ const statusWidth = Math.max("Status".length, ...rows.map((row) => row.status.length));
1331
+ const pathWidth = Math.max("Path".length, ...rows.map((row) => row.path.length));
1332
+ const border = `+-${"-".repeat(statusWidth)}-+-${"-".repeat(pathWidth)}-+`;
1333
+ const header = `| ${padCell2("Status", statusWidth)} | ${padCell2("Path", pathWidth)} |`;
1334
+ const body = rows.map(
1335
+ (row) => `| ${padCell2(row.status, statusWidth)} | ${padCell2(row.path, pathWidth)} |`
1336
+ );
1337
+ return [border, header, border, ...body, border].join("\n");
1338
+ }
1339
+ function formatResult(result) {
1340
+ const lines = [
1341
+ result.didChange ? "sduck init completed." : "sduck init completed with no file changes."
1342
+ ];
1343
+ if (result.agents.length > 0) {
1344
+ lines.push(`Selected agents: ${result.agents.join(", ")}`);
1345
+ }
1346
+ lines.push("", buildSummaryTable(result.summary.rows));
1347
+ if (result.summary.warnings.length > 0) {
1348
+ lines.push("", "Warnings:");
1349
+ lines.push(...result.summary.warnings.map((warning) => `- ${warning}`));
1350
+ }
1351
+ return lines.join("\n");
1352
+ }
1353
+ function normalizeSelectedAgents(agentIds) {
1354
+ const selectedAgentSet = new Set(agentIds);
1355
+ return SUPPORTED_AGENTS.map((agent) => agent.id).filter(
1356
+ (agentId) => selectedAgentSet.has(agentId)
1357
+ );
1358
+ }
1359
+ function createAgentCheckboxConfig() {
736
1360
  return {
737
- approvedAt,
738
- failed,
739
- nextStatus: "IN_PROGRESS",
740
- succeeded
1361
+ message: AGENT_PROMPT_MESSAGE,
1362
+ instructions: AGENT_PROMPT_INSTRUCTIONS,
1363
+ required: true,
1364
+ validate: (choices) => choices.length > 0 || AGENT_PROMPT_REQUIRED_MESSAGE,
1365
+ choices: SUPPORTED_AGENTS.map((agent) => ({
1366
+ name: agent.label,
1367
+ value: agent.id
1368
+ }))
741
1369
  };
742
1370
  }
743
- async function loadPlanApprovalCandidates(projectRoot, input) {
744
- const tasks = await listWorkspaceTasks(projectRoot);
745
- return resolvePlanApprovalCandidates(tasks, input.target);
1371
+ async function resolveSelectedAgents(options) {
1372
+ const parsedAgents = parseAgentsOption(options.agents);
1373
+ if (parsedAgents.length > 0 || !process.stdin.isTTY || !process.stdout.isTTY) {
1374
+ return normalizeSelectedAgents(parsedAgents);
1375
+ }
1376
+ return normalizeSelectedAgents(await checkbox(createAgentCheckboxConfig()));
746
1377
  }
747
- function createPlanApprovedAt(date = /* @__PURE__ */ new Date()) {
748
- return formatUtcTimestamp(date);
1378
+ async function runInitCommand(options, projectRoot) {
1379
+ try {
1380
+ const resolvedOptions = {
1381
+ force: options.force,
1382
+ agents: await resolveSelectedAgents(options)
1383
+ };
1384
+ const result = await initProject(resolvedOptions, projectRoot);
1385
+ return {
1386
+ exitCode: 0,
1387
+ stderr: "",
1388
+ stdout: formatResult(result)
1389
+ };
1390
+ } catch (error) {
1391
+ const message = error instanceof Error ? error.message : "Unknown init failure.";
1392
+ return {
1393
+ exitCode: 1,
1394
+ stderr: message,
1395
+ stdout: ""
1396
+ };
1397
+ }
749
1398
  }
750
1399
 
751
1400
  // src/commands/plan-approve.ts
752
- function padCell2(value, width) {
1401
+ import { checkbox as checkbox2 } from "@inquirer/prompts";
1402
+ function padCell3(value, width) {
753
1403
  return value.padEnd(width, " ");
754
1404
  }
755
- function buildResultTable(result) {
1405
+ function buildResultTable2(result) {
756
1406
  const rows = [
757
1407
  ...result.succeeded.map((row) => ({
758
1408
  note: row.note,
@@ -772,17 +1422,17 @@ function buildResultTable(result) {
772
1422
  const stepsWidth = Math.max("Steps".length, ...rows.map((row) => row.steps.length));
773
1423
  const noteWidth = Math.max("Note".length, ...rows.map((row) => row.note.length));
774
1424
  const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(stepsWidth)}-+-${"-".repeat(noteWidth)}-+`;
775
- const header = `| ${padCell2("Result", resultWidth)} | ${padCell2("Task", taskWidth)} | ${padCell2("Steps", stepsWidth)} | ${padCell2("Note", noteWidth)} |`;
1425
+ const header = `| ${padCell3("Result", resultWidth)} | ${padCell3("Task", taskWidth)} | ${padCell3("Steps", stepsWidth)} | ${padCell3("Note", noteWidth)} |`;
776
1426
  const body = rows.map(
777
- (row) => `| ${padCell2(row.result, resultWidth)} | ${padCell2(row.task, taskWidth)} | ${padCell2(row.steps, stepsWidth)} | ${padCell2(row.note, noteWidth)} |`
1427
+ (row) => `| ${padCell3(row.result, resultWidth)} | ${padCell3(row.task, taskWidth)} | ${padCell3(row.steps, stepsWidth)} | ${padCell3(row.note, noteWidth)} |`
778
1428
  );
779
1429
  return [border, header, border, ...body, border].join("\n");
780
1430
  }
781
1431
  function formatTaskLabel(task) {
782
1432
  return `${task.id} (${task.status})`;
783
1433
  }
784
- function formatSuccess(result) {
785
- const lines = [buildResultTable(result)];
1434
+ function formatSuccess2(result) {
1435
+ const lines = [buildResultTable2(result)];
786
1436
  if (result.succeeded.length > 0) {
787
1437
  lines.push("", "\uC0C1\uD0DC: IN_PROGRESS \u2192 \uC791\uC5C5\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.");
788
1438
  }
@@ -817,14 +1467,14 @@ async function runPlanApproveCommand(input, projectRoot) {
817
1467
  if (result.succeeded.length === 0) {
818
1468
  return {
819
1469
  exitCode: 1,
820
- stderr: buildResultTable(result),
1470
+ stderr: buildResultTable2(result),
821
1471
  stdout: ""
822
1472
  };
823
1473
  }
824
1474
  return {
825
1475
  exitCode: result.failed.length > 0 ? 1 : 0,
826
1476
  stderr: result.failed.length > 0 ? "" : "",
827
- stdout: formatSuccess(result)
1477
+ stdout: formatSuccess2(result)
828
1478
  };
829
1479
  } catch (error) {
830
1480
  return {
@@ -837,72 +1487,10 @@ async function runPlanApproveCommand(input, projectRoot) {
837
1487
 
838
1488
  // src/commands/spec-approve.ts
839
1489
  import { checkbox as checkbox3 } from "@inquirer/prompts";
840
-
841
- // src/core/spec-approve.ts
842
- import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
843
- import { join as join6 } from "path";
844
- function filterApprovalCandidates(tasks) {
845
- return tasks.filter((task) => task.status === "PENDING_SPEC_APPROVAL");
846
- }
847
- function resolveTargetCandidates(tasks, target) {
848
- const candidates = filterApprovalCandidates(tasks);
849
- if (target === void 0 || target.trim() === "") {
850
- return candidates;
851
- }
852
- const trimmedTarget = target.trim();
853
- return candidates.filter(
854
- (task) => task.id === trimmedTarget || task.slug === trimmedTarget || task.id.endsWith(trimmedTarget)
855
- );
856
- }
857
- function validateSpecApprovalTargets(tasks) {
858
- if (tasks.length === 0) {
859
- throw new Error("No approvable spec tasks found.");
860
- }
861
- const invalidTask = tasks.find((task) => task.status !== "PENDING_SPEC_APPROVAL");
862
- if (invalidTask !== void 0) {
863
- throw new Error(
864
- `Task ${invalidTask.id} is not awaiting spec approval (${invalidTask.status}).`
865
- );
866
- }
867
- }
868
- function updateSpecApprovalBlock(metaContent, approvedAt) {
869
- const withStatus = metaContent.replace(/^status:\s+.+$/m, "status: SPEC_APPROVED");
870
- return withStatus.replace(
871
- /spec:\n {2}approved:\s+false\n {2}approved_at:\s+null/m,
872
- `spec:
873
- approved: true
874
- approved_at: ${approvedAt}`
875
- );
876
- }
877
- async function approveSpecs(projectRoot, tasks, approvedAt) {
878
- validateSpecApprovalTargets(tasks);
879
- for (const task of tasks) {
880
- const metaPath = join6(projectRoot, task.path, "meta.yml");
881
- if (await getFsEntryKind(metaPath) !== "file") {
882
- throw new Error(`Missing meta.yml for task ${task.id}.`);
883
- }
884
- const updatedContent = updateSpecApprovalBlock(await readFile5(metaPath, "utf8"), approvedAt);
885
- await writeFile3(metaPath, updatedContent, "utf8");
886
- }
887
- return {
888
- approvedAt,
889
- approvedTaskIds: tasks.map((task) => task.id),
890
- nextStatus: "SPEC_APPROVED"
891
- };
892
- }
893
- async function loadSpecApprovalCandidates(projectRoot, input) {
894
- const tasks = await listWorkspaceTasks(projectRoot);
895
- return resolveTargetCandidates(tasks, input.target);
896
- }
897
- function createSpecApprovedAt(date = /* @__PURE__ */ new Date()) {
898
- return formatUtcTimestamp(date);
899
- }
900
-
901
- // src/commands/spec-approve.ts
902
1490
  function formatTaskLabel2(task) {
903
1491
  return `${task.id} (${task.status})`;
904
1492
  }
905
- function formatSuccess2(result, tasks) {
1493
+ function formatSuccess3(result, tasks) {
906
1494
  const lines = ["\uC2A4\uD399 \uC2B9\uC778\uB428"];
907
1495
  for (const task of tasks) {
908
1496
  lines.push(`- ${task.path} -> ${result.nextStatus}`);
@@ -943,7 +1531,7 @@ async function runSpecApproveCommand(input, projectRoot) {
943
1531
  return {
944
1532
  exitCode: 0,
945
1533
  stderr: "",
946
- stdout: formatSuccess2(result, selectedTasks)
1534
+ stdout: formatSuccess3(result, selectedTasks)
947
1535
  };
948
1536
  } catch (error) {
949
1537
  return {
@@ -954,104 +1542,6 @@ async function runSpecApproveCommand(input, projectRoot) {
954
1542
  }
955
1543
  }
956
1544
 
957
- // src/core/start.ts
958
- import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
959
- import { join as join7 } from "path";
960
- function normalizeSlug(input) {
961
- return input.trim().toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
962
- }
963
- function validateSlug(slug) {
964
- if (slug === "") {
965
- throw new Error("Invalid slug: slug cannot be empty after normalization.");
966
- }
967
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
968
- throw new Error("Invalid slug: use lowercase kebab-case only.");
969
- }
970
- }
971
- function createWorkspaceId(date, type, slug) {
972
- const year = String(date.getUTCFullYear());
973
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
974
- const day = String(date.getUTCDate()).padStart(2, "0");
975
- const hour = String(date.getUTCHours()).padStart(2, "0");
976
- const minute = String(date.getUTCMinutes()).padStart(2, "0");
977
- return `${year}${month}${day}-${hour}${minute}-${type}-${slug}`;
978
- }
979
- function renderInitialMeta(input) {
980
- return [
981
- `id: ${input.id}`,
982
- `type: ${input.type}`,
983
- `slug: ${input.slug}`,
984
- `created_at: ${input.createdAt}`,
985
- "",
986
- "status: PENDING_SPEC_APPROVAL",
987
- "",
988
- "spec:",
989
- " approved: false",
990
- " approved_at: null",
991
- "",
992
- "plan:",
993
- " approved: false",
994
- " approved_at: null",
995
- "",
996
- "steps:",
997
- " total: null",
998
- " completed: []",
999
- "",
1000
- "completed_at: null",
1001
- ""
1002
- ].join("\n");
1003
- }
1004
- async function resolveSpecTemplatePath(type) {
1005
- const assetsRoot = await getBundledAssetsRoot();
1006
- return join7(assetsRoot, resolveSpecTemplateRelativePath(type));
1007
- }
1008
- function applyTemplateDefaults(template, type, slug, currentDate) {
1009
- const displayName = slug.replace(/-/g, " ");
1010
- return template.replace(/\{기능명\}/g, displayName).replace(/\{버그 요약 한 줄\}/g, displayName).replace(/YYYY-MM-DD/g, formatUtcDate(currentDate)).replace(/> \*\*작성자:\*\*\s*$/m, "> **\uC791\uC131\uC790:** taehee").replace(/> \*\*연관 티켓:\*\*\s*$/m, "> **\uC5F0\uAD00 \uD2F0\uCF13:** -").replace(/^# \[(feature|fix|refactor|chore|build)\] .*/m, `# [${type}] ${displayName}`);
1011
- }
1012
- async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE__ */ new Date()) {
1013
- if (!isSupportedTaskType(rawType)) {
1014
- throw new Error(`Unsupported type: ${rawType}`);
1015
- }
1016
- const slug = normalizeSlug(rawSlug);
1017
- validateSlug(slug);
1018
- const activeTask = await findActiveTask(projectRoot);
1019
- if (activeTask !== null) {
1020
- throw new Error(
1021
- `Active task exists: ${activeTask.id} (${activeTask.status}) at ${activeTask.path}. Finish or approve it before starting a new task.`
1022
- );
1023
- }
1024
- const workspaceId = createWorkspaceId(currentDate, rawType, slug);
1025
- const workspacePath = join7("sduck-workspace", workspaceId);
1026
- const absoluteWorkspacePath = join7(projectRoot, workspacePath);
1027
- if (await getFsEntryKind(absoluteWorkspacePath) !== "missing") {
1028
- throw new Error(`Workspace already exists: ${workspacePath}`);
1029
- }
1030
- const workspaceRoot = join7(projectRoot, "sduck-workspace");
1031
- await mkdir3(workspaceRoot, { recursive: true });
1032
- await mkdir3(absoluteWorkspacePath, { recursive: false });
1033
- const templatePath = await resolveSpecTemplatePath(rawType);
1034
- if (await getFsEntryKind(templatePath) !== "file") {
1035
- throw new Error(`Missing spec template for type '${rawType}' at ${templatePath}`);
1036
- }
1037
- const specTemplate = await readFile6(templatePath, "utf8");
1038
- const specContent = applyTemplateDefaults(specTemplate, rawType, slug, currentDate);
1039
- const metaContent = renderInitialMeta({
1040
- createdAt: formatUtcTimestamp(currentDate),
1041
- id: workspaceId,
1042
- slug,
1043
- type: rawType
1044
- });
1045
- await writeFile4(join7(absoluteWorkspacePath, "meta.yml"), metaContent, "utf8");
1046
- await writeFile4(join7(absoluteWorkspacePath, "spec.md"), specContent, "utf8");
1047
- await writeFile4(join7(absoluteWorkspacePath, "plan.md"), "", "utf8");
1048
- return {
1049
- workspaceId,
1050
- workspacePath,
1051
- status: "PENDING_SPEC_APPROVAL"
1052
- };
1053
- }
1054
-
1055
1545
  // src/commands/start.ts
1056
1546
  async function runStartCommand(type, slug, projectRoot) {
1057
1547
  try {
@@ -1074,8 +1564,70 @@ async function runStartCommand(type, slug, projectRoot) {
1074
1564
  }
1075
1565
  }
1076
1566
 
1567
+ // package.json
1568
+ var package_default = {
1569
+ name: "@sduck/sduck-cli",
1570
+ version: "0.1.2",
1571
+ description: "Spec-Driven Development CLI bootstrap",
1572
+ type: "module",
1573
+ bin: {
1574
+ sduck: "dist/cli.js"
1575
+ },
1576
+ files: [
1577
+ "dist",
1578
+ "sduck-assets"
1579
+ ],
1580
+ engines: {
1581
+ node: ">=20"
1582
+ },
1583
+ scripts: {
1584
+ dev: "tsx src/cli.ts",
1585
+ build: "tsup src/cli.ts --config tsup.config.ts",
1586
+ lint: "eslint . --max-warnings=0",
1587
+ format: "prettier --write .",
1588
+ "format:check": "prettier --check .",
1589
+ typecheck: "tsc --project tsconfig.json --noEmit",
1590
+ "test:unit": "vitest run tests/unit",
1591
+ "test:e2e": "vitest run tests/e2e",
1592
+ test: "npm run test:unit && npm run test:e2e",
1593
+ prepare: "husky"
1594
+ },
1595
+ "lint-staged": {
1596
+ "*.{js,mjs,cjs,ts,mts,cts}": [
1597
+ "eslint --fix",
1598
+ "prettier --write"
1599
+ ],
1600
+ "*.{json,md,yml,yaml}": [
1601
+ "prettier --write"
1602
+ ]
1603
+ },
1604
+ dependencies: {
1605
+ "@inquirer/prompts": "^7.8.6",
1606
+ commander: "^14.0.1",
1607
+ "js-yaml": "^4.1.0"
1608
+ },
1609
+ devDependencies: {
1610
+ "@eslint/js": "^9.23.0",
1611
+ "@types/node": "^22.13.14",
1612
+ eslint: "^9.23.0",
1613
+ "eslint-config-prettier": "^10.1.1",
1614
+ "eslint-plugin-import": "^2.31.0",
1615
+ "eslint-import-resolver-typescript": "^4.3.5",
1616
+ "eslint-plugin-n": "^17.16.2",
1617
+ husky: "^9.1.7",
1618
+ "lint-staged": "^15.5.0",
1619
+ prettier: "^3.5.3",
1620
+ tsup: "^8.4.0",
1621
+ tsx: "^4.19.3",
1622
+ typescript: "^5.8.2",
1623
+ "typescript-eslint": "^8.28.0",
1624
+ vitest: "^3.0.8"
1625
+ }
1626
+ };
1627
+
1077
1628
  // src/core/command-metadata.ts
1078
1629
  var CLI_NAME = "sduck";
1630
+ var CLI_VERSION = package_default.version;
1079
1631
  var CLI_DESCRIPTION = "Spec-Driven Development workflow bootstrap CLI";
1080
1632
  var PLACEHOLDER_MESSAGE = "Core workflow commands are planned but not implemented in this bootstrap yet.";
1081
1633
  function normalizeCommandName(input) {
@@ -1084,8 +1636,8 @@ function normalizeCommandName(input) {
1084
1636
 
1085
1637
  // src/cli.ts
1086
1638
  var program = new Command();
1087
- program.name(CLI_NAME).description(CLI_DESCRIPTION).version("0.1.0");
1088
- program.command("init").description("Initialize the current repository for the SDD workflow").option("--force", "Regenerate the bundled assets in sduck-assets").option(
1639
+ program.name(CLI_NAME).description(CLI_DESCRIPTION).version(CLI_VERSION);
1640
+ program.command("init").description("Initialize the current repository for the SDD workflow").option("--force", "Regenerate the bundled assets in .sduck/sduck-assets").option(
1089
1641
  "--agents <agents>",
1090
1642
  "Comma-separated agents (claude-code,codex,opencode,gemini-cli,cursor,antigravity)"
1091
1643
  ).action(async (options) => {
@@ -1116,6 +1668,18 @@ program.command("start <type> <slug>").description("Create a new task workspace
1116
1668
  process.exitCode = result.exitCode;
1117
1669
  }
1118
1670
  });
1671
+ program.command("fast-track <type> <slug>").description("Create a minimal spec/plan task with optional bundled approval").action(async (type, slug) => {
1672
+ const result = await runFastTrackCommand({ slug, type }, process.cwd());
1673
+ if (result.stdout !== "") {
1674
+ console.log(result.stdout);
1675
+ }
1676
+ if (result.stderr !== "") {
1677
+ console.error(result.stderr);
1678
+ }
1679
+ if (result.exitCode !== 0) {
1680
+ process.exitCode = result.exitCode;
1681
+ }
1682
+ });
1119
1683
  program.command("spec").description("Manage spec workflow state").command("approve [target]").description("Approve a task spec and move it to plan writing").action(async (target) => {
1120
1684
  const input = target === void 0 ? {} : { target };
1121
1685
  const result = await runSpecApproveCommand(input, process.cwd());
@@ -1142,6 +1706,19 @@ program.command("plan").description("Manage plan workflow state").command("appro
1142
1706
  process.exitCode = result.exitCode;
1143
1707
  }
1144
1708
  });
1709
+ program.command("done [target]").description("Complete an in-progress task after validation").action(async (target) => {
1710
+ const input = target === void 0 ? {} : { target };
1711
+ const result = await runDoneCommand(input, process.cwd());
1712
+ if (result.stdout !== "") {
1713
+ console.log(result.stdout);
1714
+ }
1715
+ if (result.stderr !== "") {
1716
+ console.error(result.stderr);
1717
+ }
1718
+ if (result.exitCode !== 0) {
1719
+ process.exitCode = result.exitCode;
1720
+ }
1721
+ });
1145
1722
  program.command("roadmap").description("Show the current bootstrap status").action(() => {
1146
1723
  console.log(PLACEHOLDER_MESSAGE);
1147
1724
  });