sdd-tool 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -2
- package/dist/cli/index.js +1746 -57
- package/dist/cli/index.js.map +1 -1
- package/package.json +10 -9
package/dist/cli/index.js
CHANGED
|
@@ -128,10 +128,10 @@ var init_base = __esm({
|
|
|
128
128
|
};
|
|
129
129
|
FileSystemError = class extends SddError {
|
|
130
130
|
path;
|
|
131
|
-
constructor(code,
|
|
132
|
-
super(code, formatMessage(code,
|
|
131
|
+
constructor(code, path27) {
|
|
132
|
+
super(code, formatMessage(code, path27), ExitCode.FILE_SYSTEM_ERROR);
|
|
133
133
|
this.name = "FileSystemError";
|
|
134
|
-
this.path =
|
|
134
|
+
this.path = path27;
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
ValidationError = class extends SddError {
|
|
@@ -3631,9 +3631,9 @@ async function validateSpecs(targetPath, options = {}) {
|
|
|
3631
3631
|
async function findSpecFiles(dirPath) {
|
|
3632
3632
|
const files = [];
|
|
3633
3633
|
async function scanDir(dir) {
|
|
3634
|
-
const { promises:
|
|
3634
|
+
const { promises: fs15 } = await import("fs");
|
|
3635
3635
|
try {
|
|
3636
|
-
const entries = await
|
|
3636
|
+
const entries = await fs15.readdir(dir, { withFileTypes: true });
|
|
3637
3637
|
for (const entry of entries) {
|
|
3638
3638
|
const fullPath = path3.join(dir, entry.name);
|
|
3639
3639
|
if (entry.isDirectory()) {
|
|
@@ -5635,19 +5635,19 @@ function detectCircularDependencies(graph) {
|
|
|
5635
5635
|
const cycles = [];
|
|
5636
5636
|
const visited = /* @__PURE__ */ new Set();
|
|
5637
5637
|
const recStack = /* @__PURE__ */ new Set();
|
|
5638
|
-
function dfs(nodeId,
|
|
5638
|
+
function dfs(nodeId, path27) {
|
|
5639
5639
|
visited.add(nodeId);
|
|
5640
5640
|
recStack.add(nodeId);
|
|
5641
5641
|
const node = graph.nodes.get(nodeId);
|
|
5642
5642
|
if (!node) return false;
|
|
5643
5643
|
for (const depId of node.dependsOn) {
|
|
5644
5644
|
if (!visited.has(depId)) {
|
|
5645
|
-
if (dfs(depId, [...
|
|
5645
|
+
if (dfs(depId, [...path27, nodeId])) {
|
|
5646
5646
|
return true;
|
|
5647
5647
|
}
|
|
5648
5648
|
} else if (recStack.has(depId)) {
|
|
5649
|
-
const cycleStart =
|
|
5650
|
-
const cycle = cycleStart >= 0 ? [...
|
|
5649
|
+
const cycleStart = path27.indexOf(depId);
|
|
5650
|
+
const cycle = cycleStart >= 0 ? [...path27.slice(cycleStart), nodeId, depId] : [nodeId, depId];
|
|
5651
5651
|
cycles.push({
|
|
5652
5652
|
cycle,
|
|
5653
5653
|
description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
|
|
@@ -8139,12 +8139,354 @@ function displayConstitutionGuide() {
|
|
|
8139
8139
|
}
|
|
8140
8140
|
|
|
8141
8141
|
// src/cli/commands/migrate.ts
|
|
8142
|
-
import
|
|
8143
|
-
import { promises as
|
|
8142
|
+
import path18 from "path";
|
|
8143
|
+
import { promises as fs12 } from "fs";
|
|
8144
8144
|
init_errors();
|
|
8145
8145
|
init_fs();
|
|
8146
8146
|
init_new();
|
|
8147
8147
|
init_schemas();
|
|
8148
|
+
|
|
8149
|
+
// src/core/migrate/detector.ts
|
|
8150
|
+
init_types();
|
|
8151
|
+
init_errors();
|
|
8152
|
+
init_fs();
|
|
8153
|
+
import path17 from "path";
|
|
8154
|
+
import fs11 from "fs/promises";
|
|
8155
|
+
async function detectExternalTools(projectRoot) {
|
|
8156
|
+
try {
|
|
8157
|
+
const results = [];
|
|
8158
|
+
const openspecResult = await detectOpenSpec(projectRoot);
|
|
8159
|
+
if (openspecResult) {
|
|
8160
|
+
results.push(openspecResult);
|
|
8161
|
+
}
|
|
8162
|
+
const speckitResult = await detectSpecKit(projectRoot);
|
|
8163
|
+
if (speckitResult) {
|
|
8164
|
+
results.push(speckitResult);
|
|
8165
|
+
}
|
|
8166
|
+
const sddResult = await detectSdd(projectRoot);
|
|
8167
|
+
if (sddResult) {
|
|
8168
|
+
results.push(sddResult);
|
|
8169
|
+
}
|
|
8170
|
+
return success(results);
|
|
8171
|
+
} catch (error2) {
|
|
8172
|
+
return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
async function detectOpenSpec(projectRoot) {
|
|
8176
|
+
const openspecPath = path17.join(projectRoot, "openspec");
|
|
8177
|
+
if (!await directoryExists(openspecPath)) {
|
|
8178
|
+
return null;
|
|
8179
|
+
}
|
|
8180
|
+
const specsPath = path17.join(openspecPath, "specs");
|
|
8181
|
+
const changesPath = path17.join(openspecPath, "changes");
|
|
8182
|
+
const agentsPath = path17.join(openspecPath, "AGENTS.md");
|
|
8183
|
+
const hasAgents = await fileExists(agentsPath);
|
|
8184
|
+
const hasSpecs = await directoryExists(specsPath);
|
|
8185
|
+
const hasChanges = await directoryExists(changesPath);
|
|
8186
|
+
if (!hasSpecs && !hasChanges && !hasAgents) {
|
|
8187
|
+
return null;
|
|
8188
|
+
}
|
|
8189
|
+
const specs = [];
|
|
8190
|
+
if (hasSpecs) {
|
|
8191
|
+
const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
|
|
8192
|
+
for (const entry of specDirs) {
|
|
8193
|
+
if (entry.isDirectory()) {
|
|
8194
|
+
const specPath = path17.join(specsPath, entry.name);
|
|
8195
|
+
const specFile = path17.join(specPath, "spec.md");
|
|
8196
|
+
if (await fileExists(specFile)) {
|
|
8197
|
+
const content = await fs11.readFile(specFile, "utf-8");
|
|
8198
|
+
const title2 = extractTitle2(content);
|
|
8199
|
+
const status = extractFrontmatterField(content, "status");
|
|
8200
|
+
specs.push({
|
|
8201
|
+
id: entry.name,
|
|
8202
|
+
title: title2,
|
|
8203
|
+
path: specPath,
|
|
8204
|
+
status
|
|
8205
|
+
});
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
return {
|
|
8211
|
+
tool: "openspec",
|
|
8212
|
+
path: openspecPath,
|
|
8213
|
+
specCount: specs.length,
|
|
8214
|
+
specs,
|
|
8215
|
+
confidence: hasAgents ? "high" : hasSpecs && hasChanges ? "medium" : "low"
|
|
8216
|
+
};
|
|
8217
|
+
}
|
|
8218
|
+
async function detectSpecKit(projectRoot) {
|
|
8219
|
+
const specifyPath = path17.join(projectRoot, ".specify");
|
|
8220
|
+
if (!await directoryExists(specifyPath)) {
|
|
8221
|
+
return null;
|
|
8222
|
+
}
|
|
8223
|
+
const specsPath = path17.join(specifyPath, "specs");
|
|
8224
|
+
const memoryPath = path17.join(projectRoot, "memory");
|
|
8225
|
+
const constitutionPath = path17.join(memoryPath, "constitution.md");
|
|
8226
|
+
const hasSpecs = await directoryExists(specsPath);
|
|
8227
|
+
const hasConstitution = await fileExists(constitutionPath);
|
|
8228
|
+
if (!hasSpecs) {
|
|
8229
|
+
return null;
|
|
8230
|
+
}
|
|
8231
|
+
const specs = [];
|
|
8232
|
+
const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
|
|
8233
|
+
for (const entry of specDirs) {
|
|
8234
|
+
if (entry.isDirectory()) {
|
|
8235
|
+
const specPath = path17.join(specsPath, entry.name);
|
|
8236
|
+
const specFile = path17.join(specPath, "spec.md");
|
|
8237
|
+
const planFile = path17.join(specPath, "plan.md");
|
|
8238
|
+
const tasksFile = path17.join(specPath, "tasks.md");
|
|
8239
|
+
const hasSpec = await fileExists(specFile);
|
|
8240
|
+
const hasPlan = await fileExists(planFile);
|
|
8241
|
+
const hasTasks = await fileExists(tasksFile);
|
|
8242
|
+
if (hasSpec || hasPlan) {
|
|
8243
|
+
let title2;
|
|
8244
|
+
let status;
|
|
8245
|
+
if (hasSpec) {
|
|
8246
|
+
const content = await fs11.readFile(specFile, "utf-8");
|
|
8247
|
+
title2 = extractTitle2(content);
|
|
8248
|
+
status = extractFrontmatterField(content, "status");
|
|
8249
|
+
}
|
|
8250
|
+
specs.push({
|
|
8251
|
+
id: entry.name,
|
|
8252
|
+
title: title2,
|
|
8253
|
+
path: specPath,
|
|
8254
|
+
status: hasTasks ? "in-progress" : status
|
|
8255
|
+
});
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
return {
|
|
8260
|
+
tool: "speckit",
|
|
8261
|
+
path: specifyPath,
|
|
8262
|
+
specCount: specs.length,
|
|
8263
|
+
specs,
|
|
8264
|
+
confidence: hasConstitution ? "high" : "medium"
|
|
8265
|
+
};
|
|
8266
|
+
}
|
|
8267
|
+
async function detectSdd(projectRoot) {
|
|
8268
|
+
const sddPath = path17.join(projectRoot, ".sdd");
|
|
8269
|
+
if (!await directoryExists(sddPath)) {
|
|
8270
|
+
return null;
|
|
8271
|
+
}
|
|
8272
|
+
const specsPath = path17.join(sddPath, "specs");
|
|
8273
|
+
const configPath = path17.join(sddPath, "config.yaml");
|
|
8274
|
+
if (!await directoryExists(specsPath)) {
|
|
8275
|
+
return null;
|
|
8276
|
+
}
|
|
8277
|
+
const specs = [];
|
|
8278
|
+
const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
|
|
8279
|
+
for (const entry of specDirs) {
|
|
8280
|
+
if (entry.isDirectory()) {
|
|
8281
|
+
const specPath = path17.join(specsPath, entry.name);
|
|
8282
|
+
const specFile = path17.join(specPath, "spec.md");
|
|
8283
|
+
if (await fileExists(specFile)) {
|
|
8284
|
+
const content = await fs11.readFile(specFile, "utf-8");
|
|
8285
|
+
const title2 = extractTitle2(content);
|
|
8286
|
+
const status = extractFrontmatterField(content, "status");
|
|
8287
|
+
specs.push({
|
|
8288
|
+
id: entry.name,
|
|
8289
|
+
title: title2,
|
|
8290
|
+
path: specPath,
|
|
8291
|
+
status
|
|
8292
|
+
});
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
return {
|
|
8297
|
+
tool: "sdd",
|
|
8298
|
+
path: sddPath,
|
|
8299
|
+
specCount: specs.length,
|
|
8300
|
+
specs,
|
|
8301
|
+
confidence: await fileExists(configPath) ? "high" : "medium"
|
|
8302
|
+
};
|
|
8303
|
+
}
|
|
8304
|
+
async function migrateFromOpenSpec(sourcePath, targetPath, options = {}) {
|
|
8305
|
+
try {
|
|
8306
|
+
const specsPath = path17.join(sourcePath, "specs");
|
|
8307
|
+
const targetSpecsPath = path17.join(targetPath, "specs");
|
|
8308
|
+
let specsCreated = 0;
|
|
8309
|
+
let specsSkipped = 0;
|
|
8310
|
+
const errors = [];
|
|
8311
|
+
if (!await directoryExists(specsPath)) {
|
|
8312
|
+
return failure(new ChangeError("OpenSpec specs \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
8313
|
+
}
|
|
8314
|
+
const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
|
|
8315
|
+
for (const entry of specDirs) {
|
|
8316
|
+
if (!entry.isDirectory()) continue;
|
|
8317
|
+
const sourceSpecPath = path17.join(specsPath, entry.name);
|
|
8318
|
+
const targetSpecPath = path17.join(targetSpecsPath, entry.name);
|
|
8319
|
+
if (await directoryExists(targetSpecPath)) {
|
|
8320
|
+
if (!options.overwrite) {
|
|
8321
|
+
specsSkipped++;
|
|
8322
|
+
continue;
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
try {
|
|
8326
|
+
if (!options.dryRun) {
|
|
8327
|
+
await fs11.mkdir(targetSpecPath, { recursive: true });
|
|
8328
|
+
const files = await fs11.readdir(sourceSpecPath);
|
|
8329
|
+
for (const file of files) {
|
|
8330
|
+
const sourceFile = path17.join(sourceSpecPath, file);
|
|
8331
|
+
const targetFile = path17.join(targetSpecPath, file);
|
|
8332
|
+
const stat = await fs11.stat(sourceFile);
|
|
8333
|
+
if (stat.isFile()) {
|
|
8334
|
+
let content = await fs11.readFile(sourceFile, "utf-8");
|
|
8335
|
+
if (file === "spec.md") {
|
|
8336
|
+
content = convertOpenSpecToSdd(content, entry.name);
|
|
8337
|
+
}
|
|
8338
|
+
await fs11.writeFile(targetFile, content);
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
}
|
|
8342
|
+
specsCreated++;
|
|
8343
|
+
} catch (error2) {
|
|
8344
|
+
errors.push(`${entry.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
8345
|
+
}
|
|
8346
|
+
}
|
|
8347
|
+
return success({
|
|
8348
|
+
source: "openspec",
|
|
8349
|
+
targetPath,
|
|
8350
|
+
specsCreated,
|
|
8351
|
+
specsSkipped,
|
|
8352
|
+
errors
|
|
8353
|
+
});
|
|
8354
|
+
} catch (error2) {
|
|
8355
|
+
return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
|
|
8356
|
+
}
|
|
8357
|
+
}
|
|
8358
|
+
async function migrateFromSpecKit(sourcePath, targetPath, options = {}) {
|
|
8359
|
+
try {
|
|
8360
|
+
const specsPath = path17.join(sourcePath, "specs");
|
|
8361
|
+
const targetSpecsPath = path17.join(targetPath, "specs");
|
|
8362
|
+
let specsCreated = 0;
|
|
8363
|
+
let specsSkipped = 0;
|
|
8364
|
+
const errors = [];
|
|
8365
|
+
if (!await directoryExists(specsPath)) {
|
|
8366
|
+
return failure(new ChangeError("Spec Kit specs \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
8367
|
+
}
|
|
8368
|
+
const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
|
|
8369
|
+
for (const entry of specDirs) {
|
|
8370
|
+
if (!entry.isDirectory()) continue;
|
|
8371
|
+
const sourceSpecPath = path17.join(specsPath, entry.name);
|
|
8372
|
+
const targetSpecPath = path17.join(targetSpecsPath, entry.name);
|
|
8373
|
+
if (await directoryExists(targetSpecPath)) {
|
|
8374
|
+
if (!options.overwrite) {
|
|
8375
|
+
specsSkipped++;
|
|
8376
|
+
continue;
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
try {
|
|
8380
|
+
if (!options.dryRun) {
|
|
8381
|
+
await fs11.mkdir(targetSpecPath, { recursive: true });
|
|
8382
|
+
const files = await fs11.readdir(sourceSpecPath);
|
|
8383
|
+
for (const file of files) {
|
|
8384
|
+
const sourceFile = path17.join(sourceSpecPath, file);
|
|
8385
|
+
const targetFile = path17.join(targetSpecPath, file);
|
|
8386
|
+
const stat = await fs11.stat(sourceFile);
|
|
8387
|
+
if (stat.isFile()) {
|
|
8388
|
+
let content = await fs11.readFile(sourceFile, "utf-8");
|
|
8389
|
+
if (file === "spec.md") {
|
|
8390
|
+
content = convertSpecKitToSdd(content, entry.name);
|
|
8391
|
+
}
|
|
8392
|
+
await fs11.writeFile(targetFile, content);
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
specsCreated++;
|
|
8397
|
+
} catch (error2) {
|
|
8398
|
+
errors.push(`${entry.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
return success({
|
|
8402
|
+
source: "speckit",
|
|
8403
|
+
targetPath,
|
|
8404
|
+
specsCreated,
|
|
8405
|
+
specsSkipped,
|
|
8406
|
+
errors
|
|
8407
|
+
});
|
|
8408
|
+
} catch (error2) {
|
|
8409
|
+
return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
function convertOpenSpecToSdd(content, specId) {
|
|
8413
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8414
|
+
if (!frontmatterMatch) {
|
|
8415
|
+
const title2 = extractTitle2(content) || specId;
|
|
8416
|
+
return `---
|
|
8417
|
+
id: ${specId}
|
|
8418
|
+
title: ${title2}
|
|
8419
|
+
phase: migrated
|
|
8420
|
+
status: draft
|
|
8421
|
+
source: openspec
|
|
8422
|
+
migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
8423
|
+
---
|
|
8424
|
+
|
|
8425
|
+
${content}`;
|
|
8426
|
+
}
|
|
8427
|
+
let newFrontmatter = frontmatterMatch[1];
|
|
8428
|
+
if (!newFrontmatter.includes("phase:")) {
|
|
8429
|
+
newFrontmatter += "\nphase: migrated";
|
|
8430
|
+
}
|
|
8431
|
+
if (!newFrontmatter.includes("source:")) {
|
|
8432
|
+
newFrontmatter += "\nsource: openspec";
|
|
8433
|
+
}
|
|
8434
|
+
if (!newFrontmatter.includes("migrated_at:")) {
|
|
8435
|
+
newFrontmatter += `
|
|
8436
|
+
migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
8437
|
+
}
|
|
8438
|
+
return content.replace(/^---\n[\s\S]*?\n---/, `---
|
|
8439
|
+
${newFrontmatter}
|
|
8440
|
+
---`);
|
|
8441
|
+
}
|
|
8442
|
+
function convertSpecKitToSdd(content, specId) {
|
|
8443
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8444
|
+
if (!frontmatterMatch) {
|
|
8445
|
+
const title2 = extractTitle2(content) || specId;
|
|
8446
|
+
return `---
|
|
8447
|
+
id: ${specId}
|
|
8448
|
+
title: ${title2}
|
|
8449
|
+
phase: migrated
|
|
8450
|
+
status: draft
|
|
8451
|
+
source: speckit
|
|
8452
|
+
migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
8453
|
+
---
|
|
8454
|
+
|
|
8455
|
+
${content}`;
|
|
8456
|
+
}
|
|
8457
|
+
let newFrontmatter = frontmatterMatch[1];
|
|
8458
|
+
if (!newFrontmatter.includes("phase:")) {
|
|
8459
|
+
newFrontmatter += "\nphase: migrated";
|
|
8460
|
+
}
|
|
8461
|
+
if (!newFrontmatter.includes("source:")) {
|
|
8462
|
+
newFrontmatter += "\nsource: speckit";
|
|
8463
|
+
}
|
|
8464
|
+
if (!newFrontmatter.includes("migrated_at:")) {
|
|
8465
|
+
newFrontmatter += `
|
|
8466
|
+
migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
8467
|
+
}
|
|
8468
|
+
return content.replace(/^---\n[\s\S]*?\n---/, `---
|
|
8469
|
+
${newFrontmatter}
|
|
8470
|
+
---`);
|
|
8471
|
+
}
|
|
8472
|
+
function extractTitle2(content) {
|
|
8473
|
+
const fmMatch = content.match(/^---\n[\s\S]*?title:\s*['"]?([^'"\n]+)['"]?\n[\s\S]*?\n---/);
|
|
8474
|
+
if (fmMatch) {
|
|
8475
|
+
return fmMatch[1].trim();
|
|
8476
|
+
}
|
|
8477
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
8478
|
+
if (h1Match) {
|
|
8479
|
+
return h1Match[1].trim();
|
|
8480
|
+
}
|
|
8481
|
+
return void 0;
|
|
8482
|
+
}
|
|
8483
|
+
function extractFrontmatterField(content, field) {
|
|
8484
|
+
const regex = new RegExp(`^---\\n[\\s\\S]*?${field}:\\s*['"]?([^'"\\n]+)['"]?\\n[\\s\\S]*?\\n---`);
|
|
8485
|
+
const match = content.match(regex);
|
|
8486
|
+
return match ? match[1].trim() : void 0;
|
|
8487
|
+
}
|
|
8488
|
+
|
|
8489
|
+
// src/cli/commands/migrate.ts
|
|
8148
8490
|
function registerMigrateCommand(program2) {
|
|
8149
8491
|
const migrate = program2.command("migrate").description("\uAE30\uC874 \uBB38\uC11C\uB97C SDD \uD615\uC2DD\uC73C\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4");
|
|
8150
8492
|
migrate.command("docs <source>").description("\uB9C8\uD06C\uB2E4\uC6B4 \uBB38\uC11C\uB97C spec.md \uD615\uC2DD\uC73C\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4").option("-o, --output <dir>", "\uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").action(async (source, options) => {
|
|
@@ -8171,6 +8513,30 @@ function registerMigrateCommand(program2) {
|
|
|
8171
8513
|
process.exit(ExitCode.GENERAL_ERROR);
|
|
8172
8514
|
}
|
|
8173
8515
|
});
|
|
8516
|
+
migrate.command("detect").description("\uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uC678\uBD80 SDD \uB3C4\uAD6C\uB97C \uAC10\uC9C0\uD569\uB2C8\uB2E4").option("-p, --path <path>", "\uAC80\uC0C9 \uACBD\uB85C").action(async (options) => {
|
|
8517
|
+
try {
|
|
8518
|
+
await runDetect(options);
|
|
8519
|
+
} catch (error2) {
|
|
8520
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
8521
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8522
|
+
}
|
|
8523
|
+
});
|
|
8524
|
+
migrate.command("openspec [source]").description("OpenSpec \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").option("--overwrite", "\uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30").action(async (source, options) => {
|
|
8525
|
+
try {
|
|
8526
|
+
await runMigrateOpenSpec(source, options);
|
|
8527
|
+
} catch (error2) {
|
|
8528
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
8529
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8530
|
+
}
|
|
8531
|
+
});
|
|
8532
|
+
migrate.command("speckit [source]").description("Spec Kit \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").option("--overwrite", "\uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30").action(async (source, options) => {
|
|
8533
|
+
try {
|
|
8534
|
+
await runMigrateSpecKit(source, options);
|
|
8535
|
+
} catch (error2) {
|
|
8536
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
8537
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8538
|
+
}
|
|
8539
|
+
});
|
|
8174
8540
|
}
|
|
8175
8541
|
async function runMigrateDocs(source, options) {
|
|
8176
8542
|
const projectRoot = await findSddRoot();
|
|
@@ -8178,10 +8544,10 @@ async function runMigrateDocs(source, options) {
|
|
|
8178
8544
|
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --output \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uAC70\uB098 sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
8179
8545
|
process.exit(ExitCode.GENERAL_ERROR);
|
|
8180
8546
|
}
|
|
8181
|
-
const sourcePath =
|
|
8547
|
+
const sourcePath = path18.resolve(source);
|
|
8182
8548
|
let files = [];
|
|
8183
8549
|
try {
|
|
8184
|
-
const stat = await
|
|
8550
|
+
const stat = await fs12.stat(sourcePath);
|
|
8185
8551
|
if (stat.isDirectory()) {
|
|
8186
8552
|
files = await collectMarkdownFiles(sourcePath);
|
|
8187
8553
|
} else if (stat.isFile()) {
|
|
@@ -8197,7 +8563,7 @@ async function runMigrateDocs(source, options) {
|
|
|
8197
8563
|
}
|
|
8198
8564
|
info(`${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC`);
|
|
8199
8565
|
newline();
|
|
8200
|
-
const outputDir = options.output ?
|
|
8566
|
+
const outputDir = options.output ? path18.resolve(options.output) : path18.join(projectRoot, ".sdd", "specs");
|
|
8201
8567
|
const summary = {
|
|
8202
8568
|
total: files.length,
|
|
8203
8569
|
succeeded: 0,
|
|
@@ -8209,10 +8575,10 @@ async function runMigrateDocs(source, options) {
|
|
|
8209
8575
|
summary.results.push(result);
|
|
8210
8576
|
if (result.success) {
|
|
8211
8577
|
summary.succeeded++;
|
|
8212
|
-
info(`\u2705 ${
|
|
8578
|
+
info(`\u2705 ${path18.basename(file)} \u2192 ${result.target}`);
|
|
8213
8579
|
} else {
|
|
8214
8580
|
summary.failed++;
|
|
8215
|
-
error(`\u274C ${
|
|
8581
|
+
error(`\u274C ${path18.basename(file)}: ${result.error}`);
|
|
8216
8582
|
}
|
|
8217
8583
|
}
|
|
8218
8584
|
newline();
|
|
@@ -8224,25 +8590,25 @@ async function runMigrateDocs(source, options) {
|
|
|
8224
8590
|
}
|
|
8225
8591
|
async function migrateDocument(filePath, outputDir, dryRun) {
|
|
8226
8592
|
try {
|
|
8227
|
-
const content = await
|
|
8593
|
+
const content = await fs12.readFile(filePath, "utf-8");
|
|
8228
8594
|
const analysis = analyzeDocument(content);
|
|
8229
|
-
const featureId = generateFeatureId(analysis.title ||
|
|
8595
|
+
const featureId = generateFeatureId(analysis.title || path18.basename(filePath, ".md"));
|
|
8230
8596
|
const specContent = generateSpec({
|
|
8231
8597
|
id: featureId,
|
|
8232
|
-
title: analysis.title ||
|
|
8598
|
+
title: analysis.title || path18.basename(filePath, ".md"),
|
|
8233
8599
|
description: analysis.description || "",
|
|
8234
8600
|
requirements: analysis.requirements,
|
|
8235
8601
|
scenarios: analysis.scenarios
|
|
8236
8602
|
});
|
|
8237
|
-
const targetDir =
|
|
8238
|
-
const targetPath =
|
|
8603
|
+
const targetDir = path18.join(outputDir, featureId);
|
|
8604
|
+
const targetPath = path18.join(targetDir, "spec.md");
|
|
8239
8605
|
if (!dryRun) {
|
|
8240
8606
|
await ensureDir(targetDir);
|
|
8241
8607
|
await writeFile(targetPath, specContent);
|
|
8242
8608
|
}
|
|
8243
8609
|
return {
|
|
8244
8610
|
source: filePath,
|
|
8245
|
-
target:
|
|
8611
|
+
target: path18.relative(process.cwd(), targetPath),
|
|
8246
8612
|
success: true
|
|
8247
8613
|
};
|
|
8248
8614
|
} catch (error2) {
|
|
@@ -8287,14 +8653,14 @@ function analyzeDocument(content) {
|
|
|
8287
8653
|
};
|
|
8288
8654
|
}
|
|
8289
8655
|
async function runAnalyze(file) {
|
|
8290
|
-
const filePath =
|
|
8656
|
+
const filePath = path18.resolve(file);
|
|
8291
8657
|
if (!await fileExists(filePath)) {
|
|
8292
8658
|
error(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${file}`);
|
|
8293
8659
|
process.exit(ExitCode.FILE_SYSTEM_ERROR);
|
|
8294
8660
|
}
|
|
8295
|
-
const content = await
|
|
8661
|
+
const content = await fs12.readFile(filePath, "utf-8");
|
|
8296
8662
|
const analysis = analyzeDocument(content);
|
|
8297
|
-
info(`\u{1F4CA} \uBB38\uC11C \uBD84\uC11D: ${
|
|
8663
|
+
info(`\u{1F4CA} \uBB38\uC11C \uBD84\uC11D: ${path18.basename(file)}`);
|
|
8298
8664
|
newline();
|
|
8299
8665
|
info(`\uC81C\uBAA9: ${analysis.title || "(\uC5C6\uC74C)"}`);
|
|
8300
8666
|
info(`\uC124\uBA85: ${analysis.description || "(\uC5C6\uC74C)"}`);
|
|
@@ -8337,7 +8703,7 @@ async function runAnalyze(file) {
|
|
|
8337
8703
|
}
|
|
8338
8704
|
}
|
|
8339
8705
|
async function runScan(dir, options) {
|
|
8340
|
-
const dirPath =
|
|
8706
|
+
const dirPath = path18.resolve(dir);
|
|
8341
8707
|
if (!await directoryExists(dirPath)) {
|
|
8342
8708
|
error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
|
|
8343
8709
|
process.exit(ExitCode.FILE_SYSTEM_ERROR);
|
|
@@ -8353,7 +8719,7 @@ async function runScan(dir, options) {
|
|
|
8353
8719
|
const results = [];
|
|
8354
8720
|
for (const file of files) {
|
|
8355
8721
|
try {
|
|
8356
|
-
const content = await
|
|
8722
|
+
const content = await fs12.readFile(file, "utf-8");
|
|
8357
8723
|
const analysis = analyzeDocument(content);
|
|
8358
8724
|
results.push({ file, analysis });
|
|
8359
8725
|
} catch {
|
|
@@ -8368,7 +8734,7 @@ async function runScan(dir, options) {
|
|
|
8368
8734
|
const partial = [];
|
|
8369
8735
|
const notReady = [];
|
|
8370
8736
|
for (const { file, analysis } of results) {
|
|
8371
|
-
const relativePath =
|
|
8737
|
+
const relativePath = path18.relative(process.cwd(), file);
|
|
8372
8738
|
if (analysis.hasRfc2119 && analysis.hasScenarios) {
|
|
8373
8739
|
ready.push(relativePath);
|
|
8374
8740
|
} else if (analysis.hasRfc2119 || analysis.hasScenarios || analysis.requirements.length > 0) {
|
|
@@ -8415,15 +8781,15 @@ async function collectMarkdownFiles(dirPath) {
|
|
|
8415
8781
|
async function collectFilesWithExtensions(dirPath, extensions) {
|
|
8416
8782
|
const files = [];
|
|
8417
8783
|
async function scan(dir) {
|
|
8418
|
-
const entries = await
|
|
8784
|
+
const entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
8419
8785
|
for (const entry of entries) {
|
|
8420
|
-
const fullPath =
|
|
8786
|
+
const fullPath = path18.join(dir, entry.name);
|
|
8421
8787
|
if (entry.isDirectory()) {
|
|
8422
8788
|
if (!["node_modules", ".git", ".sdd", "dist", "build"].includes(entry.name)) {
|
|
8423
8789
|
await scan(fullPath);
|
|
8424
8790
|
}
|
|
8425
8791
|
} else if (entry.isFile()) {
|
|
8426
|
-
const ext =
|
|
8792
|
+
const ext = path18.extname(entry.name).toLowerCase();
|
|
8427
8793
|
if (extensions.some((e) => e.toLowerCase() === ext)) {
|
|
8428
8794
|
if (!["agents.md", "readme.md", "changelog.md", "license.md"].includes(entry.name.toLowerCase())) {
|
|
8429
8795
|
files.push(fullPath);
|
|
@@ -8435,9 +8801,200 @@ async function collectFilesWithExtensions(dirPath, extensions) {
|
|
|
8435
8801
|
await scan(dirPath);
|
|
8436
8802
|
return files;
|
|
8437
8803
|
}
|
|
8804
|
+
async function runDetect(options) {
|
|
8805
|
+
const projectRoot = options.path ? path18.resolve(options.path) : process.cwd();
|
|
8806
|
+
info("\u{1F50D} \uC678\uBD80 SDD \uB3C4\uAD6C \uAC10\uC9C0 \uC911...");
|
|
8807
|
+
info(` \uACBD\uB85C: ${projectRoot}`);
|
|
8808
|
+
newline();
|
|
8809
|
+
const result = await detectExternalTools(projectRoot);
|
|
8810
|
+
if (!result.success) {
|
|
8811
|
+
error(result.error.message);
|
|
8812
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8813
|
+
}
|
|
8814
|
+
const tools = result.data;
|
|
8815
|
+
if (tools.length === 0) {
|
|
8816
|
+
info("\uAC10\uC9C0\uB41C \uC678\uBD80 SDD \uB3C4\uAD6C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
8817
|
+
return;
|
|
8818
|
+
}
|
|
8819
|
+
for (const tool of tools) {
|
|
8820
|
+
const icon = getToolIcon(tool.tool);
|
|
8821
|
+
const confidence = getConfidenceLabel(tool.confidence);
|
|
8822
|
+
info(`${icon} ${getToolName(tool.tool)}`);
|
|
8823
|
+
info(` \uACBD\uB85C: ${tool.path}`);
|
|
8824
|
+
info(` \uC2E0\uB8B0\uB3C4: ${confidence}`);
|
|
8825
|
+
info(` \uC2A4\uD399 \uC218: ${tool.specCount}\uAC1C`);
|
|
8826
|
+
if (tool.specs.length > 0) {
|
|
8827
|
+
newline();
|
|
8828
|
+
info(" \uBC1C\uACAC\uB41C \uC2A4\uD399:");
|
|
8829
|
+
for (const spec of tool.specs.slice(0, 5)) {
|
|
8830
|
+
const status = spec.status ? ` [${spec.status}]` : "";
|
|
8831
|
+
listItem(`${spec.id}: ${spec.title || "(\uC81C\uBAA9 \uC5C6\uC74C)"}${status}`, 2);
|
|
8832
|
+
}
|
|
8833
|
+
if (tool.specs.length > 5) {
|
|
8834
|
+
info(` ... \uC678 ${tool.specs.length - 5}\uAC1C`);
|
|
8835
|
+
}
|
|
8836
|
+
}
|
|
8837
|
+
newline();
|
|
8838
|
+
}
|
|
8839
|
+
const openspec = tools.find((t) => t.tool === "openspec");
|
|
8840
|
+
const speckit = tools.find((t) => t.tool === "speckit");
|
|
8841
|
+
if (openspec || speckit) {
|
|
8842
|
+
info("\u{1F4A1} \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uBA85\uB839\uC5B4:");
|
|
8843
|
+
if (openspec) {
|
|
8844
|
+
listItem(`sdd migrate openspec "${openspec.path}"`, 1);
|
|
8845
|
+
}
|
|
8846
|
+
if (speckit) {
|
|
8847
|
+
listItem(`sdd migrate speckit "${speckit.path}"`, 1);
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
async function runMigrateOpenSpec(source, options) {
|
|
8852
|
+
const projectRoot = await findSddRoot();
|
|
8853
|
+
if (!projectRoot) {
|
|
8854
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
8855
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8856
|
+
}
|
|
8857
|
+
let sourcePath;
|
|
8858
|
+
if (source) {
|
|
8859
|
+
sourcePath = path18.resolve(source);
|
|
8860
|
+
} else {
|
|
8861
|
+
const detectResult = await detectExternalTools(projectRoot);
|
|
8862
|
+
if (!detectResult.success) {
|
|
8863
|
+
error(detectResult.error.message);
|
|
8864
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8865
|
+
}
|
|
8866
|
+
const openspec = detectResult.data.find((t) => t.tool === "openspec");
|
|
8867
|
+
if (!openspec) {
|
|
8868
|
+
error("OpenSpec \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.");
|
|
8869
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8870
|
+
}
|
|
8871
|
+
sourcePath = openspec.path;
|
|
8872
|
+
}
|
|
8873
|
+
const sddPath = path18.join(projectRoot, ".sdd");
|
|
8874
|
+
info("\u{1F504} OpenSpec\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC911...");
|
|
8875
|
+
info(` \uC18C\uC2A4: ${sourcePath}`);
|
|
8876
|
+
info(` \uB300\uC0C1: ${sddPath}`);
|
|
8877
|
+
if (options.dryRun) {
|
|
8878
|
+
warn(" (dry-run \uBAA8\uB4DC)");
|
|
8879
|
+
}
|
|
8880
|
+
newline();
|
|
8881
|
+
const result = await migrateFromOpenSpec(sourcePath, sddPath, {
|
|
8882
|
+
dryRun: options.dryRun,
|
|
8883
|
+
overwrite: options.overwrite
|
|
8884
|
+
});
|
|
8885
|
+
if (!result.success) {
|
|
8886
|
+
error(result.error.message);
|
|
8887
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8888
|
+
}
|
|
8889
|
+
const data = result.data;
|
|
8890
|
+
success2("\u2705 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC");
|
|
8891
|
+
info(` \uC0DD\uC131: ${data.specsCreated}\uAC1C`);
|
|
8892
|
+
info(` \uC2A4\uD0B5: ${data.specsSkipped}\uAC1C`);
|
|
8893
|
+
if (data.errors.length > 0) {
|
|
8894
|
+
newline();
|
|
8895
|
+
warn("\u26A0\uFE0F \uC77C\uBD80 \uC624\uB958 \uBC1C\uC0DD:");
|
|
8896
|
+
for (const error2 of data.errors) {
|
|
8897
|
+
error(` - ${error2}`);
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
if (options.dryRun) {
|
|
8901
|
+
newline();
|
|
8902
|
+
info("\uC2E4\uC81C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD558\uB824\uBA74 --dry-run \uC635\uC158\uC744 \uC81C\uAC70\uD558\uC138\uC694.");
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
async function runMigrateSpecKit(source, options) {
|
|
8906
|
+
const projectRoot = await findSddRoot();
|
|
8907
|
+
if (!projectRoot) {
|
|
8908
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
8909
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8910
|
+
}
|
|
8911
|
+
let sourcePath;
|
|
8912
|
+
if (source) {
|
|
8913
|
+
sourcePath = path18.resolve(source);
|
|
8914
|
+
} else {
|
|
8915
|
+
const detectResult = await detectExternalTools(projectRoot);
|
|
8916
|
+
if (!detectResult.success) {
|
|
8917
|
+
error(detectResult.error.message);
|
|
8918
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8919
|
+
}
|
|
8920
|
+
const speckit = detectResult.data.find((t) => t.tool === "speckit");
|
|
8921
|
+
if (!speckit) {
|
|
8922
|
+
error("Spec Kit \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.");
|
|
8923
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8924
|
+
}
|
|
8925
|
+
sourcePath = speckit.path;
|
|
8926
|
+
}
|
|
8927
|
+
const sddPath = path18.join(projectRoot, ".sdd");
|
|
8928
|
+
info("\u{1F504} Spec Kit\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC911...");
|
|
8929
|
+
info(` \uC18C\uC2A4: ${sourcePath}`);
|
|
8930
|
+
info(` \uB300\uC0C1: ${sddPath}`);
|
|
8931
|
+
if (options.dryRun) {
|
|
8932
|
+
warn(" (dry-run \uBAA8\uB4DC)");
|
|
8933
|
+
}
|
|
8934
|
+
newline();
|
|
8935
|
+
const result = await migrateFromSpecKit(sourcePath, sddPath, {
|
|
8936
|
+
dryRun: options.dryRun,
|
|
8937
|
+
overwrite: options.overwrite
|
|
8938
|
+
});
|
|
8939
|
+
if (!result.success) {
|
|
8940
|
+
error(result.error.message);
|
|
8941
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
8942
|
+
}
|
|
8943
|
+
const data = result.data;
|
|
8944
|
+
success2("\u2705 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC");
|
|
8945
|
+
info(` \uC0DD\uC131: ${data.specsCreated}\uAC1C`);
|
|
8946
|
+
info(` \uC2A4\uD0B5: ${data.specsSkipped}\uAC1C`);
|
|
8947
|
+
if (data.errors.length > 0) {
|
|
8948
|
+
newline();
|
|
8949
|
+
warn("\u26A0\uFE0F \uC77C\uBD80 \uC624\uB958 \uBC1C\uC0DD:");
|
|
8950
|
+
for (const error2 of data.errors) {
|
|
8951
|
+
error(` - ${error2}`);
|
|
8952
|
+
}
|
|
8953
|
+
}
|
|
8954
|
+
if (options.dryRun) {
|
|
8955
|
+
newline();
|
|
8956
|
+
info("\uC2E4\uC81C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD558\uB824\uBA74 --dry-run \uC635\uC158\uC744 \uC81C\uAC70\uD558\uC138\uC694.");
|
|
8957
|
+
}
|
|
8958
|
+
}
|
|
8959
|
+
function getToolIcon(tool) {
|
|
8960
|
+
switch (tool) {
|
|
8961
|
+
case "openspec":
|
|
8962
|
+
return "\u{1F4E6}";
|
|
8963
|
+
case "speckit":
|
|
8964
|
+
return "\u{1F527}";
|
|
8965
|
+
case "sdd":
|
|
8966
|
+
return "\u{1F4CB}";
|
|
8967
|
+
default:
|
|
8968
|
+
return "\u2753";
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
function getToolName(tool) {
|
|
8972
|
+
switch (tool) {
|
|
8973
|
+
case "openspec":
|
|
8974
|
+
return "OpenSpec";
|
|
8975
|
+
case "speckit":
|
|
8976
|
+
return "Spec Kit";
|
|
8977
|
+
case "sdd":
|
|
8978
|
+
return "SDD";
|
|
8979
|
+
default:
|
|
8980
|
+
return tool;
|
|
8981
|
+
}
|
|
8982
|
+
}
|
|
8983
|
+
function getConfidenceLabel(confidence) {
|
|
8984
|
+
switch (confidence) {
|
|
8985
|
+
case "high":
|
|
8986
|
+
return "\uB192\uC74C \u2713";
|
|
8987
|
+
case "medium":
|
|
8988
|
+
return "\uC911\uAC04";
|
|
8989
|
+
case "low":
|
|
8990
|
+
return "\uB0AE\uC74C";
|
|
8991
|
+
default:
|
|
8992
|
+
return confidence;
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8438
8995
|
|
|
8439
8996
|
// src/cli/commands/cicd.ts
|
|
8440
|
-
import
|
|
8997
|
+
import path19 from "path";
|
|
8441
8998
|
init_errors();
|
|
8442
8999
|
init_fs();
|
|
8443
9000
|
function registerCicdCommand(program2) {
|
|
@@ -8489,16 +9046,16 @@ async function runSetup(platform, options) {
|
|
|
8489
9046
|
listItem("PR/MR \uC0DD\uC131 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2A4\uD399 \uAC80\uC99D\uC774 \uC2E4\uD589\uB429\uB2C8\uB2E4");
|
|
8490
9047
|
}
|
|
8491
9048
|
async function setupGitHubActions(projectRoot, strict) {
|
|
8492
|
-
const workflowDir =
|
|
9049
|
+
const workflowDir = path19.join(projectRoot, ".github", "workflows");
|
|
8493
9050
|
await ensureDir(workflowDir);
|
|
8494
9051
|
const workflowContent = generateGitHubWorkflow(strict);
|
|
8495
|
-
const workflowPath =
|
|
9052
|
+
const workflowPath = path19.join(workflowDir, "sdd-validate.yml");
|
|
8496
9053
|
await writeFile(workflowPath, workflowContent);
|
|
8497
9054
|
info(`\u2705 GitHub Actions \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0DD\uC131: .github/workflows/sdd-validate.yml`);
|
|
8498
9055
|
}
|
|
8499
9056
|
async function setupGitLabCI(projectRoot, strict) {
|
|
8500
9057
|
const ciContent = generateGitLabCI(strict);
|
|
8501
|
-
const ciPath =
|
|
9058
|
+
const ciPath = path19.join(projectRoot, ".gitlab-ci-sdd.yml");
|
|
8502
9059
|
await writeFile(ciPath, ciContent);
|
|
8503
9060
|
info(`\u2705 GitLab CI \uAD6C\uC131 \uC0DD\uC131: .gitlab-ci-sdd.yml`);
|
|
8504
9061
|
info(" (\uAE30\uC874 .gitlab-ci.yml\uC5D0 include\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC138\uC694)");
|
|
@@ -8594,7 +9151,7 @@ async function runHooksSetup(type, options) {
|
|
|
8594
9151
|
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
8595
9152
|
process.exit(ExitCode.GENERAL_ERROR);
|
|
8596
9153
|
}
|
|
8597
|
-
const hooksDir =
|
|
9154
|
+
const hooksDir = path19.join(projectRoot, ".husky");
|
|
8598
9155
|
if (options.install) {
|
|
8599
9156
|
info("husky \uC124\uCE58 \uBC29\uBC95:");
|
|
8600
9157
|
newline();
|
|
@@ -8608,7 +9165,7 @@ async function runHooksSetup(type, options) {
|
|
|
8608
9165
|
const hooks = type ? [type] : ["pre-commit", "pre-push"];
|
|
8609
9166
|
for (const hook of hooks) {
|
|
8610
9167
|
const hookContent = generateHookScript(hook);
|
|
8611
|
-
const hookPath =
|
|
9168
|
+
const hookPath = path19.join(hooksDir, hook);
|
|
8612
9169
|
await writeFile(hookPath, hookContent);
|
|
8613
9170
|
info(`\u2705 ${hook} \uD6C5 \uC0DD\uC131: .husky/${hook}`);
|
|
8614
9171
|
}
|
|
@@ -8690,7 +9247,7 @@ async function runCiCheck(options) {
|
|
|
8690
9247
|
let hasErrors = false;
|
|
8691
9248
|
let hasWarnings = false;
|
|
8692
9249
|
info("1. Constitution \uAC80\uC99D...");
|
|
8693
|
-
const constitutionPath =
|
|
9250
|
+
const constitutionPath = path19.join(projectRoot, ".sdd", "constitution.md");
|
|
8694
9251
|
if (await fileExists(constitutionPath)) {
|
|
8695
9252
|
info(" \u2705 constitution.md \uC874\uC7AC");
|
|
8696
9253
|
} else {
|
|
@@ -8698,7 +9255,7 @@ async function runCiCheck(options) {
|
|
|
8698
9255
|
hasWarnings = true;
|
|
8699
9256
|
}
|
|
8700
9257
|
info("2. \uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC \uD655\uC778...");
|
|
8701
|
-
const specsPath =
|
|
9258
|
+
const specsPath = path19.join(projectRoot, ".sdd", "specs");
|
|
8702
9259
|
if (await directoryExists(specsPath)) {
|
|
8703
9260
|
info(" \u2705 specs/ \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC");
|
|
8704
9261
|
} else {
|
|
@@ -8708,7 +9265,7 @@ async function runCiCheck(options) {
|
|
|
8708
9265
|
info("3. \uAE30\uBCF8 \uAD6C\uC870 \uD655\uC778...");
|
|
8709
9266
|
const requiredDirs = ["changes", "archive", "templates"];
|
|
8710
9267
|
for (const dir of requiredDirs) {
|
|
8711
|
-
const dirPath =
|
|
9268
|
+
const dirPath = path19.join(projectRoot, ".sdd", dir);
|
|
8712
9269
|
if (await directoryExists(dirPath)) {
|
|
8713
9270
|
info(` \u2705 ${dir}/ \uC874\uC7AC`);
|
|
8714
9271
|
} else {
|
|
@@ -8736,7 +9293,7 @@ async function runCiCheck(options) {
|
|
|
8736
9293
|
}
|
|
8737
9294
|
|
|
8738
9295
|
// src/cli/commands/transition.ts
|
|
8739
|
-
import
|
|
9296
|
+
import path20 from "path";
|
|
8740
9297
|
init_errors();
|
|
8741
9298
|
init_fs();
|
|
8742
9299
|
function registerTransitionCommand(program2) {
|
|
@@ -8767,9 +9324,9 @@ async function runNewToChange(specId, options) {
|
|
|
8767
9324
|
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
8768
9325
|
process.exit(ExitCode.GENERAL_ERROR);
|
|
8769
9326
|
}
|
|
8770
|
-
const sddPath =
|
|
8771
|
-
const specsPath =
|
|
8772
|
-
const specPath =
|
|
9327
|
+
const sddPath = path20.join(projectRoot, ".sdd");
|
|
9328
|
+
const specsPath = path20.join(sddPath, "specs");
|
|
9329
|
+
const specPath = path20.join(specsPath, specId, "spec.md");
|
|
8773
9330
|
if (!await fileExists(specPath)) {
|
|
8774
9331
|
error(`\uC2A4\uD399\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specId}`);
|
|
8775
9332
|
info("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC2A4\uD399 \uBAA9\uB85D\uC740 `sdd list`\uB85C \uD655\uC778\uD558\uC138\uC694.");
|
|
@@ -8779,7 +9336,7 @@ async function runNewToChange(specId, options) {
|
|
|
8779
9336
|
newline();
|
|
8780
9337
|
info(`\uB300\uC0C1 \uC2A4\uD399: ${specId}`);
|
|
8781
9338
|
const changeId = generateChangeId();
|
|
8782
|
-
const changePath =
|
|
9339
|
+
const changePath = path20.join(sddPath, "changes", changeId);
|
|
8783
9340
|
await ensureDir(changePath);
|
|
8784
9341
|
const specContent = await readFile(specPath);
|
|
8785
9342
|
if (!specContent.success) {
|
|
@@ -8789,11 +9346,11 @@ async function runNewToChange(specId, options) {
|
|
|
8789
9346
|
const title2 = options.title || `${specId} \uAE30\uB2A5 \uD655\uC7A5`;
|
|
8790
9347
|
const reason = options.reason || "new \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
|
|
8791
9348
|
const proposalContent = generateTransitionProposal(specId, title2, reason, "new-to-change");
|
|
8792
|
-
await writeFile(
|
|
9349
|
+
await writeFile(path20.join(changePath, "proposal.md"), proposalContent);
|
|
8793
9350
|
const deltaContent = generateDeltaTemplate(specId);
|
|
8794
|
-
await writeFile(
|
|
9351
|
+
await writeFile(path20.join(changePath, "delta.md"), deltaContent);
|
|
8795
9352
|
const tasksContent = generateTasksTemplate();
|
|
8796
|
-
await writeFile(
|
|
9353
|
+
await writeFile(path20.join(changePath, "tasks.md"), tasksContent);
|
|
8797
9354
|
newline();
|
|
8798
9355
|
success2(`\uC804\uD658 \uC644\uB8CC! \uBCC0\uACBD \uC81C\uC548\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
|
|
8799
9356
|
newline();
|
|
@@ -8815,8 +9372,8 @@ async function runChangeToNew(changeId, options) {
|
|
|
8815
9372
|
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
8816
9373
|
process.exit(ExitCode.GENERAL_ERROR);
|
|
8817
9374
|
}
|
|
8818
|
-
const sddPath =
|
|
8819
|
-
const changePath =
|
|
9375
|
+
const sddPath = path20.join(projectRoot, ".sdd");
|
|
9376
|
+
const changePath = path20.join(sddPath, "changes", changeId);
|
|
8820
9377
|
if (!await directoryExists(changePath)) {
|
|
8821
9378
|
error(`\uBCC0\uACBD \uC81C\uC548\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${changeId}`);
|
|
8822
9379
|
info("\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D\uC740 `sdd change -l`\uB85C \uD655\uC778\uD558\uC138\uC694.");
|
|
@@ -8825,7 +9382,7 @@ async function runChangeToNew(changeId, options) {
|
|
|
8825
9382
|
info("=== \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC804\uD658: change \u2192 new ===");
|
|
8826
9383
|
newline();
|
|
8827
9384
|
info(`\uC6D0\uBCF8 \uBCC0\uACBD: ${changeId}`);
|
|
8828
|
-
const proposalPath =
|
|
9385
|
+
const proposalPath = path20.join(changePath, "proposal.md");
|
|
8829
9386
|
let extractedTitle = "";
|
|
8830
9387
|
if (await fileExists(proposalPath)) {
|
|
8831
9388
|
const proposalContent = await readFile(proposalPath);
|
|
@@ -8837,8 +9394,8 @@ async function runChangeToNew(changeId, options) {
|
|
|
8837
9394
|
}
|
|
8838
9395
|
}
|
|
8839
9396
|
const featureName = options.name || extractedTitle.toLowerCase().replace(/\s+/g, "-") || `feature-from-${changeId}`;
|
|
8840
|
-
const specsPath =
|
|
8841
|
-
const newSpecPath =
|
|
9397
|
+
const specsPath = path20.join(sddPath, "specs");
|
|
9398
|
+
const newSpecPath = path20.join(specsPath, featureName);
|
|
8842
9399
|
if (await directoryExists(newSpecPath)) {
|
|
8843
9400
|
error(`\uC2A4\uD399\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${featureName}`);
|
|
8844
9401
|
info("\uB2E4\uB978 \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694: --name <name>");
|
|
@@ -8847,12 +9404,12 @@ async function runChangeToNew(changeId, options) {
|
|
|
8847
9404
|
await ensureDir(newSpecPath);
|
|
8848
9405
|
const reason = options.reason || "change \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
|
|
8849
9406
|
const specContent = generateTransitionSpec(featureName, extractedTitle || featureName, reason, changeId);
|
|
8850
|
-
await writeFile(
|
|
9407
|
+
await writeFile(path20.join(newSpecPath, "spec.md"), specContent);
|
|
8851
9408
|
const planContent = generatePlanTemplate(featureName);
|
|
8852
|
-
await writeFile(
|
|
9409
|
+
await writeFile(path20.join(newSpecPath, "plan.md"), planContent);
|
|
8853
9410
|
const tasksContent = generateTasksTemplate();
|
|
8854
|
-
await writeFile(
|
|
8855
|
-
const statusPath =
|
|
9411
|
+
await writeFile(path20.join(newSpecPath, "tasks.md"), tasksContent);
|
|
9412
|
+
const statusPath = path20.join(changePath, ".status");
|
|
8856
9413
|
await writeFile(statusPath, JSON.stringify({
|
|
8857
9414
|
status: "transitioned",
|
|
8858
9415
|
transitionedTo: featureName,
|
|
@@ -9086,6 +9643,1135 @@ status: pending
|
|
|
9086
9643
|
`;
|
|
9087
9644
|
}
|
|
9088
9645
|
|
|
9646
|
+
// src/cli/commands/watch.ts
|
|
9647
|
+
import path22 from "path";
|
|
9648
|
+
|
|
9649
|
+
// src/core/watch/watcher.ts
|
|
9650
|
+
import chokidar from "chokidar";
|
|
9651
|
+
import path21 from "path";
|
|
9652
|
+
import { EventEmitter } from "events";
|
|
9653
|
+
var SpecWatcher = class extends EventEmitter {
|
|
9654
|
+
watcher = null;
|
|
9655
|
+
specsPath;
|
|
9656
|
+
debounceMs;
|
|
9657
|
+
debounceTimer = null;
|
|
9658
|
+
pendingEvents = [];
|
|
9659
|
+
isRunning = false;
|
|
9660
|
+
constructor(options) {
|
|
9661
|
+
super();
|
|
9662
|
+
this.specsPath = options.specsPath;
|
|
9663
|
+
this.debounceMs = options.debounceMs ?? 500;
|
|
9664
|
+
}
|
|
9665
|
+
/**
|
|
9666
|
+
* 감시 시작
|
|
9667
|
+
*/
|
|
9668
|
+
start() {
|
|
9669
|
+
if (this.isRunning) {
|
|
9670
|
+
return;
|
|
9671
|
+
}
|
|
9672
|
+
const ignored = [
|
|
9673
|
+
"**/node_modules/**",
|
|
9674
|
+
"**/.git/**",
|
|
9675
|
+
"**/.*"
|
|
9676
|
+
];
|
|
9677
|
+
this.watcher = chokidar.watch(this.specsPath, {
|
|
9678
|
+
persistent: true,
|
|
9679
|
+
ignoreInitial: true,
|
|
9680
|
+
ignored,
|
|
9681
|
+
awaitWriteFinish: {
|
|
9682
|
+
stabilityThreshold: 100,
|
|
9683
|
+
pollInterval: 100
|
|
9684
|
+
}
|
|
9685
|
+
});
|
|
9686
|
+
this.watcher.on("add", (filePath) => this.handleEvent("add", filePath)).on("change", (filePath) => this.handleEvent("change", filePath)).on("unlink", (filePath) => this.handleEvent("unlink", filePath)).on("error", (error2) => this.emit("error", error2)).on("ready", () => {
|
|
9687
|
+
this.isRunning = true;
|
|
9688
|
+
this.emit("ready");
|
|
9689
|
+
});
|
|
9690
|
+
}
|
|
9691
|
+
/**
|
|
9692
|
+
* 감시 중지
|
|
9693
|
+
*/
|
|
9694
|
+
async stop() {
|
|
9695
|
+
if (this.debounceTimer) {
|
|
9696
|
+
clearTimeout(this.debounceTimer);
|
|
9697
|
+
this.debounceTimer = null;
|
|
9698
|
+
}
|
|
9699
|
+
if (this.watcher) {
|
|
9700
|
+
await this.watcher.close();
|
|
9701
|
+
this.watcher = null;
|
|
9702
|
+
}
|
|
9703
|
+
this.isRunning = false;
|
|
9704
|
+
this.pendingEvents = [];
|
|
9705
|
+
}
|
|
9706
|
+
/**
|
|
9707
|
+
* 실행 상태 확인
|
|
9708
|
+
*/
|
|
9709
|
+
get running() {
|
|
9710
|
+
return this.isRunning;
|
|
9711
|
+
}
|
|
9712
|
+
/**
|
|
9713
|
+
* 파일 이벤트 처리
|
|
9714
|
+
*/
|
|
9715
|
+
handleEvent(type, filePath) {
|
|
9716
|
+
if (!filePath.endsWith(".md")) {
|
|
9717
|
+
return;
|
|
9718
|
+
}
|
|
9719
|
+
const event = {
|
|
9720
|
+
type,
|
|
9721
|
+
path: filePath,
|
|
9722
|
+
relativePath: path21.relative(this.specsPath, filePath),
|
|
9723
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
9724
|
+
};
|
|
9725
|
+
this.pendingEvents.push(event);
|
|
9726
|
+
if (this.debounceTimer) {
|
|
9727
|
+
clearTimeout(this.debounceTimer);
|
|
9728
|
+
}
|
|
9729
|
+
this.debounceTimer = setTimeout(() => {
|
|
9730
|
+
this.flushEvents();
|
|
9731
|
+
}, this.debounceMs);
|
|
9732
|
+
}
|
|
9733
|
+
/**
|
|
9734
|
+
* 대기 중인 이벤트 처리
|
|
9735
|
+
*/
|
|
9736
|
+
flushEvents() {
|
|
9737
|
+
if (this.pendingEvents.length === 0) {
|
|
9738
|
+
return;
|
|
9739
|
+
}
|
|
9740
|
+
const events = [...this.pendingEvents];
|
|
9741
|
+
this.pendingEvents = [];
|
|
9742
|
+
this.debounceTimer = null;
|
|
9743
|
+
this.emit("change", events);
|
|
9744
|
+
}
|
|
9745
|
+
};
|
|
9746
|
+
function createWatcher(options) {
|
|
9747
|
+
return new SpecWatcher(options);
|
|
9748
|
+
}
|
|
9749
|
+
|
|
9750
|
+
// src/cli/commands/watch.ts
|
|
9751
|
+
init_fs();
|
|
9752
|
+
init_errors();
|
|
9753
|
+
function registerWatchCommand(program2) {
|
|
9754
|
+
program2.command("watch").description("\uC2A4\uD399 \uD30C\uC77C \uBCC0\uACBD\uC744 \uC2E4\uC2DC\uAC04 \uAC10\uC2DC\uD558\uACE0 \uC790\uB3D9 \uAC80\uC99D\uD569\uB2C8\uB2E4").option("--no-validate", "\uC790\uB3D9 \uAC80\uC99D \uBE44\uD65C\uC131\uD654").option("--impact", "\uC601\uD5A5\uB3C4 \uBD84\uC11D \uD3EC\uD568").option("-q, --quiet", "\uC131\uACF5 \uC2DC \uCD9C\uB825 \uC0DD\uB7B5").option("--debounce <ms>", "\uB514\uBC14\uC6B4\uC2A4 \uC2DC\uAC04 (\uAE30\uBCF8: 500ms)", "500").action(async (options) => {
|
|
9755
|
+
try {
|
|
9756
|
+
await runWatch(options);
|
|
9757
|
+
} catch (error2) {
|
|
9758
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
9759
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
9760
|
+
}
|
|
9761
|
+
});
|
|
9762
|
+
}
|
|
9763
|
+
async function runWatch(options) {
|
|
9764
|
+
const projectRoot = await findSddRoot();
|
|
9765
|
+
if (!projectRoot) {
|
|
9766
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
9767
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
9768
|
+
}
|
|
9769
|
+
const sddPath = path22.join(projectRoot, ".sdd");
|
|
9770
|
+
const specsPath = path22.join(sddPath, "specs");
|
|
9771
|
+
const debounceMs = parseInt(options.debounce || "500", 10);
|
|
9772
|
+
info("\u{1F441}\uFE0F Watch \uBAA8\uB4DC \uC2DC\uC791");
|
|
9773
|
+
info(` \uACBD\uB85C: ${specsPath}`);
|
|
9774
|
+
info(` \uB514\uBC14\uC6B4\uC2A4: ${debounceMs}ms`);
|
|
9775
|
+
info(` \uAC80\uC99D: ${options.validate !== false ? "\uD65C\uC131\uD654" : "\uBE44\uD65C\uC131\uD654"}`);
|
|
9776
|
+
newline();
|
|
9777
|
+
info("\uD30C\uC77C \uBCC0\uACBD\uC744 \uAC10\uC2DC \uC911... (Ctrl+C\uB85C \uC885\uB8CC)");
|
|
9778
|
+
newline();
|
|
9779
|
+
const watcher = createWatcher({
|
|
9780
|
+
specsPath,
|
|
9781
|
+
debounceMs
|
|
9782
|
+
});
|
|
9783
|
+
let validationCount = 0;
|
|
9784
|
+
let errorCount = 0;
|
|
9785
|
+
watcher.on("change", async (events) => {
|
|
9786
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
9787
|
+
const addCount = events.filter((e) => e.type === "add").length;
|
|
9788
|
+
const changeCount = events.filter((e) => e.type === "change").length;
|
|
9789
|
+
const unlinkCount = events.filter((e) => e.type === "unlink").length;
|
|
9790
|
+
const parts = [];
|
|
9791
|
+
if (addCount > 0) parts.push(`\uCD94\uAC00 ${addCount}`);
|
|
9792
|
+
if (changeCount > 0) parts.push(`\uC218\uC815 ${changeCount}`);
|
|
9793
|
+
if (unlinkCount > 0) parts.push(`\uC0AD\uC81C ${unlinkCount}`);
|
|
9794
|
+
info(`[${timestamp}] \uBCC0\uACBD \uAC10\uC9C0: ${parts.join(", ")}`);
|
|
9795
|
+
for (const event of events) {
|
|
9796
|
+
const icon = event.type === "add" ? "\u2795" : event.type === "change" ? "\u270F\uFE0F" : "\u274C";
|
|
9797
|
+
info(` ${icon} ${event.relativePath}`);
|
|
9798
|
+
}
|
|
9799
|
+
if (options.validate !== false) {
|
|
9800
|
+
newline();
|
|
9801
|
+
info("\u{1F50D} \uAC80\uC99D \uC2E4\uD589 \uC911...");
|
|
9802
|
+
const result = await validateSpecs(sddPath, { strict: false });
|
|
9803
|
+
validationCount++;
|
|
9804
|
+
if (result.success) {
|
|
9805
|
+
const data = result.data;
|
|
9806
|
+
const hasErrors = data.results.some((r) => r.errors.length > 0);
|
|
9807
|
+
const hasWarnings = data.results.some((r) => r.warnings.length > 0);
|
|
9808
|
+
if (hasErrors) {
|
|
9809
|
+
errorCount++;
|
|
9810
|
+
error(`\u274C \uAC80\uC99D \uC2E4\uD328: ${data.errorCount}\uAC1C \uC5D0\uB7EC, ${data.warningCount}\uAC1C \uACBD\uACE0`);
|
|
9811
|
+
for (const specResult of data.results) {
|
|
9812
|
+
if (specResult.errors.length > 0) {
|
|
9813
|
+
error(` ${specResult.file}:`);
|
|
9814
|
+
for (const err of specResult.errors) {
|
|
9815
|
+
error(` - ${err}`);
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
} else if (hasWarnings) {
|
|
9820
|
+
if (!options.quiet) {
|
|
9821
|
+
warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.warningCount}\uAC1C \uACBD\uACE0`);
|
|
9822
|
+
}
|
|
9823
|
+
} else {
|
|
9824
|
+
if (!options.quiet) {
|
|
9825
|
+
success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.validCount}\uAC1C \uC2A4\uD399)`);
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9828
|
+
} else {
|
|
9829
|
+
errorCount++;
|
|
9830
|
+
error(`\u274C \uAC80\uC99D \uC624\uB958: ${result.error.message}`);
|
|
9831
|
+
}
|
|
9832
|
+
newline();
|
|
9833
|
+
}
|
|
9834
|
+
});
|
|
9835
|
+
watcher.on("error", (error2) => {
|
|
9836
|
+
error(`\uAC10\uC2DC \uC624\uB958: ${error2.message}`);
|
|
9837
|
+
});
|
|
9838
|
+
watcher.on("ready", () => {
|
|
9839
|
+
success2("\u2705 \uAC10\uC2DC \uC900\uBE44 \uC644\uB8CC");
|
|
9840
|
+
newline();
|
|
9841
|
+
});
|
|
9842
|
+
const cleanup = async () => {
|
|
9843
|
+
newline();
|
|
9844
|
+
info("Watch \uBAA8\uB4DC \uC885\uB8CC \uC911...");
|
|
9845
|
+
await watcher.stop();
|
|
9846
|
+
newline();
|
|
9847
|
+
info("\u{1F4CA} \uC138\uC158 \uC694\uC57D:");
|
|
9848
|
+
info(` \uAC80\uC99D \uC2E4\uD589: ${validationCount}\uD68C`);
|
|
9849
|
+
info(` \uC5D0\uB7EC \uBC1C\uC0DD: ${errorCount}\uD68C`);
|
|
9850
|
+
process.exit(0);
|
|
9851
|
+
};
|
|
9852
|
+
process.on("SIGINT", cleanup);
|
|
9853
|
+
process.on("SIGTERM", cleanup);
|
|
9854
|
+
watcher.start();
|
|
9855
|
+
await new Promise(() => {
|
|
9856
|
+
});
|
|
9857
|
+
}
|
|
9858
|
+
|
|
9859
|
+
// src/cli/commands/quality.ts
|
|
9860
|
+
import path24 from "path";
|
|
9861
|
+
|
|
9862
|
+
// src/core/quality/analyzer.ts
|
|
9863
|
+
init_types();
|
|
9864
|
+
init_errors();
|
|
9865
|
+
init_fs();
|
|
9866
|
+
import path23 from "path";
|
|
9867
|
+
import { promises as fs13 } from "fs";
|
|
9868
|
+
function getGrade(percentage) {
|
|
9869
|
+
if (percentage >= 90) return "A";
|
|
9870
|
+
if (percentage >= 80) return "B";
|
|
9871
|
+
if (percentage >= 70) return "C";
|
|
9872
|
+
if (percentage >= 60) return "D";
|
|
9873
|
+
return "F";
|
|
9874
|
+
}
|
|
9875
|
+
function scoreRfc2119(content) {
|
|
9876
|
+
const maxScore = 10;
|
|
9877
|
+
const details = [];
|
|
9878
|
+
const suggestions = [];
|
|
9879
|
+
const keywords = ["SHALL", "MUST", "SHOULD", "MAY", "SHALL NOT", "MUST NOT", "SHOULD NOT"];
|
|
9880
|
+
const found = [];
|
|
9881
|
+
for (const kw of keywords) {
|
|
9882
|
+
const regex = new RegExp(`\\b${kw}\\b`, "gi");
|
|
9883
|
+
const matches = content.match(regex);
|
|
9884
|
+
if (matches && matches.length > 0) {
|
|
9885
|
+
found.push(`${kw}: ${matches.length}\uAC1C`);
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
let score = 0;
|
|
9889
|
+
if (found.length > 0) {
|
|
9890
|
+
score = Math.min(maxScore, found.length * 2);
|
|
9891
|
+
details.push(`\uBC1C\uACAC\uB41C \uD0A4\uC6CC\uB4DC: ${found.join(", ")}`);
|
|
9892
|
+
} else {
|
|
9893
|
+
details.push("RFC 2119 \uD0A4\uC6CC\uB4DC\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC74C");
|
|
9894
|
+
suggestions.push("\uC694\uAD6C\uC0AC\uD56D\uC5D0 SHALL, MUST, SHOULD, MAY \uD0A4\uC6CC\uB4DC\uB97C \uC0AC\uC6A9\uD558\uC138\uC694");
|
|
9895
|
+
}
|
|
9896
|
+
return {
|
|
9897
|
+
name: "RFC 2119 \uD0A4\uC6CC\uB4DC",
|
|
9898
|
+
score,
|
|
9899
|
+
maxScore,
|
|
9900
|
+
percentage: score / maxScore * 100,
|
|
9901
|
+
details,
|
|
9902
|
+
suggestions
|
|
9903
|
+
};
|
|
9904
|
+
}
|
|
9905
|
+
function scoreScenarios(content) {
|
|
9906
|
+
const maxScore = 20;
|
|
9907
|
+
const details = [];
|
|
9908
|
+
const suggestions = [];
|
|
9909
|
+
const givenCount = (content.match(/\*\*GIVEN\*\*|\bGIVEN\b/gi) || []).length;
|
|
9910
|
+
const whenCount = (content.match(/\*\*WHEN\*\*|\bWHEN\b/gi) || []).length;
|
|
9911
|
+
const thenCount = (content.match(/\*\*THEN\*\*|\bTHEN\b/gi) || []).length;
|
|
9912
|
+
const scenarioCount = Math.min(givenCount, whenCount, thenCount);
|
|
9913
|
+
let score = 0;
|
|
9914
|
+
if (scenarioCount > 0) {
|
|
9915
|
+
score = Math.min(maxScore, scenarioCount * 5);
|
|
9916
|
+
details.push(`\uC644\uC804\uD55C \uC2DC\uB098\uB9AC\uC624: ${scenarioCount}\uAC1C`);
|
|
9917
|
+
details.push(`GIVEN: ${givenCount}, WHEN: ${whenCount}, THEN: ${thenCount}`);
|
|
9918
|
+
} else {
|
|
9919
|
+
details.push("GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uAC00 \uC5C6\uC74C");
|
|
9920
|
+
suggestions.push("\uCD5C\uC18C 2\uAC1C \uC774\uC0C1\uC758 GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uB97C \uC791\uC131\uD558\uC138\uC694");
|
|
9921
|
+
}
|
|
9922
|
+
if (scenarioCount < 2 && scenarioCount > 0) {
|
|
9923
|
+
suggestions.push("\uCD94\uAC00 \uC2DC\uB098\uB9AC\uC624 \uC791\uC131\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4 (\uCD5C\uC18C 2\uAC1C)");
|
|
9924
|
+
}
|
|
9925
|
+
return {
|
|
9926
|
+
name: "GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624",
|
|
9927
|
+
score,
|
|
9928
|
+
maxScore,
|
|
9929
|
+
percentage: score / maxScore * 100,
|
|
9930
|
+
details,
|
|
9931
|
+
suggestions
|
|
9932
|
+
};
|
|
9933
|
+
}
|
|
9934
|
+
function scoreRequirements(content) {
|
|
9935
|
+
const maxScore = 15;
|
|
9936
|
+
const details = [];
|
|
9937
|
+
const suggestions = [];
|
|
9938
|
+
const reqIdPattern = /REQ-\d+|REQ-[A-Z]+-\d+/gi;
|
|
9939
|
+
const reqIds = content.match(reqIdPattern) || [];
|
|
9940
|
+
const hasRequirementsSection = /^##\s*(요구사항|Requirements)/im.test(content);
|
|
9941
|
+
let score = 0;
|
|
9942
|
+
if (hasRequirementsSection) {
|
|
9943
|
+
score += 5;
|
|
9944
|
+
details.push("\uC694\uAD6C\uC0AC\uD56D \uC139\uC158\uC774 \uC874\uC7AC\uD568");
|
|
9945
|
+
} else {
|
|
9946
|
+
suggestions.push("## \uC694\uAD6C\uC0AC\uD56D \uC139\uC158\uC744 \uCD94\uAC00\uD558\uC138\uC694");
|
|
9947
|
+
}
|
|
9948
|
+
if (reqIds.length > 0) {
|
|
9949
|
+
score += Math.min(10, reqIds.length * 2);
|
|
9950
|
+
details.push(`\uC694\uAD6C\uC0AC\uD56D ID: ${reqIds.length}\uAC1C (${[...new Set(reqIds)].slice(0, 3).join(", ")}...)`);
|
|
9951
|
+
} else {
|
|
9952
|
+
suggestions.push("\uC694\uAD6C\uC0AC\uD56D\uC5D0 REQ-01 \uD615\uC2DD\uC758 ID\uB97C \uBD80\uC5EC\uD558\uC138\uC694");
|
|
9953
|
+
}
|
|
9954
|
+
return {
|
|
9955
|
+
name: "\uC694\uAD6C\uC0AC\uD56D \uBA85\uD655\uC131",
|
|
9956
|
+
score,
|
|
9957
|
+
maxScore,
|
|
9958
|
+
percentage: score / maxScore * 100,
|
|
9959
|
+
details,
|
|
9960
|
+
suggestions
|
|
9961
|
+
};
|
|
9962
|
+
}
|
|
9963
|
+
function scoreDependencies(spec) {
|
|
9964
|
+
const maxScore = 10;
|
|
9965
|
+
const details = [];
|
|
9966
|
+
const suggestions = [];
|
|
9967
|
+
let score = 0;
|
|
9968
|
+
if (spec.frontmatter.depends) {
|
|
9969
|
+
const deps = Array.isArray(spec.frontmatter.depends) ? spec.frontmatter.depends : [spec.frontmatter.depends];
|
|
9970
|
+
if (deps.length > 0 && deps[0] !== null) {
|
|
9971
|
+
score = maxScore;
|
|
9972
|
+
details.push(`\uC758\uC874\uC131: ${deps.join(", ")}`);
|
|
9973
|
+
} else {
|
|
9974
|
+
score = 5;
|
|
9975
|
+
details.push("\uC758\uC874\uC131 \uC5C6\uC74C (\uBA85\uC2DC\uC801 \uC120\uC5B8)");
|
|
9976
|
+
}
|
|
9977
|
+
} else {
|
|
9978
|
+
details.push("\uC758\uC874\uC131 \uD544\uB4DC\uAC00 \uC5C6\uC74C");
|
|
9979
|
+
suggestions.push("frontmatter\uC5D0 depends \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694");
|
|
9980
|
+
}
|
|
9981
|
+
return {
|
|
9982
|
+
name: "\uC758\uC874\uC131 \uBA85\uC2DC",
|
|
9983
|
+
score,
|
|
9984
|
+
maxScore,
|
|
9985
|
+
percentage: score / maxScore * 100,
|
|
9986
|
+
details,
|
|
9987
|
+
suggestions
|
|
9988
|
+
};
|
|
9989
|
+
}
|
|
9990
|
+
function scoreStructure(content) {
|
|
9991
|
+
const maxScore = 15;
|
|
9992
|
+
const details = [];
|
|
9993
|
+
const suggestions = [];
|
|
9994
|
+
const requiredSections = [
|
|
9995
|
+
{ pattern: /^#\s+.+/m, name: "\uC81C\uBAA9 (H1)" },
|
|
9996
|
+
{ pattern: /^##\s*(요구사항|Requirements)/im, name: "\uC694\uAD6C\uC0AC\uD56D \uC139\uC158" },
|
|
9997
|
+
{ pattern: /^##\s*(시나리오|Scenario)/im, name: "\uC2DC\uB098\uB9AC\uC624 \uC139\uC158" }
|
|
9998
|
+
];
|
|
9999
|
+
const optionalSections = [
|
|
10000
|
+
{ pattern: /^##\s*(개요|Overview|설명|Description)/im, name: "\uAC1C\uC694/\uC124\uBA85 \uC139\uC158" },
|
|
10001
|
+
{ pattern: /^##\s*(제약|Constraints|제한)/im, name: "\uC81C\uC57D\uC0AC\uD56D \uC139\uC158" },
|
|
10002
|
+
{ pattern: /^##\s*(비고|Notes|참고)/im, name: "\uBE44\uACE0 \uC139\uC158" }
|
|
10003
|
+
];
|
|
10004
|
+
let score = 0;
|
|
10005
|
+
const foundRequired = [];
|
|
10006
|
+
const missingRequired = [];
|
|
10007
|
+
for (const section of requiredSections) {
|
|
10008
|
+
if (section.pattern.test(content)) {
|
|
10009
|
+
foundRequired.push(section.name);
|
|
10010
|
+
score += 4;
|
|
10011
|
+
} else {
|
|
10012
|
+
missingRequired.push(section.name);
|
|
10013
|
+
}
|
|
10014
|
+
}
|
|
10015
|
+
for (const section of optionalSections) {
|
|
10016
|
+
if (section.pattern.test(content)) {
|
|
10017
|
+
score += 1;
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
score = Math.min(maxScore, score);
|
|
10021
|
+
if (foundRequired.length > 0) {
|
|
10022
|
+
details.push(`\uD544\uC218 \uC139\uC158: ${foundRequired.join(", ")}`);
|
|
10023
|
+
}
|
|
10024
|
+
if (missingRequired.length > 0) {
|
|
10025
|
+
suggestions.push(`\uB204\uB77D\uB41C \uC139\uC158: ${missingRequired.join(", ")}`);
|
|
10026
|
+
}
|
|
10027
|
+
return {
|
|
10028
|
+
name: "\uBB38\uC11C \uAD6C\uC870",
|
|
10029
|
+
score,
|
|
10030
|
+
maxScore,
|
|
10031
|
+
percentage: score / maxScore * 100,
|
|
10032
|
+
details,
|
|
10033
|
+
suggestions
|
|
10034
|
+
};
|
|
10035
|
+
}
|
|
10036
|
+
function scoreConstitution(spec, hasConstitution) {
|
|
10037
|
+
const maxScore = 10;
|
|
10038
|
+
const details = [];
|
|
10039
|
+
const suggestions = [];
|
|
10040
|
+
let score = 0;
|
|
10041
|
+
if (!hasConstitution) {
|
|
10042
|
+
score = maxScore;
|
|
10043
|
+
details.push("Constitution \uBBF8\uC124\uC815 (\uAC80\uC0AC \uC0DD\uB7B5)");
|
|
10044
|
+
} else if (spec.frontmatter.constitution_version) {
|
|
10045
|
+
score = maxScore;
|
|
10046
|
+
details.push(`Constitution \uBC84\uC804: ${spec.frontmatter.constitution_version}`);
|
|
10047
|
+
} else {
|
|
10048
|
+
details.push("constitution_version \uD544\uB4DC \uC5C6\uC74C");
|
|
10049
|
+
suggestions.push("frontmatter\uC5D0 constitution_version\uC744 \uCD94\uAC00\uD558\uC138\uC694");
|
|
10050
|
+
}
|
|
10051
|
+
return {
|
|
10052
|
+
name: "Constitution \uC900\uC218",
|
|
10053
|
+
score,
|
|
10054
|
+
maxScore,
|
|
10055
|
+
percentage: score / maxScore * 100,
|
|
10056
|
+
details,
|
|
10057
|
+
suggestions
|
|
10058
|
+
};
|
|
10059
|
+
}
|
|
10060
|
+
function scoreLinks(content) {
|
|
10061
|
+
const maxScore = 10;
|
|
10062
|
+
const details = [];
|
|
10063
|
+
const suggestions = [];
|
|
10064
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
10065
|
+
const links = [...content.matchAll(linkPattern)];
|
|
10066
|
+
let score = 5;
|
|
10067
|
+
if (links.length > 0) {
|
|
10068
|
+
score = Math.min(maxScore, 5 + links.length);
|
|
10069
|
+
details.push(`\uB9C1\uD06C: ${links.length}\uAC1C`);
|
|
10070
|
+
} else {
|
|
10071
|
+
details.push("\uB9C1\uD06C \uC5C6\uC74C");
|
|
10072
|
+
suggestions.push("\uAD00\uB828 \uBB38\uC11C\uB098 \uC678\uBD80 \uCC38\uC870 \uB9C1\uD06C\uB97C \uCD94\uAC00\uD558\uBA74 \uC88B\uC2B5\uB2C8\uB2E4");
|
|
10073
|
+
}
|
|
10074
|
+
return {
|
|
10075
|
+
name: "\uCC38\uC870 \uB9C1\uD06C",
|
|
10076
|
+
score,
|
|
10077
|
+
maxScore,
|
|
10078
|
+
percentage: score / maxScore * 100,
|
|
10079
|
+
details,
|
|
10080
|
+
suggestions
|
|
10081
|
+
};
|
|
10082
|
+
}
|
|
10083
|
+
function scoreMetadata(spec) {
|
|
10084
|
+
const maxScore = 10;
|
|
10085
|
+
const details = [];
|
|
10086
|
+
const suggestions = [];
|
|
10087
|
+
const requiredFields = ["id", "title", "status"];
|
|
10088
|
+
const optionalFields = ["created", "updated", "author", "version"];
|
|
10089
|
+
let score = 0;
|
|
10090
|
+
const missingRequired = [];
|
|
10091
|
+
for (const field of requiredFields) {
|
|
10092
|
+
if (spec.frontmatter[field]) {
|
|
10093
|
+
score += 2;
|
|
10094
|
+
} else {
|
|
10095
|
+
missingRequired.push(field);
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
10098
|
+
for (const field of optionalFields) {
|
|
10099
|
+
if (spec.frontmatter[field]) {
|
|
10100
|
+
score += 1;
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
score = Math.min(maxScore, score);
|
|
10104
|
+
const presentFields = Object.keys(spec.frontmatter).filter(
|
|
10105
|
+
(k) => spec.frontmatter[k] !== null && spec.frontmatter[k] !== void 0
|
|
10106
|
+
);
|
|
10107
|
+
details.push(`\uBA54\uD0C0\uB370\uC774\uD130 \uD544\uB4DC: ${presentFields.length}\uAC1C`);
|
|
10108
|
+
if (missingRequired.length > 0) {
|
|
10109
|
+
suggestions.push(`\uD544\uC218 \uD544\uB4DC \uB204\uB77D: ${missingRequired.join(", ")}`);
|
|
10110
|
+
}
|
|
10111
|
+
return {
|
|
10112
|
+
name: "\uBA54\uD0C0\uB370\uC774\uD130 \uC644\uC131\uB3C4",
|
|
10113
|
+
score,
|
|
10114
|
+
maxScore,
|
|
10115
|
+
percentage: score / maxScore * 100,
|
|
10116
|
+
details,
|
|
10117
|
+
suggestions
|
|
10118
|
+
};
|
|
10119
|
+
}
|
|
10120
|
+
async function analyzeSpecQuality(specPath, sddPath) {
|
|
10121
|
+
try {
|
|
10122
|
+
if (!await fileExists(specPath)) {
|
|
10123
|
+
return failure(new ChangeError(`\uC2A4\uD399 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specPath}`));
|
|
10124
|
+
}
|
|
10125
|
+
const contentResult = await readFile(specPath);
|
|
10126
|
+
if (!contentResult.success) {
|
|
10127
|
+
return failure(new ChangeError("\uC2A4\uD399 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
10128
|
+
}
|
|
10129
|
+
const content = contentResult.data;
|
|
10130
|
+
const parseResult = parseSpec(content);
|
|
10131
|
+
if (!parseResult.success) {
|
|
10132
|
+
return failure(new ChangeError(`\uC2A4\uD399 \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
|
|
10133
|
+
}
|
|
10134
|
+
const spec = parseResult.data;
|
|
10135
|
+
const constitutionPath = path23.join(sddPath, "constitution.md");
|
|
10136
|
+
const hasConstitution = await fileExists(constitutionPath);
|
|
10137
|
+
const items = [
|
|
10138
|
+
scoreRfc2119(content),
|
|
10139
|
+
scoreScenarios(content),
|
|
10140
|
+
scoreRequirements(content),
|
|
10141
|
+
scoreDependencies(spec),
|
|
10142
|
+
scoreStructure(content),
|
|
10143
|
+
scoreConstitution(spec, hasConstitution),
|
|
10144
|
+
scoreLinks(content),
|
|
10145
|
+
scoreMetadata(spec)
|
|
10146
|
+
];
|
|
10147
|
+
const totalScore = items.reduce((sum, item) => sum + item.score, 0);
|
|
10148
|
+
const maxScore = items.reduce((sum, item) => sum + item.maxScore, 0);
|
|
10149
|
+
const percentage = Math.round(totalScore / maxScore * 100);
|
|
10150
|
+
const grade = getGrade(percentage);
|
|
10151
|
+
const topSuggestions = items.filter((item) => item.suggestions.length > 0).sort((a, b) => a.percentage - b.percentage).slice(0, 3).flatMap((item) => item.suggestions);
|
|
10152
|
+
const specId = spec.frontmatter.id || path23.basename(path23.dirname(specPath));
|
|
10153
|
+
const summary = `\uC2A4\uD399 '${specId}'\uC758 \uD488\uC9C8 \uC810\uC218: ${totalScore}/${maxScore} (${percentage}%, \uB4F1\uAE09: ${grade})`;
|
|
10154
|
+
return success({
|
|
10155
|
+
specId,
|
|
10156
|
+
specPath,
|
|
10157
|
+
totalScore,
|
|
10158
|
+
maxScore,
|
|
10159
|
+
percentage,
|
|
10160
|
+
grade,
|
|
10161
|
+
items,
|
|
10162
|
+
summary,
|
|
10163
|
+
topSuggestions
|
|
10164
|
+
});
|
|
10165
|
+
} catch (error2) {
|
|
10166
|
+
return failure(
|
|
10167
|
+
new ChangeError(
|
|
10168
|
+
`\uD488\uC9C8 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10169
|
+
)
|
|
10170
|
+
);
|
|
10171
|
+
}
|
|
10172
|
+
}
|
|
10173
|
+
async function analyzeProjectQuality(sddPath) {
|
|
10174
|
+
try {
|
|
10175
|
+
const specsPath = path23.join(sddPath, "specs");
|
|
10176
|
+
if (!await directoryExists(specsPath)) {
|
|
10177
|
+
return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
10178
|
+
}
|
|
10179
|
+
const specFiles = [];
|
|
10180
|
+
await findSpecFiles2(specsPath, specFiles);
|
|
10181
|
+
if (specFiles.length === 0) {
|
|
10182
|
+
return failure(new ChangeError("\uC2A4\uD399 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
10183
|
+
}
|
|
10184
|
+
const specResults = [];
|
|
10185
|
+
for (const specFile of specFiles) {
|
|
10186
|
+
const result = await analyzeSpecQuality(specFile, sddPath);
|
|
10187
|
+
if (result.success) {
|
|
10188
|
+
specResults.push(result.data);
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
if (specResults.length === 0) {
|
|
10192
|
+
return failure(new ChangeError("\uBD84\uC11D \uAC00\uB2A5\uD55C \uC2A4\uD399\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
10193
|
+
}
|
|
10194
|
+
const totalPercentage = specResults.reduce((sum, r) => sum + r.percentage, 0);
|
|
10195
|
+
const averagePercentage = Math.round(totalPercentage / specResults.length);
|
|
10196
|
+
const averageScore = Math.round(
|
|
10197
|
+
specResults.reduce((sum, r) => sum + r.totalScore, 0) / specResults.length
|
|
10198
|
+
);
|
|
10199
|
+
const grade = getGrade(averagePercentage);
|
|
10200
|
+
const summary = `\uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8: \uD3C9\uADE0 ${averagePercentage}% (\uB4F1\uAE09: ${grade}), ${specResults.length}\uAC1C \uC2A4\uD399 \uBD84\uC11D`;
|
|
10201
|
+
return success({
|
|
10202
|
+
averageScore,
|
|
10203
|
+
averagePercentage,
|
|
10204
|
+
grade,
|
|
10205
|
+
totalSpecs: specResults.length,
|
|
10206
|
+
specResults,
|
|
10207
|
+
summary
|
|
10208
|
+
});
|
|
10209
|
+
} catch (error2) {
|
|
10210
|
+
return failure(
|
|
10211
|
+
new ChangeError(
|
|
10212
|
+
`\uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10213
|
+
)
|
|
10214
|
+
);
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
async function findSpecFiles2(dir, files) {
|
|
10218
|
+
const entries = await fs13.readdir(dir, { withFileTypes: true });
|
|
10219
|
+
for (const entry of entries) {
|
|
10220
|
+
const fullPath = path23.join(dir, entry.name);
|
|
10221
|
+
if (entry.isDirectory()) {
|
|
10222
|
+
await findSpecFiles2(fullPath, files);
|
|
10223
|
+
} else if (entry.name === "spec.md") {
|
|
10224
|
+
files.push(fullPath);
|
|
10225
|
+
}
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10228
|
+
function formatQualityResult(result) {
|
|
10229
|
+
const lines = [];
|
|
10230
|
+
const gradeIcon = result.grade === "A" ? "\u{1F3C6}" : result.grade === "B" ? "\u2705" : result.grade === "C" ? "\u{1F7E1}" : result.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
|
|
10231
|
+
lines.push(`\u{1F4CA} \uD488\uC9C8 \uBD84\uC11D: ${result.specId}`);
|
|
10232
|
+
lines.push(` ${gradeIcon} \uB4F1\uAE09: ${result.grade} (${result.percentage}%)`);
|
|
10233
|
+
lines.push(` \u{1F4C8} \uC810\uC218: ${result.totalScore}/${result.maxScore}`);
|
|
10234
|
+
lines.push("");
|
|
10235
|
+
lines.push("\u{1F4CB} \uD56D\uBAA9\uBCC4 \uC810\uC218:");
|
|
10236
|
+
for (const item of result.items) {
|
|
10237
|
+
const icon = item.percentage >= 80 ? "\u2705" : item.percentage >= 60 ? "\u{1F7E1}" : "\u{1F534}";
|
|
10238
|
+
lines.push(` ${icon} ${item.name}: ${item.score}/${item.maxScore} (${Math.round(item.percentage)}%)`);
|
|
10239
|
+
for (const detail of item.details) {
|
|
10240
|
+
lines.push(` \u2514\u2500 ${detail}`);
|
|
10241
|
+
}
|
|
10242
|
+
}
|
|
10243
|
+
lines.push("");
|
|
10244
|
+
if (result.topSuggestions.length > 0) {
|
|
10245
|
+
lines.push("\u{1F4A1} \uAC1C\uC120 \uC81C\uC548:");
|
|
10246
|
+
for (const suggestion of result.topSuggestions) {
|
|
10247
|
+
lines.push(` - ${suggestion}`);
|
|
10248
|
+
}
|
|
10249
|
+
}
|
|
10250
|
+
return lines.join("\n");
|
|
10251
|
+
}
|
|
10252
|
+
function formatProjectQualityResult(result) {
|
|
10253
|
+
const lines = [];
|
|
10254
|
+
const gradeIcon = result.grade === "A" ? "\u{1F3C6}" : result.grade === "B" ? "\u2705" : result.grade === "C" ? "\u{1F7E1}" : result.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
|
|
10255
|
+
lines.push("\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8 \uBD84\uC11D");
|
|
10256
|
+
lines.push(` ${gradeIcon} \uD3C9\uADE0 \uB4F1\uAE09: ${result.grade} (${result.averagePercentage}%)`);
|
|
10257
|
+
lines.push(` \u{1F4C8} \uBD84\uC11D\uB41C \uC2A4\uD399: ${result.totalSpecs}\uAC1C`);
|
|
10258
|
+
lines.push("");
|
|
10259
|
+
const gradeCount = { A: 0, B: 0, C: 0, D: 0, F: 0 };
|
|
10260
|
+
for (const spec of result.specResults) {
|
|
10261
|
+
gradeCount[spec.grade]++;
|
|
10262
|
+
}
|
|
10263
|
+
lines.push("\u{1F4C8} \uB4F1\uAE09 \uBD84\uD3EC:");
|
|
10264
|
+
if (gradeCount.A > 0) lines.push(` \u{1F3C6} A: ${gradeCount.A}\uAC1C`);
|
|
10265
|
+
if (gradeCount.B > 0) lines.push(` \u2705 B: ${gradeCount.B}\uAC1C`);
|
|
10266
|
+
if (gradeCount.C > 0) lines.push(` \u{1F7E1} C: ${gradeCount.C}\uAC1C`);
|
|
10267
|
+
if (gradeCount.D > 0) lines.push(` \u{1F7E0} D: ${gradeCount.D}\uAC1C`);
|
|
10268
|
+
if (gradeCount.F > 0) lines.push(` \u{1F534} F: ${gradeCount.F}\uAC1C`);
|
|
10269
|
+
lines.push("");
|
|
10270
|
+
lines.push("\u{1F4CB} \uC2A4\uD399\uBCC4 \uC810\uC218:");
|
|
10271
|
+
const sortedSpecs = [...result.specResults].sort((a, b) => b.percentage - a.percentage);
|
|
10272
|
+
for (const spec of sortedSpecs) {
|
|
10273
|
+
const icon = spec.grade === "A" ? "\u{1F3C6}" : spec.grade === "B" ? "\u2705" : spec.grade === "C" ? "\u{1F7E1}" : spec.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
|
|
10274
|
+
lines.push(` ${icon} ${spec.specId}: ${spec.percentage}% (${spec.grade})`);
|
|
10275
|
+
}
|
|
10276
|
+
return lines.join("\n");
|
|
10277
|
+
}
|
|
10278
|
+
|
|
10279
|
+
// src/cli/commands/quality.ts
|
|
10280
|
+
init_fs();
|
|
10281
|
+
init_errors();
|
|
10282
|
+
function registerQualityCommand(program2) {
|
|
10283
|
+
program2.command("quality [feature]").description("\uC2A4\uD399 \uD488\uC9C8\uC744 \uBD84\uC11D\uD558\uACE0 \uC810\uC218\uB97C \uC0B0\uCD9C\uD569\uB2C8\uB2E4").option("-a, --all", "\uC804\uCCB4 \uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D").option("--json", "JSON \uD615\uC2DD \uCD9C\uB825").option("--min-score <score>", "\uCD5C\uC18C \uC810\uC218 \uAE30\uC900 (\uC774\uD558 \uC2DC \uC5D0\uB7EC)", "0").action(async (feature, options) => {
|
|
10284
|
+
try {
|
|
10285
|
+
await runQuality(feature, options);
|
|
10286
|
+
} catch (error2) {
|
|
10287
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
10288
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10289
|
+
}
|
|
10290
|
+
});
|
|
10291
|
+
}
|
|
10292
|
+
async function runQuality(feature, options) {
|
|
10293
|
+
const projectRoot = await findSddRoot();
|
|
10294
|
+
if (!projectRoot) {
|
|
10295
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
10296
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10297
|
+
}
|
|
10298
|
+
const sddPath = path24.join(projectRoot, ".sdd");
|
|
10299
|
+
const minScore = parseInt(options.minScore || "0", 10);
|
|
10300
|
+
if (options.all || !feature) {
|
|
10301
|
+
const result2 = await analyzeProjectQuality(sddPath);
|
|
10302
|
+
if (!result2.success) {
|
|
10303
|
+
error(result2.error.message);
|
|
10304
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10305
|
+
}
|
|
10306
|
+
if (options.json) {
|
|
10307
|
+
console.log(JSON.stringify(result2.data, null, 2));
|
|
10308
|
+
} else {
|
|
10309
|
+
console.log(formatProjectQualityResult(result2.data));
|
|
10310
|
+
}
|
|
10311
|
+
if (result2.data.averagePercentage < minScore) {
|
|
10312
|
+
newline();
|
|
10313
|
+
error(`\uD488\uC9C8 \uC810\uC218\uAC00 \uCD5C\uC18C \uAE30\uC900(${minScore}%) \uBBF8\uB2EC\uC785\uB2C8\uB2E4.`);
|
|
10314
|
+
process.exit(ExitCode.VALIDATION_ERROR);
|
|
10315
|
+
}
|
|
10316
|
+
return;
|
|
10317
|
+
}
|
|
10318
|
+
const specPath = path24.join(sddPath, "specs", feature, "spec.md");
|
|
10319
|
+
const result = await analyzeSpecQuality(specPath, sddPath);
|
|
10320
|
+
if (!result.success) {
|
|
10321
|
+
error(result.error.message);
|
|
10322
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10323
|
+
}
|
|
10324
|
+
if (options.json) {
|
|
10325
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
10326
|
+
} else {
|
|
10327
|
+
console.log(formatQualityResult(result.data));
|
|
10328
|
+
}
|
|
10329
|
+
if (result.data.percentage < minScore) {
|
|
10330
|
+
newline();
|
|
10331
|
+
error(`\uD488\uC9C8 \uC810\uC218\uAC00 \uCD5C\uC18C \uAE30\uC900(${minScore}%) \uBBF8\uB2EC\uC785\uB2C8\uB2E4.`);
|
|
10332
|
+
process.exit(ExitCode.VALIDATION_ERROR);
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
|
|
10336
|
+
// src/cli/commands/report.ts
|
|
10337
|
+
import path26 from "path";
|
|
10338
|
+
|
|
10339
|
+
// src/core/report/reporter.ts
|
|
10340
|
+
init_types();
|
|
10341
|
+
init_errors();
|
|
10342
|
+
import path25 from "path";
|
|
10343
|
+
import fs14 from "fs/promises";
|
|
10344
|
+
init_fs();
|
|
10345
|
+
async function loadSpecList(specsPath) {
|
|
10346
|
+
try {
|
|
10347
|
+
if (!await fileExists(specsPath)) {
|
|
10348
|
+
return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
10349
|
+
}
|
|
10350
|
+
const result = await readDir(specsPath);
|
|
10351
|
+
if (!result.success) {
|
|
10352
|
+
return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
10353
|
+
}
|
|
10354
|
+
const specs = [];
|
|
10355
|
+
for (const entry of result.data) {
|
|
10356
|
+
const featurePath = path25.join(specsPath, entry);
|
|
10357
|
+
const stat = await fs14.stat(featurePath);
|
|
10358
|
+
if (stat.isDirectory()) {
|
|
10359
|
+
const specFile = path25.join(featurePath, "spec.md");
|
|
10360
|
+
if (await fileExists(specFile)) {
|
|
10361
|
+
const content = await fs14.readFile(specFile, "utf-8");
|
|
10362
|
+
const metadata = parseSpecMetadata2(content);
|
|
10363
|
+
specs.push({
|
|
10364
|
+
id: entry,
|
|
10365
|
+
title: metadata?.title,
|
|
10366
|
+
phase: metadata?.phase,
|
|
10367
|
+
status: metadata?.status,
|
|
10368
|
+
description: metadata?.description
|
|
10369
|
+
});
|
|
10370
|
+
}
|
|
10371
|
+
}
|
|
10372
|
+
}
|
|
10373
|
+
return success(specs);
|
|
10374
|
+
} catch (error2) {
|
|
10375
|
+
return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
function parseSpecMetadata2(content) {
|
|
10379
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
10380
|
+
if (!frontmatterMatch) return null;
|
|
10381
|
+
const frontmatter = frontmatterMatch[1];
|
|
10382
|
+
const result = {};
|
|
10383
|
+
const lines = frontmatter.split("\n");
|
|
10384
|
+
for (const line of lines) {
|
|
10385
|
+
const match = line.match(/^(\w+):\s*['"]?([^'"]+)['"]?$/);
|
|
10386
|
+
if (match) {
|
|
10387
|
+
result[match[1]] = match[2].trim();
|
|
10388
|
+
}
|
|
10389
|
+
}
|
|
10390
|
+
return {
|
|
10391
|
+
title: result.title,
|
|
10392
|
+
phase: result.phase,
|
|
10393
|
+
status: result.status,
|
|
10394
|
+
description: result.description
|
|
10395
|
+
};
|
|
10396
|
+
}
|
|
10397
|
+
async function generateReport(sddPath, options) {
|
|
10398
|
+
try {
|
|
10399
|
+
const specsPath = path25.join(sddPath, "specs");
|
|
10400
|
+
const specsResult = await loadSpecList(specsPath);
|
|
10401
|
+
if (!specsResult.success) {
|
|
10402
|
+
return failure(specsResult.error);
|
|
10403
|
+
}
|
|
10404
|
+
const specs = specsResult.data;
|
|
10405
|
+
const reportData = {
|
|
10406
|
+
title: options.title || "SDD \uD504\uB85C\uC81D\uD2B8 \uB9AC\uD3EC\uD2B8",
|
|
10407
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10408
|
+
projectPath: sddPath,
|
|
10409
|
+
specs,
|
|
10410
|
+
summary: {
|
|
10411
|
+
totalSpecs: specs.length,
|
|
10412
|
+
byPhase: {},
|
|
10413
|
+
byStatus: {}
|
|
10414
|
+
}
|
|
10415
|
+
};
|
|
10416
|
+
for (const spec of specs) {
|
|
10417
|
+
const phase = spec.phase || "unknown";
|
|
10418
|
+
reportData.summary.byPhase[phase] = (reportData.summary.byPhase[phase] || 0) + 1;
|
|
10419
|
+
const status = spec.status || "unknown";
|
|
10420
|
+
reportData.summary.byStatus[status] = (reportData.summary.byStatus[status] || 0) + 1;
|
|
10421
|
+
}
|
|
10422
|
+
if (options.includeQuality !== false) {
|
|
10423
|
+
const qualityResult = await analyzeProjectQuality(sddPath);
|
|
10424
|
+
if (qualityResult.success) {
|
|
10425
|
+
reportData.quality = qualityResult.data;
|
|
10426
|
+
reportData.summary.averageQuality = qualityResult.data.averagePercentage;
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10429
|
+
if (options.includeValidation !== false) {
|
|
10430
|
+
const validationResult = await validateSpecs(sddPath, { strict: false });
|
|
10431
|
+
if (validationResult.success) {
|
|
10432
|
+
reportData.validation = validationResult.data;
|
|
10433
|
+
reportData.summary.validationErrors = validationResult.data.errorCount;
|
|
10434
|
+
reportData.summary.validationWarnings = validationResult.data.warningCount;
|
|
10435
|
+
}
|
|
10436
|
+
}
|
|
10437
|
+
let content;
|
|
10438
|
+
switch (options.format) {
|
|
10439
|
+
case "html":
|
|
10440
|
+
content = renderHtmlReport(reportData);
|
|
10441
|
+
break;
|
|
10442
|
+
case "markdown":
|
|
10443
|
+
content = renderMarkdownReport(reportData);
|
|
10444
|
+
break;
|
|
10445
|
+
case "json":
|
|
10446
|
+
content = JSON.stringify(reportData, null, 2);
|
|
10447
|
+
break;
|
|
10448
|
+
default:
|
|
10449
|
+
return failure(new ChangeError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${options.format}`));
|
|
10450
|
+
}
|
|
10451
|
+
if (options.outputPath) {
|
|
10452
|
+
await fs14.mkdir(path25.dirname(options.outputPath), { recursive: true });
|
|
10453
|
+
await fs14.writeFile(options.outputPath, content, "utf-8");
|
|
10454
|
+
}
|
|
10455
|
+
return success({
|
|
10456
|
+
format: options.format,
|
|
10457
|
+
content,
|
|
10458
|
+
outputPath: options.outputPath
|
|
10459
|
+
});
|
|
10460
|
+
} catch (error2) {
|
|
10461
|
+
return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
|
|
10462
|
+
}
|
|
10463
|
+
}
|
|
10464
|
+
function renderHtmlReport(data) {
|
|
10465
|
+
const gradeColor = (grade) => {
|
|
10466
|
+
switch (grade) {
|
|
10467
|
+
case "A":
|
|
10468
|
+
return "#22c55e";
|
|
10469
|
+
case "B":
|
|
10470
|
+
return "#84cc16";
|
|
10471
|
+
case "C":
|
|
10472
|
+
return "#eab308";
|
|
10473
|
+
case "D":
|
|
10474
|
+
return "#f97316";
|
|
10475
|
+
case "F":
|
|
10476
|
+
return "#ef4444";
|
|
10477
|
+
default:
|
|
10478
|
+
return "#6b7280";
|
|
10479
|
+
}
|
|
10480
|
+
};
|
|
10481
|
+
const statusBadge = (status) => {
|
|
10482
|
+
const colors = {
|
|
10483
|
+
draft: "#6b7280",
|
|
10484
|
+
review: "#3b82f6",
|
|
10485
|
+
approved: "#22c55e",
|
|
10486
|
+
implemented: "#8b5cf6",
|
|
10487
|
+
deprecated: "#ef4444"
|
|
10488
|
+
};
|
|
10489
|
+
const color = colors[status] || "#6b7280";
|
|
10490
|
+
return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:12px;">${status}</span>`;
|
|
10491
|
+
};
|
|
10492
|
+
const specRows = data.specs.map((spec) => `
|
|
10493
|
+
<tr>
|
|
10494
|
+
<td><strong>${spec.id}</strong></td>
|
|
10495
|
+
<td>${spec.title || "-"}</td>
|
|
10496
|
+
<td>${spec.phase || "-"}</td>
|
|
10497
|
+
<td>${statusBadge(spec.status || "unknown")}</td>
|
|
10498
|
+
<td>${spec.description || "-"}</td>
|
|
10499
|
+
</tr>
|
|
10500
|
+
`).join("");
|
|
10501
|
+
const qualityRows = data.quality?.results.map((q) => `
|
|
10502
|
+
<tr>
|
|
10503
|
+
<td>${q.specId}</td>
|
|
10504
|
+
<td>${q.percentage}%</td>
|
|
10505
|
+
<td style="color:${gradeColor(q.grade)};font-weight:bold;">${q.grade}</td>
|
|
10506
|
+
<td>${q.totalScore}/${q.maxScore}</td>
|
|
10507
|
+
</tr>
|
|
10508
|
+
`).join("") || "";
|
|
10509
|
+
const validationRows = data.validation?.results.map((v) => `
|
|
10510
|
+
<tr>
|
|
10511
|
+
<td>${v.file}</td>
|
|
10512
|
+
<td style="color:${v.errors.length > 0 ? "#ef4444" : "#22c55e"};">${v.errors.length > 0 ? "\u274C \uC2E4\uD328" : "\u2705 \uD1B5\uACFC"}</td>
|
|
10513
|
+
<td>${v.errors.length}</td>
|
|
10514
|
+
<td>${v.warnings.length}</td>
|
|
10515
|
+
</tr>
|
|
10516
|
+
`).join("") || "";
|
|
10517
|
+
return `<!DOCTYPE html>
|
|
10518
|
+
<html lang="ko">
|
|
10519
|
+
<head>
|
|
10520
|
+
<meta charset="UTF-8">
|
|
10521
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10522
|
+
<title>${data.title}</title>
|
|
10523
|
+
<style>
|
|
10524
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10525
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; background: #f9fafb; padding: 2rem; }
|
|
10526
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
10527
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #111827; }
|
|
10528
|
+
h2 { font-size: 1.5rem; margin: 2rem 0 1rem; color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }
|
|
10529
|
+
.meta { color: #6b7280; margin-bottom: 2rem; }
|
|
10530
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
10531
|
+
.card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
10532
|
+
.card-title { font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem; }
|
|
10533
|
+
.card-value { font-size: 2rem; font-weight: bold; color: #111827; }
|
|
10534
|
+
.card-value.success { color: #22c55e; }
|
|
10535
|
+
.card-value.warning { color: #eab308; }
|
|
10536
|
+
.card-value.error { color: #ef4444; }
|
|
10537
|
+
table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem; }
|
|
10538
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
|
|
10539
|
+
th { background: #f3f4f6; font-weight: 600; color: #374151; }
|
|
10540
|
+
tr:hover { background: #f9fafb; }
|
|
10541
|
+
.phase-chart { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
10542
|
+
.phase-item { background: #e0e7ff; color: #3730a3; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.875rem; }
|
|
10543
|
+
</style>
|
|
10544
|
+
</head>
|
|
10545
|
+
<body>
|
|
10546
|
+
<div class="container">
|
|
10547
|
+
<h1>${data.title}</h1>
|
|
10548
|
+
<p class="meta">\uC0DD\uC131: ${new Date(data.generatedAt).toLocaleString("ko-KR")} | \uACBD\uB85C: ${data.projectPath}</p>
|
|
10549
|
+
|
|
10550
|
+
<div class="summary">
|
|
10551
|
+
<div class="card">
|
|
10552
|
+
<div class="card-title">\uCD1D \uC2A4\uD399 \uC218</div>
|
|
10553
|
+
<div class="card-value">${data.summary.totalSpecs}</div>
|
|
10554
|
+
</div>
|
|
10555
|
+
${data.summary.averageQuality !== void 0 ? `
|
|
10556
|
+
<div class="card">
|
|
10557
|
+
<div class="card-title">\uD3C9\uADE0 \uD488\uC9C8</div>
|
|
10558
|
+
<div class="card-value ${data.summary.averageQuality >= 80 ? "success" : data.summary.averageQuality >= 60 ? "warning" : "error"}">${data.summary.averageQuality.toFixed(1)}%</div>
|
|
10559
|
+
</div>` : ""}
|
|
10560
|
+
${data.summary.validationErrors !== void 0 ? `
|
|
10561
|
+
<div class="card">
|
|
10562
|
+
<div class="card-title">\uAC80\uC99D \uC5D0\uB7EC</div>
|
|
10563
|
+
<div class="card-value ${data.summary.validationErrors === 0 ? "success" : "error"}">${data.summary.validationErrors}</div>
|
|
10564
|
+
</div>` : ""}
|
|
10565
|
+
${data.summary.validationWarnings !== void 0 ? `
|
|
10566
|
+
<div class="card">
|
|
10567
|
+
<div class="card-title">\uAC80\uC99D \uACBD\uACE0</div>
|
|
10568
|
+
<div class="card-value ${data.summary.validationWarnings === 0 ? "success" : "warning"}">${data.summary.validationWarnings}</div>
|
|
10569
|
+
</div>` : ""}
|
|
10570
|
+
</div>
|
|
10571
|
+
|
|
10572
|
+
<h2>Phase\uBCC4 \uBD84\uD3EC</h2>
|
|
10573
|
+
<div class="phase-chart">
|
|
10574
|
+
${Object.entries(data.summary.byPhase).map(([phase, count]) => `
|
|
10575
|
+
<span class="phase-item">${phase}: ${count}</span>
|
|
10576
|
+
`).join("")}
|
|
10577
|
+
</div>
|
|
10578
|
+
|
|
10579
|
+
<h2>\uC2A4\uD399 \uBAA9\uB85D</h2>
|
|
10580
|
+
<table>
|
|
10581
|
+
<thead>
|
|
10582
|
+
<tr>
|
|
10583
|
+
<th>ID</th>
|
|
10584
|
+
<th>\uC81C\uBAA9</th>
|
|
10585
|
+
<th>Phase</th>
|
|
10586
|
+
<th>\uC0C1\uD0DC</th>
|
|
10587
|
+
<th>\uC124\uBA85</th>
|
|
10588
|
+
</tr>
|
|
10589
|
+
</thead>
|
|
10590
|
+
<tbody>
|
|
10591
|
+
${specRows}
|
|
10592
|
+
</tbody>
|
|
10593
|
+
</table>
|
|
10594
|
+
|
|
10595
|
+
${data.quality ? `
|
|
10596
|
+
<h2>\uD488\uC9C8 \uBD84\uC11D</h2>
|
|
10597
|
+
<table>
|
|
10598
|
+
<thead>
|
|
10599
|
+
<tr>
|
|
10600
|
+
<th>\uC2A4\uD399 ID</th>
|
|
10601
|
+
<th>\uC810\uC218</th>
|
|
10602
|
+
<th>\uB4F1\uAE09</th>
|
|
10603
|
+
<th>\uC0C1\uC138</th>
|
|
10604
|
+
</tr>
|
|
10605
|
+
</thead>
|
|
10606
|
+
<tbody>
|
|
10607
|
+
${qualityRows}
|
|
10608
|
+
</tbody>
|
|
10609
|
+
</table>` : ""}
|
|
10610
|
+
|
|
10611
|
+
${data.validation ? `
|
|
10612
|
+
<h2>\uAC80\uC99D \uACB0\uACFC</h2>
|
|
10613
|
+
<table>
|
|
10614
|
+
<thead>
|
|
10615
|
+
<tr>
|
|
10616
|
+
<th>\uD30C\uC77C</th>
|
|
10617
|
+
<th>\uC0C1\uD0DC</th>
|
|
10618
|
+
<th>\uC5D0\uB7EC</th>
|
|
10619
|
+
<th>\uACBD\uACE0</th>
|
|
10620
|
+
</tr>
|
|
10621
|
+
</thead>
|
|
10622
|
+
<tbody>
|
|
10623
|
+
${validationRows}
|
|
10624
|
+
</tbody>
|
|
10625
|
+
</table>` : ""}
|
|
10626
|
+
|
|
10627
|
+
<footer style="margin-top:3rem;padding-top:1rem;border-top:1px solid #e5e7eb;color:#6b7280;font-size:0.875rem;">
|
|
10628
|
+
Generated by SDD CLI v0.5.0
|
|
10629
|
+
</footer>
|
|
10630
|
+
</div>
|
|
10631
|
+
</body>
|
|
10632
|
+
</html>`;
|
|
10633
|
+
}
|
|
10634
|
+
function renderMarkdownReport(data) {
|
|
10635
|
+
const lines = [];
|
|
10636
|
+
lines.push(`# ${data.title}`);
|
|
10637
|
+
lines.push("");
|
|
10638
|
+
lines.push(`> \uC0DD\uC131: ${new Date(data.generatedAt).toLocaleString("ko-KR")}`);
|
|
10639
|
+
lines.push(`> \uACBD\uB85C: ${data.projectPath}`);
|
|
10640
|
+
lines.push("");
|
|
10641
|
+
lines.push("## \uC694\uC57D");
|
|
10642
|
+
lines.push("");
|
|
10643
|
+
lines.push(`| \uD56D\uBAA9 | \uAC12 |`);
|
|
10644
|
+
lines.push(`|------|-----|`);
|
|
10645
|
+
lines.push(`| \uCD1D \uC2A4\uD399 \uC218 | ${data.summary.totalSpecs} |`);
|
|
10646
|
+
if (data.summary.averageQuality !== void 0) {
|
|
10647
|
+
lines.push(`| \uD3C9\uADE0 \uD488\uC9C8 | ${data.summary.averageQuality.toFixed(1)}% |`);
|
|
10648
|
+
}
|
|
10649
|
+
if (data.summary.validationErrors !== void 0) {
|
|
10650
|
+
lines.push(`| \uAC80\uC99D \uC5D0\uB7EC | ${data.summary.validationErrors} |`);
|
|
10651
|
+
}
|
|
10652
|
+
if (data.summary.validationWarnings !== void 0) {
|
|
10653
|
+
lines.push(`| \uAC80\uC99D \uACBD\uACE0 | ${data.summary.validationWarnings} |`);
|
|
10654
|
+
}
|
|
10655
|
+
lines.push("");
|
|
10656
|
+
lines.push("## Phase\uBCC4 \uBD84\uD3EC");
|
|
10657
|
+
lines.push("");
|
|
10658
|
+
for (const [phase, count] of Object.entries(data.summary.byPhase)) {
|
|
10659
|
+
lines.push(`- **${phase}**: ${count}\uAC1C`);
|
|
10660
|
+
}
|
|
10661
|
+
lines.push("");
|
|
10662
|
+
lines.push("## \uC0C1\uD0DC\uBCC4 \uBD84\uD3EC");
|
|
10663
|
+
lines.push("");
|
|
10664
|
+
for (const [status, count] of Object.entries(data.summary.byStatus)) {
|
|
10665
|
+
lines.push(`- **${status}**: ${count}\uAC1C`);
|
|
10666
|
+
}
|
|
10667
|
+
lines.push("");
|
|
10668
|
+
lines.push("## \uC2A4\uD399 \uBAA9\uB85D");
|
|
10669
|
+
lines.push("");
|
|
10670
|
+
lines.push("| ID | \uC81C\uBAA9 | Phase | \uC0C1\uD0DC |");
|
|
10671
|
+
lines.push("|----|------|-------|------|");
|
|
10672
|
+
for (const spec of data.specs) {
|
|
10673
|
+
lines.push(`| ${spec.id} | ${spec.title || "-"} | ${spec.phase || "-"} | ${spec.status || "-"} |`);
|
|
10674
|
+
}
|
|
10675
|
+
lines.push("");
|
|
10676
|
+
if (data.quality) {
|
|
10677
|
+
lines.push("## \uD488\uC9C8 \uBD84\uC11D");
|
|
10678
|
+
lines.push("");
|
|
10679
|
+
lines.push(`\uD3C9\uADE0 \uC810\uC218: **${data.quality.averagePercentage.toFixed(1)}%** (${data.quality.averageGrade})`);
|
|
10680
|
+
lines.push("");
|
|
10681
|
+
lines.push("| \uC2A4\uD399 ID | \uC810\uC218 | \uB4F1\uAE09 |");
|
|
10682
|
+
lines.push("|---------|------|------|");
|
|
10683
|
+
for (const q of data.quality.results) {
|
|
10684
|
+
lines.push(`| ${q.specId} | ${q.percentage}% | ${q.grade} |`);
|
|
10685
|
+
}
|
|
10686
|
+
lines.push("");
|
|
10687
|
+
}
|
|
10688
|
+
if (data.validation) {
|
|
10689
|
+
lines.push("## \uAC80\uC99D \uACB0\uACFC");
|
|
10690
|
+
lines.push("");
|
|
10691
|
+
lines.push(`- \uAC80\uC99D\uB41C \uC2A4\uD399: ${data.validation.validCount}\uAC1C`);
|
|
10692
|
+
lines.push(`- \uC5D0\uB7EC: ${data.validation.errorCount}\uAC1C`);
|
|
10693
|
+
lines.push(`- \uACBD\uACE0: ${data.validation.warningCount}\uAC1C`);
|
|
10694
|
+
lines.push("");
|
|
10695
|
+
if (data.validation.errorCount > 0 || data.validation.warningCount > 0) {
|
|
10696
|
+
lines.push("### \uC0C1\uC138 \uACB0\uACFC");
|
|
10697
|
+
lines.push("");
|
|
10698
|
+
for (const v of data.validation.results) {
|
|
10699
|
+
if (v.errors.length > 0 || v.warnings.length > 0) {
|
|
10700
|
+
lines.push(`#### ${v.file}`);
|
|
10701
|
+
for (const e of v.errors) {
|
|
10702
|
+
lines.push(`- ${e}`);
|
|
10703
|
+
}
|
|
10704
|
+
for (const w of v.warnings) {
|
|
10705
|
+
lines.push(`- ${w}`);
|
|
10706
|
+
}
|
|
10707
|
+
lines.push("");
|
|
10708
|
+
}
|
|
10709
|
+
}
|
|
10710
|
+
}
|
|
10711
|
+
}
|
|
10712
|
+
lines.push("---");
|
|
10713
|
+
lines.push("*Generated by SDD CLI v0.5.0*");
|
|
10714
|
+
return lines.join("\n");
|
|
10715
|
+
}
|
|
10716
|
+
|
|
10717
|
+
// src/cli/commands/report.ts
|
|
10718
|
+
init_fs();
|
|
10719
|
+
init_errors();
|
|
10720
|
+
function registerReportCommand(program2) {
|
|
10721
|
+
program2.command("report").description("\uC2A4\uD399 \uB9AC\uD3EC\uD2B8\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4").option("-f, --format <format>", "\uCD9C\uB825 \uD615\uC2DD (html, markdown, json)", "html").option("-o, --output <path>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C").option("--title <title>", "\uB9AC\uD3EC\uD2B8 \uC81C\uBAA9").option("--no-quality", "\uD488\uC9C8 \uBD84\uC11D \uC81C\uC678").option("--no-validation", "\uAC80\uC99D \uACB0\uACFC \uC81C\uC678").action(async (options) => {
|
|
10722
|
+
try {
|
|
10723
|
+
await runReport(options);
|
|
10724
|
+
} catch (error2) {
|
|
10725
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
10726
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10727
|
+
}
|
|
10728
|
+
});
|
|
10729
|
+
}
|
|
10730
|
+
async function runReport(options) {
|
|
10731
|
+
const projectRoot = await findSddRoot();
|
|
10732
|
+
if (!projectRoot) {
|
|
10733
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
10734
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10735
|
+
}
|
|
10736
|
+
const sddPath = path26.join(projectRoot, ".sdd");
|
|
10737
|
+
const format = options.format || "html";
|
|
10738
|
+
if (!["html", "markdown", "json"].includes(format)) {
|
|
10739
|
+
error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${format}`);
|
|
10740
|
+
info("\uC9C0\uC6D0 \uD615\uC2DD: html, markdown, json");
|
|
10741
|
+
process.exit(ExitCode.VALIDATION_ERROR);
|
|
10742
|
+
}
|
|
10743
|
+
let outputPath = options.output;
|
|
10744
|
+
if (!outputPath) {
|
|
10745
|
+
const ext = format === "markdown" ? "md" : format;
|
|
10746
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
10747
|
+
outputPath = path26.join(projectRoot, `sdd-report-${timestamp}.${ext}`);
|
|
10748
|
+
} else if (!path26.isAbsolute(outputPath)) {
|
|
10749
|
+
outputPath = path26.join(projectRoot, outputPath);
|
|
10750
|
+
}
|
|
10751
|
+
info("\u{1F4CA} \uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uC911...");
|
|
10752
|
+
info(` \uD615\uC2DD: ${format}`);
|
|
10753
|
+
info(` \uD488\uC9C8 \uBD84\uC11D: ${options.quality !== false ? "\uD3EC\uD568" : "\uC81C\uC678"}`);
|
|
10754
|
+
info(` \uAC80\uC99D \uACB0\uACFC: ${options.validation !== false ? "\uD3EC\uD568" : "\uC81C\uC678"}`);
|
|
10755
|
+
newline();
|
|
10756
|
+
const result = await generateReport(sddPath, {
|
|
10757
|
+
format,
|
|
10758
|
+
outputPath,
|
|
10759
|
+
title: options.title,
|
|
10760
|
+
includeQuality: options.quality !== false,
|
|
10761
|
+
includeValidation: options.validation !== false
|
|
10762
|
+
});
|
|
10763
|
+
if (!result.success) {
|
|
10764
|
+
error(result.error.message);
|
|
10765
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
10766
|
+
}
|
|
10767
|
+
success2(`\u2705 \uB9AC\uD3EC\uD2B8\uAC00 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
|
|
10768
|
+
info(` \uACBD\uB85C: ${result.data.outputPath}`);
|
|
10769
|
+
if (!options.output && format === "json") {
|
|
10770
|
+
newline();
|
|
10771
|
+
console.log(result.data.content);
|
|
10772
|
+
}
|
|
10773
|
+
}
|
|
10774
|
+
|
|
9089
10775
|
// src/cli/index.ts
|
|
9090
10776
|
var require2 = createRequire(import.meta.url);
|
|
9091
10777
|
var pkg = require2("../../package.json");
|
|
@@ -9104,6 +10790,9 @@ registerStartCommand(program);
|
|
|
9104
10790
|
registerMigrateCommand(program);
|
|
9105
10791
|
registerCicdCommand(program);
|
|
9106
10792
|
registerTransitionCommand(program);
|
|
10793
|
+
registerWatchCommand(program);
|
|
10794
|
+
registerQualityCommand(program);
|
|
10795
|
+
registerReportCommand(program);
|
|
9107
10796
|
function run() {
|
|
9108
10797
|
program.parse();
|
|
9109
10798
|
}
|