lee-spec-kit 0.2.4 → 0.4.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/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import path7 from 'path';
2
+ import path4 from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { program } from 'commander';
5
- import prompts from 'prompts';
6
- import chalk from 'chalk';
7
5
  import fs6 from 'fs-extra';
6
+ import prompts from 'prompts';
7
+ import chalk6 from 'chalk';
8
8
  import { glob } from 'glob';
9
9
  import { spawn, execSync } from 'child_process';
10
10
  import os from 'os';
11
11
 
12
12
  var getFilename = () => fileURLToPath(import.meta.url);
13
- var getDirname = () => path7.dirname(getFilename());
13
+ var getDirname = () => path4.dirname(getFilename());
14
14
  var __dirname$1 = /* @__PURE__ */ getDirname();
15
15
  async function copyTemplates(src, dest) {
16
16
  await fs6.copy(src, dest, {
@@ -37,10 +37,10 @@ async function replaceInFiles(dir, replacements) {
37
37
  }
38
38
  }
39
39
  var __filename2 = fileURLToPath(import.meta.url);
40
- var __dirname2 = path7.dirname(__filename2);
40
+ var __dirname2 = path4.dirname(__filename2);
41
41
  function getTemplatesDir() {
42
- const rootDir = path7.resolve(__dirname2, "..");
43
- return path7.join(rootDir, "templates");
42
+ const rootDir = path4.resolve(__dirname2, "..");
43
+ return path4.join(rootDir, "templates");
44
44
  }
45
45
 
46
46
  // src/utils/validation.ts
@@ -63,7 +63,7 @@ function validateSafeName(name) {
63
63
  if (name.includes("\0")) {
64
64
  return { valid: false, error: "\uC774\uB984\uC5D0 null \uBB38\uC790\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." };
65
65
  }
66
- const safePattern = /^[\w가-힣\-]+$/;
66
+ const safePattern = /^[\w가-힣-]+$/;
67
67
  if (!safePattern.test(name)) {
68
68
  return {
69
69
  valid: false,
@@ -154,44 +154,45 @@ function initCommand(program2) {
154
154
  await runInit(options);
155
155
  } catch (error) {
156
156
  if (error instanceof Error && error.message === "canceled") {
157
- console.log(chalk.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
157
+ console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
158
158
  process.exit(0);
159
159
  }
160
- console.error(chalk.red("\uC624\uB958:"), error);
160
+ console.error(chalk6.red("\uC624\uB958:"), error);
161
161
  process.exit(1);
162
162
  }
163
163
  });
164
164
  }
165
165
  async function runInit(options) {
166
166
  const cwd = process.cwd();
167
- const defaultName = path7.basename(cwd);
167
+ const defaultName = path4.basename(cwd);
168
168
  let projectName = options.name || defaultName;
169
169
  let projectType = options.type;
170
170
  let lang = options.lang || "ko";
171
171
  let docsRepo = "embedded";
172
172
  let pushDocs;
173
173
  let docsRemote;
174
- const targetDir = path7.resolve(cwd, options.dir || "./docs");
174
+ let projectRoot;
175
+ const targetDir = path4.resolve(cwd, options.dir || "./docs");
175
176
  const isInsideGitRepo = checkGitRepo(cwd);
176
177
  if (!options.yes) {
177
178
  console.log();
178
- console.log(chalk.blue(`\u{1F4CD} \uD604\uC7AC \uC704\uCE58: ${cwd}`));
179
+ console.log(chalk6.blue(`\u{1F4CD} \uD604\uC7AC \uC704\uCE58: ${cwd}`));
179
180
  if (isInsideGitRepo) {
180
- console.log(chalk.green("\u2705 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0\uB428"));
181
+ console.log(chalk6.green("\u2705 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0\uB428"));
181
182
  console.log();
182
- console.log(chalk.gray("\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uB0B4\uC5D0\uC11C \uC2E4\uD589\uD558\uACE0 \uACC4\uC2ED\uB2C8\uB2E4."));
183
+ console.log(chalk6.gray("\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uB0B4\uC5D0\uC11C \uC2E4\uD589\uD558\uACE0 \uACC4\uC2ED\uB2C8\uB2E4."));
183
184
  console.log(
184
- chalk.gray(
185
+ chalk6.gray(
185
186
  "\u2022 embedded: \uC5EC\uAE30\uC5D0 ./docs \uD3F4\uB354\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4. \uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 \uAD00\uB9AC\uB429\uB2C8\uB2E4."
186
187
  )
187
188
  );
188
189
  console.log(
189
- chalk.gray("\u2022 standalone: \uBCC4\uB3C4 \uD3F4\uB354\uC5D0\uC11C \uB3C5\uB9BD docs \uB808\uD3EC\uB85C \uAD00\uB9AC\uD558\uB824\uBA74,")
190
+ chalk6.gray("\u2022 standalone: \uBCC4\uB3C4 \uD3F4\uB354\uC5D0\uC11C \uB3C5\uB9BD docs \uB808\uD3EC\uB85C \uAD00\uB9AC\uD558\uB824\uBA74,")
190
191
  );
191
- console.log(chalk.gray(" \uD574\uB2F9 \uD3F4\uB354\uB85C \uC774\uB3D9 \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694."));
192
+ console.log(chalk6.gray(" \uD574\uB2F9 \uD3F4\uB354\uB85C \uC774\uB3D9 \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694."));
192
193
  } else {
193
- console.log(chalk.yellow("\u26A0\uFE0F Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."));
194
- console.log(chalk.gray("\uC0C8\uB85C\uC6B4 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uC0DD\uC131\uB429\uB2C8\uB2E4."));
194
+ console.log(chalk6.yellow("\u26A0\uFE0F Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."));
195
+ console.log(chalk6.gray("\uC0C8\uB85C\uC6B4 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uC0DD\uC131\uB429\uB2C8\uB2E4."));
195
196
  }
196
197
  console.log();
197
198
  const response = await prompts(
@@ -260,6 +261,51 @@ async function runInit(options) {
260
261
  lang = response.lang || lang;
261
262
  docsRepo = response.docsRepo || "embedded";
262
263
  if (docsRepo === "standalone") {
264
+ const resolvedType = projectType || response.projectType || "single";
265
+ if (resolvedType === "fullstack") {
266
+ const projectRootResponse = await prompts(
267
+ [
268
+ {
269
+ type: "text",
270
+ name: "feRoot",
271
+ message: "Frontend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
272
+ validate: (value) => value.trim() ? true : "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694"
273
+ },
274
+ {
275
+ type: "text",
276
+ name: "beRoot",
277
+ message: "Backend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
278
+ validate: (value) => value.trim() ? true : "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694"
279
+ }
280
+ ],
281
+ {
282
+ onCancel: () => {
283
+ throw new Error("canceled");
284
+ }
285
+ }
286
+ );
287
+ projectRoot = {
288
+ fe: projectRootResponse.feRoot,
289
+ be: projectRootResponse.beRoot
290
+ };
291
+ } else {
292
+ const projectRootResponse = await prompts(
293
+ [
294
+ {
295
+ type: "text",
296
+ name: "projectRoot",
297
+ message: "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
298
+ validate: (value) => value.trim() ? true : "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694"
299
+ }
300
+ ],
301
+ {
302
+ onCancel: () => {
303
+ throw new Error("canceled");
304
+ }
305
+ }
306
+ );
307
+ projectRoot = projectRootResponse.projectRoot;
308
+ }
263
309
  const standaloneResponse = await prompts(
264
310
  [
265
311
  {
@@ -322,21 +368,21 @@ async function runInit(options) {
322
368
  initial: false
323
369
  });
324
370
  if (!overwrite) {
325
- console.log(chalk.yellow("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
371
+ console.log(chalk6.yellow("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
326
372
  return;
327
373
  }
328
374
  }
329
375
  }
330
376
  console.log();
331
- console.log(chalk.blue("\u{1F4C1} docs \uAD6C\uC870 \uC0DD\uC131 \uC911..."));
332
- console.log(chalk.gray(` \uD504\uB85C\uC81D\uD2B8: ${projectName}`));
333
- console.log(chalk.gray(` \uD0C0\uC785: ${projectType}`));
334
- console.log(chalk.gray(` \uC5B8\uC5B4: ${lang}`));
335
- console.log(chalk.gray(` \uACBD\uB85C: ${targetDir}`));
377
+ console.log(chalk6.blue("\u{1F4C1} docs \uAD6C\uC870 \uC0DD\uC131 \uC911..."));
378
+ console.log(chalk6.gray(` \uD504\uB85C\uC81D\uD2B8: ${projectName}`));
379
+ console.log(chalk6.gray(` \uD0C0\uC785: ${projectType}`));
380
+ console.log(chalk6.gray(` \uC5B8\uC5B4: ${lang}`));
381
+ console.log(chalk6.gray(` \uACBD\uB85C: ${targetDir}`));
336
382
  console.log();
337
383
  const templatesDir = getTemplatesDir();
338
- const commonPath = path7.join(templatesDir, lang, "common");
339
- const typePath = path7.join(templatesDir, lang, projectType);
384
+ const commonPath = path4.join(templatesDir, lang, "common");
385
+ const typePath = path4.join(templatesDir, lang, projectType);
340
386
  if (await fs6.pathExists(commonPath)) {
341
387
  await copyTemplates(commonPath, targetDir);
342
388
  }
@@ -363,16 +409,19 @@ async function runInit(options) {
363
409
  if (pushDocs && docsRemote) {
364
410
  config.docsRemote = docsRemote;
365
411
  }
412
+ if (projectRoot) {
413
+ config.projectRoot = projectRoot;
414
+ }
366
415
  }
367
- const configPath = path7.join(targetDir, ".lee-spec-kit.json");
416
+ const configPath = path4.join(targetDir, ".lee-spec-kit.json");
368
417
  await fs6.writeJson(configPath, config, { spaces: 2 });
369
- console.log(chalk.green("\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!"));
418
+ console.log(chalk6.green("\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!"));
370
419
  console.log();
371
420
  await initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote);
372
- console.log(chalk.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
373
- console.log(chalk.gray(` 1. ${targetDir}/prd/README.md \uC791\uC131`));
421
+ console.log(chalk6.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
422
+ console.log(chalk6.gray(` 1. ${targetDir}/prd/README.md \uC791\uC131`));
374
423
  console.log(
375
- chalk.gray(" 2. npx lee-spec-kit feature <name> \uC73C\uB85C \uAE30\uB2A5 \uCD94\uAC00")
424
+ chalk6.gray(" 2. npx lee-spec-kit feature <name> \uC73C\uB85C \uAE30\uB2A5 \uCD94\uAC00")
376
425
  );
377
426
  console.log();
378
427
  }
@@ -383,12 +432,12 @@ async function initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote) {
383
432
  cwd,
384
433
  stdio: "ignore"
385
434
  });
386
- console.log(chalk.blue("\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911..."));
435
+ console.log(chalk6.blue("\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911..."));
387
436
  } catch {
388
- console.log(chalk.blue("\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911..."));
437
+ console.log(chalk6.blue("\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911..."));
389
438
  execSync("git init", { cwd, stdio: "ignore" });
390
439
  }
391
- const relativePath = path7.relative(cwd, targetDir);
440
+ const relativePath = path4.relative(cwd, targetDir);
392
441
  execSync(`git add "${relativePath}"`, { cwd, stdio: "ignore" });
393
442
  execSync('git commit -m "init: docs \uAD6C\uC870 \uCD08\uAE30\uD654 (lee-spec-kit)"', {
394
443
  cwd,
@@ -400,58 +449,81 @@ async function initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote) {
400
449
  cwd,
401
450
  stdio: "ignore"
402
451
  });
403
- console.log(chalk.green(`\u2705 Git remote \uC124\uC815 \uC644\uB8CC: ${docsRemote}`));
452
+ console.log(chalk6.green(`\u2705 Git remote \uC124\uC815 \uC644\uB8CC: ${docsRemote}`));
404
453
  } catch {
405
- console.log(chalk.yellow("\u26A0\uFE0F Git remote\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4."));
454
+ console.log(chalk6.yellow("\u26A0\uFE0F Git remote\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4."));
406
455
  }
407
456
  }
408
- console.log(chalk.green("\u2705 Git \uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC!"));
457
+ console.log(chalk6.green("\u2705 Git \uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC!"));
409
458
  console.log();
410
- } catch (error) {
459
+ } catch {
411
460
  console.log(
412
- chalk.yellow("\u26A0\uFE0F Git \uCD08\uAE30\uD654\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 (\uC218\uB3D9\uC73C\uB85C \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694)")
461
+ chalk6.yellow("\u26A0\uFE0F Git \uCD08\uAE30\uD654\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 (\uC218\uB3D9\uC73C\uB85C \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694)")
413
462
  );
414
463
  console.log();
415
464
  }
416
465
  }
466
+ function getAncestorDirs(startDir) {
467
+ const dirs = [];
468
+ let current = path4.resolve(startDir);
469
+ while (true) {
470
+ dirs.push(current);
471
+ const parent = path4.dirname(current);
472
+ if (parent === current) break;
473
+ current = parent;
474
+ }
475
+ return dirs;
476
+ }
417
477
  async function getConfig(cwd) {
418
- const possibleDirs = [
419
- path7.join(cwd, "docs"),
420
- cwd
421
- // 이미 docs 폴더 안에 있을 수 있음
478
+ const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || process.env.LSK_DOCS_DIR || "").trim();
479
+ const baseDirs = [
480
+ ...explicitDocsDir ? [path4.resolve(explicitDocsDir)] : [],
481
+ ...getAncestorDirs(cwd)
422
482
  ];
423
- for (const docsDir of possibleDirs) {
424
- const configPath = path7.join(docsDir, ".lee-spec-kit.json");
425
- if (await fs6.pathExists(configPath)) {
426
- try {
427
- const configFile = await fs6.readJson(configPath);
428
- return {
429
- docsDir,
430
- projectName: configFile.projectName,
431
- projectType: configFile.projectType,
432
- lang: configFile.lang,
433
- docsRepo: configFile.docsRepo,
434
- pushDocs: configFile.pushDocs,
435
- docsRemote: configFile.docsRemote
436
- };
437
- } catch {
483
+ const visitedBaseDirs = /* @__PURE__ */ new Set();
484
+ const visitedDocsDirs = /* @__PURE__ */ new Set();
485
+ for (const baseDir of baseDirs) {
486
+ const resolvedBaseDir = path4.resolve(baseDir);
487
+ if (visitedBaseDirs.has(resolvedBaseDir)) continue;
488
+ visitedBaseDirs.add(resolvedBaseDir);
489
+ const possibleDocsDirs = [path4.join(resolvedBaseDir, "docs"), resolvedBaseDir];
490
+ for (const docsDir of possibleDocsDirs) {
491
+ const resolvedDocsDir = path4.resolve(docsDir);
492
+ if (visitedDocsDirs.has(resolvedDocsDir)) continue;
493
+ visitedDocsDirs.add(resolvedDocsDir);
494
+ const configPath = path4.join(resolvedDocsDir, ".lee-spec-kit.json");
495
+ if (await fs6.pathExists(configPath)) {
496
+ try {
497
+ const configFile = await fs6.readJson(configPath);
498
+ return {
499
+ docsDir: resolvedDocsDir,
500
+ projectName: configFile.projectName,
501
+ projectType: configFile.projectType,
502
+ lang: configFile.lang,
503
+ docsRepo: configFile.docsRepo,
504
+ pushDocs: configFile.pushDocs,
505
+ docsRemote: configFile.docsRemote,
506
+ projectRoot: configFile.projectRoot
507
+ };
508
+ } catch {
509
+ }
438
510
  }
439
- }
440
- const agentsPath = path7.join(docsDir, "agents");
441
- const featuresPath = path7.join(docsDir, "features");
442
- if (await fs6.pathExists(agentsPath) && await fs6.pathExists(featuresPath)) {
443
- const bePath = path7.join(featuresPath, "be");
444
- const fePath = path7.join(featuresPath, "fe");
445
- const projectType = await fs6.pathExists(bePath) || await fs6.pathExists(fePath) ? "fullstack" : "single";
446
- const agentsMdPath = path7.join(agentsPath, "agents.md");
447
- let lang = "ko";
448
- if (await fs6.pathExists(agentsMdPath)) {
449
- const content = await fs6.readFile(agentsMdPath, "utf-8");
450
- if (!/[가-힣]/.test(content)) {
451
- lang = "en";
511
+ const agentsPath = path4.join(resolvedDocsDir, "agents");
512
+ const featuresPath = path4.join(resolvedDocsDir, "features");
513
+ if (await fs6.pathExists(agentsPath) && await fs6.pathExists(featuresPath)) {
514
+ const bePath = path4.join(featuresPath, "be");
515
+ const fePath = path4.join(featuresPath, "fe");
516
+ const projectType = await fs6.pathExists(bePath) || await fs6.pathExists(fePath) ? "fullstack" : "single";
517
+ const agentsMdPath = path4.join(agentsPath, "agents.md");
518
+ let lang = "ko";
519
+ if (await fs6.pathExists(agentsMdPath)) {
520
+ const content = await fs6.readFile(agentsMdPath, "utf-8");
521
+ if (!/[가-힣]/.test(content)) {
522
+ lang = "en";
523
+ }
452
524
  }
525
+ return { docsDir: resolvedDocsDir, projectType, lang };
453
526
  }
454
- return { docsDir, projectType, lang };
455
527
  }
456
528
  }
457
529
  return null;
@@ -464,10 +536,10 @@ function featureCommand(program2) {
464
536
  await runFeature(name, options);
465
537
  } catch (error) {
466
538
  if (error instanceof Error && error.message === "canceled") {
467
- console.log(chalk.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
539
+ console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
468
540
  process.exit(0);
469
541
  }
470
- console.error(chalk.red("\uC624\uB958:"), error);
542
+ console.error(chalk6.red("\uC624\uB958:"), error);
471
543
  process.exit(1);
472
544
  }
473
545
  });
@@ -477,7 +549,7 @@ async function runFeature(name, options) {
477
549
  const config = await getConfig(cwd);
478
550
  if (!config) {
479
551
  console.error(
480
- chalk.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
552
+ chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
481
553
  );
482
554
  process.exit(1);
483
555
  }
@@ -515,19 +587,19 @@ async function runFeature(name, options) {
515
587
  }
516
588
  let featuresDir;
517
589
  if (projectType === "fullstack" && repo) {
518
- featuresDir = path7.join(docsDir, "features", repo);
590
+ featuresDir = path4.join(docsDir, "features", repo);
519
591
  } else {
520
- featuresDir = path7.join(docsDir, "features");
592
+ featuresDir = path4.join(docsDir, "features");
521
593
  }
522
594
  const featureFolderName = `${featureId}-${name}`;
523
- const featureDir = path7.join(featuresDir, featureFolderName);
595
+ const featureDir = path4.join(featuresDir, featureFolderName);
524
596
  if (await fs6.pathExists(featureDir)) {
525
- console.error(chalk.red(`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: ${featureDir}`));
597
+ console.error(chalk6.red(`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: ${featureDir}`));
526
598
  process.exit(1);
527
599
  }
528
- const featureBasePath = path7.join(docsDir, "features", "feature-base");
600
+ const featureBasePath = path4.join(docsDir, "features", "feature-base");
529
601
  if (!await fs6.pathExists(featureBasePath)) {
530
- console.error(chalk.red("feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
602
+ console.error(chalk6.red("feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
531
603
  process.exit(1);
532
604
  }
533
605
  await fs6.copy(featureBasePath, featureDir);
@@ -552,21 +624,21 @@ async function runFeature(name, options) {
552
624
  }
553
625
  await replaceInFiles(featureDir, replacements);
554
626
  console.log();
555
- console.log(chalk.green(`\u2705 Feature \uD3F4\uB354 \uC0DD\uC131 \uC644\uB8CC: ${featureDir}`));
627
+ console.log(chalk6.green(`\u2705 Feature \uD3F4\uB354 \uC0DD\uC131 \uC644\uB8CC: ${featureDir}`));
556
628
  console.log();
557
- console.log(chalk.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
558
- console.log(chalk.gray(` 1. ${featureDir}/spec.md \uC791\uC131`));
559
- console.log(chalk.gray(" 2. \uC0AC\uC6A9\uC790 \uB9AC\uBDF0 \uC694\uCCAD"));
560
- console.log(chalk.gray(" 3. \uC2B9\uC778 \uD6C4 plan.md \uC791\uC131"));
629
+ console.log(chalk6.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
630
+ console.log(chalk6.gray(` 1. ${featureDir}/spec.md \uC791\uC131`));
631
+ console.log(chalk6.gray(" 2. \uC0AC\uC6A9\uC790 \uB9AC\uBDF0 \uC694\uCCAD"));
632
+ console.log(chalk6.gray(" 3. \uC2B9\uC778 \uD6C4 plan.md \uC791\uC131"));
561
633
  console.log();
562
634
  }
563
635
  async function getNextFeatureId(docsDir, projectType) {
564
- const featuresDir = path7.join(docsDir, "features");
636
+ const featuresDir = path4.join(docsDir, "features");
565
637
  let max = 0;
566
638
  const scanDirs = [];
567
639
  if (projectType === "fullstack") {
568
- scanDirs.push(path7.join(featuresDir, "be"));
569
- scanDirs.push(path7.join(featuresDir, "fe"));
640
+ scanDirs.push(path4.join(featuresDir, "be"));
641
+ scanDirs.push(path4.join(featuresDir, "fe"));
570
642
  } else {
571
643
  scanDirs.push(featuresDir);
572
644
  }
@@ -591,7 +663,7 @@ function statusCommand(program2) {
591
663
  try {
592
664
  await runStatus(options);
593
665
  } catch (error) {
594
- console.error(chalk.red("\uC624\uB958:"), error);
666
+ console.error(chalk6.red("\uC624\uB958:"), error);
595
667
  process.exit(1);
596
668
  }
597
669
  });
@@ -601,25 +673,25 @@ async function runStatus(options) {
601
673
  const config = await getConfig(cwd);
602
674
  if (!config) {
603
675
  console.error(
604
- chalk.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
676
+ chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
605
677
  );
606
678
  process.exit(1);
607
679
  }
608
680
  const { docsDir, projectType } = config;
609
- const featuresDir = path7.join(docsDir, "features");
681
+ const featuresDir = path4.join(docsDir, "features");
610
682
  const features = [];
611
683
  const idMap = /* @__PURE__ */ new Map();
612
684
  const scopes = projectType === "fullstack" ? ["be", "fe"] : [""];
613
685
  for (const scope of scopes) {
614
- const scanDir = scope ? path7.join(featuresDir, scope) : featuresDir;
686
+ const scanDir = scope ? path4.join(featuresDir, scope) : featuresDir;
615
687
  if (!await fs6.pathExists(scanDir)) continue;
616
688
  const entries = await fs6.readdir(scanDir, { withFileTypes: true });
617
689
  for (const entry of entries) {
618
690
  if (!entry.isDirectory()) continue;
619
691
  if (entry.name === "feature-base") continue;
620
- const featureDir = path7.join(scanDir, entry.name);
621
- const specPath = path7.join(featureDir, "spec.md");
622
- const tasksPath = path7.join(featureDir, "tasks.md");
692
+ const featureDir = path4.join(scanDir, entry.name);
693
+ const specPath = path4.join(featureDir, "spec.md");
694
+ const tasksPath = path4.join(featureDir, "tasks.md");
623
695
  if (!await fs6.pathExists(specPath)) continue;
624
696
  if (!await fs6.pathExists(tasksPath)) continue;
625
697
  const specContent = await fs6.readFile(specPath, "utf-8");
@@ -628,7 +700,7 @@ async function runStatus(options) {
628
700
  const name = extractSpecValue(specContent, "\uAE30\uB2A5\uBA85") || extractSpecValue(specContent, "Feature Name") || entry.name;
629
701
  const repo = extractSpecValue(specContent, "\uB300\uC0C1 \uB808\uD3EC") || extractSpecValue(specContent, "Target Repo") || (scope ? `{{projectName}}-${scope}` : "{{projectName}}");
630
702
  const issue = extractSpecValue(specContent, "\uC774\uC288 \uBC88\uD638") || extractSpecValue(specContent, "Issue Number") || "-";
631
- const relPath = path7.relative(docsDir, featureDir);
703
+ const relPath = path4.relative(docsDir, featureDir);
632
704
  if (!idMap.has(id)) {
633
705
  idMap.set(id, []);
634
706
  }
@@ -656,7 +728,7 @@ async function runStatus(options) {
656
728
  }
657
729
  }
658
730
  if (features.length === 0) {
659
- console.log(chalk.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
731
+ console.log(chalk6.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
660
732
  return;
661
733
  }
662
734
  if (options.strict) {
@@ -664,21 +736,21 @@ async function runStatus(options) {
664
736
  ([, paths]) => paths.length > 1
665
737
  );
666
738
  if (duplicates.length > 0) {
667
- console.error(chalk.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
739
+ console.error(chalk6.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
668
740
  for (const [id, paths] of duplicates) {
669
- console.error(chalk.red(` ${id}:`));
741
+ console.error(chalk6.red(` ${id}:`));
670
742
  for (const p of paths) {
671
- console.error(chalk.red(` - ${p}`));
743
+ console.error(chalk6.red(` - ${p}`));
672
744
  }
673
745
  }
674
746
  process.exit(1);
675
747
  }
676
748
  const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
677
749
  if (unknowns.length > 0) {
678
- console.error(chalk.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
750
+ console.error(chalk6.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
679
751
  for (const [, paths] of unknowns) {
680
752
  for (const p of paths) {
681
- console.error(chalk.red(` - ${p}`));
753
+ console.error(chalk6.red(` - ${p}`));
682
754
  }
683
755
  }
684
756
  process.exit(1);
@@ -691,14 +763,14 @@ async function runStatus(options) {
691
763
  console.log(header);
692
764
  console.log(separator);
693
765
  for (const f of features) {
694
- const statusColor = f.status === "DONE" ? chalk.green : f.status === "DOING" ? chalk.yellow : chalk.gray;
766
+ const statusColor = f.status === "DONE" ? chalk6.green : f.status === "DOING" ? chalk6.yellow : chalk6.gray;
695
767
  console.log(
696
768
  `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
697
769
  );
698
770
  }
699
771
  console.log();
700
772
  if (options.write) {
701
- const outputPath = path7.join(featuresDir, "status.md");
773
+ const outputPath = path4.join(featuresDir, "status.md");
702
774
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
703
775
  const content = [
704
776
  "# Feature Status",
@@ -714,7 +786,7 @@ async function runStatus(options) {
714
786
  ""
715
787
  ].join("\n");
716
788
  await fs6.writeFile(outputPath, content, "utf-8");
717
- console.log(chalk.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
789
+ console.log(chalk6.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
718
790
  }
719
791
  }
720
792
  function extractSpecValue(content, key) {
@@ -746,10 +818,10 @@ function updateCommand(program2) {
746
818
  await runUpdate(options);
747
819
  } catch (error) {
748
820
  if (error instanceof Error && error.message === "canceled") {
749
- console.log(chalk.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
821
+ console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
750
822
  process.exit(0);
751
823
  }
752
- console.error(chalk.red("\uC624\uB958:"), error);
824
+ console.error(chalk6.red("\uC624\uB958:"), error);
753
825
  process.exit(1);
754
826
  }
755
827
  });
@@ -759,25 +831,25 @@ async function runUpdate(options) {
759
831
  const config = await getConfig(cwd);
760
832
  if (!config) {
761
833
  console.error(
762
- chalk.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
834
+ chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
763
835
  );
764
836
  process.exit(1);
765
837
  }
766
838
  const { docsDir, projectType, lang } = config;
767
839
  const templatesDir = getTemplatesDir();
768
- const sourceDir = path7.join(templatesDir, lang, projectType);
840
+ const sourceDir = path4.join(templatesDir, lang, projectType);
769
841
  const updateAgents = options.agents || !options.agents && !options.templates;
770
842
  const updateTemplates = options.templates || !options.agents && !options.templates;
771
- console.log(chalk.blue("\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4..."));
772
- console.log(chalk.gray(` - \uC5B8\uC5B4: ${lang}`));
773
- console.log(chalk.gray(` - \uD0C0\uC785: ${projectType}`));
843
+ console.log(chalk6.blue("\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4..."));
844
+ console.log(chalk6.gray(` - \uC5B8\uC5B4: ${lang}`));
845
+ console.log(chalk6.gray(` - \uD0C0\uC785: ${projectType}`));
774
846
  console.log();
775
847
  let updatedCount = 0;
776
848
  if (updateAgents) {
777
- console.log(chalk.blue("\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
778
- const commonAgents = path7.join(templatesDir, lang, "common", "agents");
779
- const typeAgents = path7.join(templatesDir, lang, projectType, "agents");
780
- const targetAgents = path7.join(docsDir, "agents");
849
+ console.log(chalk6.blue("\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
850
+ const commonAgents = path4.join(templatesDir, lang, "common", "agents");
851
+ const typeAgents = path4.join(templatesDir, lang, projectType, "agents");
852
+ const targetAgents = path4.join(docsDir, "agents");
781
853
  const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
782
854
  const replacements = {
783
855
  "{{featurePath}}": featurePath
@@ -795,12 +867,12 @@ async function runUpdate(options) {
795
867
  const count = await updateFolder(typeAgents, targetAgents, options.force);
796
868
  updatedCount += count;
797
869
  }
798
- console.log(chalk.green(` \u2705 agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
870
+ console.log(chalk6.green(` \u2705 agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
799
871
  }
800
872
  if (updateTemplates) {
801
- console.log(chalk.blue("\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
802
- const sourceFeatureBase = path7.join(sourceDir, "features", "feature-base");
803
- const targetFeatureBase = path7.join(docsDir, "features", "feature-base");
873
+ console.log(chalk6.blue("\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
874
+ const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
875
+ const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
804
876
  if (await fs6.pathExists(sourceFeatureBase)) {
805
877
  const count = await updateFolder(
806
878
  sourceFeatureBase,
@@ -808,22 +880,23 @@ async function runUpdate(options) {
808
880
  options.force
809
881
  );
810
882
  updatedCount += count;
811
- console.log(chalk.green(` \u2705 ${count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
883
+ console.log(chalk6.green(` \u2705 ${count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
812
884
  }
813
885
  }
814
886
  console.log();
815
- console.log(chalk.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
887
+ console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
816
888
  }
817
889
  async function updateFolder(sourceDir, targetDir, force, replacements) {
890
+ const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
818
891
  await fs6.ensureDir(targetDir);
819
892
  const files = await fs6.readdir(sourceDir);
820
893
  let updatedCount = 0;
821
894
  for (const file of files) {
822
- const sourcePath = path7.join(sourceDir, file);
823
- const targetPath = path7.join(targetDir, file);
895
+ const sourcePath = path4.join(sourceDir, file);
896
+ const targetPath = path4.join(targetDir, file);
824
897
  const stat = await fs6.stat(sourcePath);
825
898
  if (stat.isFile()) {
826
- if (file === "custom.md") {
899
+ if (protectedFiles.has(file)) {
827
900
  continue;
828
901
  }
829
902
  let sourceContent = await fs6.readFile(sourcePath, "utf-8");
@@ -840,14 +913,14 @@ async function updateFolder(sourceDir, targetDir, force, replacements) {
840
913
  }
841
914
  if (!force) {
842
915
  console.log(
843
- chalk.yellow(` \u26A0\uFE0F ${file} - \uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)`)
916
+ chalk6.yellow(` \u26A0\uFE0F ${file} - \uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)`)
844
917
  );
845
918
  shouldUpdate = false;
846
919
  }
847
920
  }
848
921
  if (shouldUpdate) {
849
922
  await fs6.writeFile(targetPath, sourceContent);
850
- console.log(chalk.gray(` \u{1F4C4} ${file} \uC5C5\uB370\uC774\uD2B8`));
923
+ console.log(chalk6.gray(` \u{1F4C4} ${file} \uC5C5\uB370\uC774\uD2B8`));
851
924
  updatedCount++;
852
925
  }
853
926
  } else if (stat.isDirectory()) {
@@ -862,11 +935,1181 @@ async function updateFolder(sourceDir, targetDir, force, replacements) {
862
935
  }
863
936
  return updatedCount;
864
937
  }
865
- var CACHE_FILE = path7.join(os.homedir(), ".lee-spec-kit-version-cache.json");
938
+ function configCommand(program2) {
939
+ program2.command("config").description("View or modify project configuration").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Repository type for fullstack: fe | be").action(async (options) => {
940
+ try {
941
+ await runConfig(options);
942
+ } catch (error) {
943
+ if (error instanceof Error && error.message === "canceled") {
944
+ console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
945
+ process.exit(0);
946
+ }
947
+ console.error(chalk6.red("\uC624\uB958:"), error);
948
+ process.exit(1);
949
+ }
950
+ });
951
+ }
952
+ async function runConfig(options) {
953
+ const cwd = process.cwd();
954
+ const config = await getConfig(cwd);
955
+ if (!config) {
956
+ console.log(
957
+ chalk6.red("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.")
958
+ );
959
+ process.exit(1);
960
+ }
961
+ const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
962
+ if (!options.projectRoot) {
963
+ console.log();
964
+ console.log(chalk6.blue("\u{1F4CB} \uD604\uC7AC \uC124\uC815:"));
965
+ console.log();
966
+ console.log(chalk6.gray(` \uACBD\uB85C: ${configPath}`));
967
+ console.log();
968
+ const configFile2 = await fs6.readJson(configPath);
969
+ console.log(JSON.stringify(configFile2, null, 2));
970
+ console.log();
971
+ return;
972
+ }
973
+ const configFile = await fs6.readJson(configPath);
974
+ if (configFile.docsRepo !== "standalone") {
975
+ console.log(
976
+ chalk6.yellow("\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.")
977
+ );
978
+ return;
979
+ }
980
+ const projectType = configFile.projectType;
981
+ if (projectType === "fullstack") {
982
+ if (!options.repo) {
983
+ const response = await prompts(
984
+ [
985
+ {
986
+ type: "select",
987
+ name: "repo",
988
+ message: "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
989
+ choices: [
990
+ { title: "Frontend (fe)", value: "fe" },
991
+ { title: "Backend (be)", value: "be" }
992
+ ]
993
+ }
994
+ ],
995
+ {
996
+ onCancel: () => {
997
+ throw new Error("canceled");
998
+ }
999
+ }
1000
+ );
1001
+ options.repo = response.repo;
1002
+ }
1003
+ if (!options.repo || !["fe", "be"].includes(options.repo)) {
1004
+ console.log(
1005
+ chalk6.red(
1006
+ "Fullstack \uD504\uB85C\uC81D\uD2B8\uB294 --repo fe \uB610\uB294 --repo be\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
1007
+ )
1008
+ );
1009
+ return;
1010
+ }
1011
+ const currentRoot = configFile.projectRoot || { fe: "", be: "" };
1012
+ if (typeof currentRoot === "string") {
1013
+ configFile.projectRoot = {
1014
+ fe: options.repo === "fe" ? options.projectRoot : "",
1015
+ be: options.repo === "be" ? options.projectRoot : ""
1016
+ };
1017
+ } else {
1018
+ currentRoot[options.repo] = options.projectRoot;
1019
+ configFile.projectRoot = currentRoot;
1020
+ }
1021
+ console.log(
1022
+ chalk6.green(
1023
+ `\u2705 ${options.repo.toUpperCase()} projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`
1024
+ )
1025
+ );
1026
+ } else {
1027
+ configFile.projectRoot = options.projectRoot;
1028
+ console.log(
1029
+ chalk6.green(`\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`)
1030
+ );
1031
+ }
1032
+ await fs6.writeJson(configPath, configFile, { spaces: 2 });
1033
+ console.log();
1034
+ }
1035
+
1036
+ // src/utils/context/i18n.ts
1037
+ function formatTemplate(template, vars) {
1038
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
1039
+ const value = vars[key];
1040
+ return value === void 0 ? `{${key}}` : String(value);
1041
+ });
1042
+ }
1043
+ var I18N = {
1044
+ ko: {
1045
+ steps: {
1046
+ featureFolder: "Feature \uD3F4\uB354 \uC0DD\uC131",
1047
+ specWrite: "spec.md \uC791\uC131",
1048
+ specApprove: "spec.md \uC2B9\uC778",
1049
+ planWrite: "plan.md \uC791\uC131",
1050
+ planApprove: "plan.md \uC2B9\uC778",
1051
+ tasksWrite: "tasks.md \uC791\uC131",
1052
+ docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uAE30\uD68D)",
1053
+ issueCreate: "GitHub Issue \uC0DD\uC131",
1054
+ branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
1055
+ tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
1056
+ prCreate: "PR \uC0DD\uC131",
1057
+ codeReview: "\uCF54\uB4DC \uB9AC\uBDF0",
1058
+ featureDone: "Feature \uC644\uB8CC"
1059
+ },
1060
+ messages: {
1061
+ specCreate: "spec.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/spec.md \uCC38\uACE0)",
1062
+ specImprove: "spec.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
1063
+ specApproval: "spec.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1064
+ planCreate: "plan.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/plan.md \uCC38\uACE0)",
1065
+ planImprove: "plan.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
1066
+ planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1067
+ tasksCreate: "tasks.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694. (features/feature-base/tasks.md \uCC38\uACE0)",
1068
+ tasksNeedAtLeastOne: "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.",
1069
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uAE30\uD68D \uBB38\uC11C \uC791\uC131"',
1070
+ issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
1071
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uC774\uC288 #{issueNumber} \uBC18\uC601"',
1072
+ standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
1073
+ createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1074
+ tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
1075
+ tasksAllDoneButChecklist: "\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uAC00 \uC644\uC804\uD788 \uCCB4\uD06C\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. ({checked}/{total})",
1076
+ finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
1077
+ startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
1078
+ checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
1079
+ prLegacyAsk: "tasks.md\uC5D0 PR/PR \uC0C1\uD0DC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD15C\uD50C\uB9BF\uC744 \uCD5C\uC2E0 \uD3EC\uB9F7\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD560\uAE4C\uC694? (OK \uD544\uC694)",
1080
+ prCreate: "PR\uC744 \uC0DD\uC131\uD558\uACE0 tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694. (skills/create-pr.md \uCC38\uACE0)",
1081
+ prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uACE0 PR \uC0C1\uD0DC\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR \uC0C1\uD0DC: Review \u2192 Approved)",
1082
+ prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.",
1083
+ featureDone: "PR\uC774 Approved\uC774\uACE0 \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
1084
+ fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
1085
+ },
1086
+ warnings: {
1087
+ projectBranchUnavailable: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.)",
1088
+ docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
1089
+ legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694."
1090
+ }
1091
+ },
1092
+ en: {
1093
+ steps: {
1094
+ featureFolder: "Create feature folder",
1095
+ specWrite: "Write spec.md",
1096
+ specApprove: "Approve spec.md",
1097
+ planWrite: "Write plan.md",
1098
+ planApprove: "Approve plan.md",
1099
+ tasksWrite: "Write tasks.md",
1100
+ docsCommitPlanning: "Commit planning docs",
1101
+ issueCreate: "Create GitHub Issue",
1102
+ branchCreate: "Create branch",
1103
+ tasksExecute: "Execute tasks",
1104
+ prCreate: "Create PR",
1105
+ codeReview: "Code review",
1106
+ featureDone: "Feature done"
1107
+ },
1108
+ messages: {
1109
+ specCreate: "Copy the spec.md template and write it. (See features/feature-base/spec.md)",
1110
+ specImprove: "Improve spec.md and set Status to Review.",
1111
+ specApproval: "Share spec.md with the user and get approval (OK).",
1112
+ planCreate: "Copy the plan.md template and write it. (See features/feature-base/plan.md)",
1113
+ planImprove: "Improve plan.md and set Status to Review.",
1114
+ planApproval: "Share plan.md with the user and get approval (OK).",
1115
+ tasksCreate: "Copy the tasks.md template and write tasks. (See features/feature-base/tasks.md)",
1116
+ tasksNeedAtLeastOne: "Add at least one task to tasks.md.",
1117
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): planning docs"',
1118
+ issueCreateAndWrite: "Create a GitHub Issue, then fill in the issue number in spec.md/tasks.md and prepare to commit docs. (See skills/create-issue.md)",
1119
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): issue #{issueNumber}"',
1120
+ standaloneNeedsProjectRoot: "In standalone mode, projectRoot is required. (npx lee-spec-kit config --project-root ...)",
1121
+ createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1122
+ tasksAllDoneButNoChecklist: 'All tasks are DONE but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
1123
+ tasksAllDoneButChecklist: "All tasks are DONE but the completion checklist is not fully checked. ({checked}/{total})",
1124
+ finishDoingTask: 'Finish the active DOING/REVIEW task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
1125
+ startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
1126
+ checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
1127
+ prLegacyAsk: "Legacy tasks.md format detected (missing PR/PR Status fields). Update to the latest format? (OK required)",
1128
+ prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
1129
+ prResolveReview: "Resolve review comments and update PR status. (PR Status: Review \u2192 Approved)",
1130
+ prRequestReview: "Request reviews and update PR status to Review.",
1131
+ featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
1132
+ fallbackRerunContext: "Unable to determine current state. Verify docs and run context again."
1133
+ },
1134
+ warnings: {
1135
+ projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
1136
+ docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
1137
+ legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps."
1138
+ }
1139
+ }
1140
+ };
1141
+ function tr(lang, category, key, vars = {}) {
1142
+ const template = I18N[lang][category][key] ?? I18N.ko[category][key] ?? `${category}.${key}`;
1143
+ return formatTemplate(template, vars);
1144
+ }
1145
+
1146
+ // src/utils/context/steps.ts
1147
+ function isCompletionChecklistDone(feature) {
1148
+ return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1149
+ }
1150
+ function isPrMetadataConfigured(feature) {
1151
+ return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1152
+ }
1153
+ function isFeatureDone(feature) {
1154
+ return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
1155
+ }
1156
+ function getStepDefinitions(lang) {
1157
+ return [
1158
+ {
1159
+ step: 1,
1160
+ name: tr(lang, "steps", "featureFolder"),
1161
+ checklist: { done: () => true }
1162
+ },
1163
+ {
1164
+ step: 2,
1165
+ name: tr(lang, "steps", "specWrite"),
1166
+ checklist: {
1167
+ done: (f) => f.specStatus === "Review" || f.specStatus === "Approved"
1168
+ },
1169
+ current: {
1170
+ when: (f) => !f.docs.specExists || !f.specStatus || f.specStatus === "Draft",
1171
+ actions: (f) => [
1172
+ {
1173
+ type: "instruction",
1174
+ message: !f.docs.specExists ? tr(lang, "messages", "specCreate") : tr(lang, "messages", "specImprove")
1175
+ }
1176
+ ]
1177
+ }
1178
+ },
1179
+ {
1180
+ step: 3,
1181
+ name: tr(lang, "steps", "specApprove"),
1182
+ checklist: { done: (f) => f.specStatus === "Approved" },
1183
+ current: {
1184
+ when: (f) => f.specStatus === "Review",
1185
+ actions: () => [
1186
+ {
1187
+ type: "instruction",
1188
+ requiresUserOk: true,
1189
+ message: tr(lang, "messages", "specApproval")
1190
+ }
1191
+ ]
1192
+ }
1193
+ },
1194
+ {
1195
+ step: 4,
1196
+ name: tr(lang, "steps", "planWrite"),
1197
+ checklist: {
1198
+ done: (f) => f.planStatus === "Review" || f.planStatus === "Approved"
1199
+ },
1200
+ current: {
1201
+ when: (f) => f.specStatus === "Approved" && (!f.docs.planExists || !f.planStatus || f.planStatus === "Draft"),
1202
+ actions: (f) => [
1203
+ {
1204
+ type: "instruction",
1205
+ message: !f.docs.planExists ? tr(lang, "messages", "planCreate") : tr(lang, "messages", "planImprove")
1206
+ }
1207
+ ]
1208
+ }
1209
+ },
1210
+ {
1211
+ step: 5,
1212
+ name: tr(lang, "steps", "planApprove"),
1213
+ checklist: { done: (f) => f.planStatus === "Approved" },
1214
+ current: {
1215
+ when: (f) => f.planStatus === "Review",
1216
+ actions: () => [
1217
+ {
1218
+ type: "instruction",
1219
+ requiresUserOk: true,
1220
+ message: tr(lang, "messages", "planApproval")
1221
+ }
1222
+ ]
1223
+ }
1224
+ },
1225
+ {
1226
+ step: 6,
1227
+ name: tr(lang, "steps", "tasksWrite"),
1228
+ checklist: {
1229
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0,
1230
+ detail: (f) => f.tasks.total > 0 ? `(${f.tasks.total})` : ""
1231
+ },
1232
+ current: {
1233
+ when: (f) => f.planStatus === "Approved" && (!f.docs.tasksExists || f.tasks.total === 0),
1234
+ actions: (f) => {
1235
+ if (!f.docs.tasksExists) {
1236
+ return [
1237
+ {
1238
+ type: "instruction",
1239
+ message: tr(lang, "messages", "tasksCreate")
1240
+ }
1241
+ ];
1242
+ }
1243
+ return [
1244
+ {
1245
+ type: "instruction",
1246
+ message: tr(lang, "messages", "tasksNeedAtLeastOne")
1247
+ }
1248
+ ];
1249
+ }
1250
+ }
1251
+ },
1252
+ {
1253
+ step: 7,
1254
+ name: tr(lang, "steps", "docsCommitPlanning"),
1255
+ checklist: {
1256
+ done: (f) => f.issueNumber ? true : f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
1257
+ },
1258
+ current: {
1259
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber && f.git.docsHasUncommittedChanges,
1260
+ actions: (f) => [
1261
+ {
1262
+ type: "command",
1263
+ requiresUserOk: true,
1264
+ scope: "docs",
1265
+ cwd: f.git.docsGitCwd,
1266
+ cmd: tr(lang, "messages", "docsCommitPlanning", {
1267
+ docsGitCwd: f.git.docsGitCwd,
1268
+ featurePath: f.docs.featurePathFromDocs,
1269
+ folderName: f.folderName
1270
+ })
1271
+ }
1272
+ ]
1273
+ }
1274
+ },
1275
+ {
1276
+ step: 8,
1277
+ name: tr(lang, "steps", "issueCreate"),
1278
+ checklist: {
1279
+ done: (f) => !!f.issueNumber && !f.git.docsHasUncommittedChanges
1280
+ },
1281
+ current: {
1282
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && (!f.issueNumber || f.git.docsHasUncommittedChanges),
1283
+ actions: (f) => {
1284
+ if (!f.issueNumber) {
1285
+ return [
1286
+ {
1287
+ type: "instruction",
1288
+ requiresUserOk: true,
1289
+ message: tr(lang, "messages", "issueCreateAndWrite")
1290
+ }
1291
+ ];
1292
+ }
1293
+ return [
1294
+ {
1295
+ type: "command",
1296
+ requiresUserOk: true,
1297
+ scope: "docs",
1298
+ cwd: f.git.docsGitCwd,
1299
+ cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
1300
+ docsGitCwd: f.git.docsGitCwd,
1301
+ featurePath: f.docs.featurePathFromDocs,
1302
+ issueNumber: f.issueNumber,
1303
+ folderName: f.folderName
1304
+ })
1305
+ }
1306
+ ];
1307
+ }
1308
+ }
1309
+ },
1310
+ {
1311
+ step: 9,
1312
+ name: tr(lang, "steps", "branchCreate"),
1313
+ checklist: { done: (f) => f.git.onExpectedBranch },
1314
+ current: {
1315
+ when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1316
+ actions: (f) => {
1317
+ if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1318
+ return [
1319
+ {
1320
+ type: "instruction",
1321
+ message: tr(lang, "messages", "standaloneNeedsProjectRoot")
1322
+ }
1323
+ ];
1324
+ }
1325
+ return [
1326
+ {
1327
+ type: "command",
1328
+ scope: "project",
1329
+ cwd: f.git.projectGitCwd,
1330
+ cmd: tr(lang, "messages", "createBranch", {
1331
+ projectGitCwd: f.git.projectGitCwd,
1332
+ issueNumber: f.issueNumber,
1333
+ slug: f.slug
1334
+ })
1335
+ }
1336
+ ];
1337
+ }
1338
+ }
1339
+ },
1340
+ {
1341
+ step: 10,
1342
+ name: tr(lang, "steps", "tasksExecute"),
1343
+ checklist: {
1344
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f),
1345
+ detail: (f) => f.tasks.total > 0 ? `(${f.tasks.done}/${f.tasks.total})` : ""
1346
+ },
1347
+ current: {
1348
+ when: (f) => f.git.onExpectedBranch && f.docs.tasksExists && f.tasks.total > 0 && (f.tasks.done < f.tasks.total || !isCompletionChecklistDone(f)),
1349
+ actions: (f) => {
1350
+ if (f.tasks.total === f.tasks.done && !isCompletionChecklistDone(f)) {
1351
+ return [
1352
+ {
1353
+ type: "instruction",
1354
+ requiresUserOk: true,
1355
+ message: !f.completionChecklist ? tr(lang, "messages", "tasksAllDoneButNoChecklist") : tr(lang, "messages", "tasksAllDoneButChecklist", {
1356
+ checked: f.completionChecklist.checked,
1357
+ total: f.completionChecklist.total
1358
+ })
1359
+ }
1360
+ ];
1361
+ }
1362
+ if (f.activeTask) {
1363
+ return [
1364
+ {
1365
+ type: "instruction",
1366
+ requiresUserOk: true,
1367
+ message: tr(lang, "messages", "finishDoingTask", {
1368
+ title: f.activeTask.title,
1369
+ done: f.tasks.done,
1370
+ total: f.tasks.total
1371
+ })
1372
+ }
1373
+ ];
1374
+ }
1375
+ if (f.nextTodoTask) {
1376
+ return [
1377
+ {
1378
+ type: "instruction",
1379
+ requiresUserOk: true,
1380
+ message: tr(lang, "messages", "startNextTodoTask", {
1381
+ title: f.nextTodoTask.title,
1382
+ done: f.tasks.done,
1383
+ total: f.tasks.total
1384
+ })
1385
+ }
1386
+ ];
1387
+ }
1388
+ return [
1389
+ {
1390
+ type: "instruction",
1391
+ requiresUserOk: true,
1392
+ message: tr(lang, "messages", "checkTaskStatuses", {
1393
+ done: f.tasks.done,
1394
+ total: f.tasks.total
1395
+ })
1396
+ }
1397
+ ];
1398
+ }
1399
+ }
1400
+ },
1401
+ {
1402
+ step: 11,
1403
+ name: tr(lang, "steps", "prCreate"),
1404
+ checklist: { done: (f) => isPrMetadataConfigured(f) && !!f.pr.link },
1405
+ current: {
1406
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f) && (!isPrMetadataConfigured(f) || !f.pr.link),
1407
+ actions: (f) => {
1408
+ if (!isPrMetadataConfigured(f)) {
1409
+ return [
1410
+ {
1411
+ type: "instruction",
1412
+ requiresUserOk: true,
1413
+ message: tr(lang, "messages", "prLegacyAsk")
1414
+ }
1415
+ ];
1416
+ }
1417
+ return [
1418
+ {
1419
+ type: "instruction",
1420
+ requiresUserOk: true,
1421
+ message: tr(lang, "messages", "prCreate")
1422
+ }
1423
+ ];
1424
+ }
1425
+ }
1426
+ },
1427
+ {
1428
+ step: 12,
1429
+ name: tr(lang, "steps", "codeReview"),
1430
+ checklist: {
1431
+ done: (f) => isPrMetadataConfigured(f) && f.pr.status === "Approved"
1432
+ },
1433
+ current: {
1434
+ when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
1435
+ actions: (f) => {
1436
+ if (f.pr.status === "Review") {
1437
+ return [
1438
+ {
1439
+ type: "instruction",
1440
+ message: tr(lang, "messages", "prResolveReview")
1441
+ }
1442
+ ];
1443
+ }
1444
+ return [
1445
+ {
1446
+ type: "instruction",
1447
+ message: tr(lang, "messages", "prRequestReview")
1448
+ }
1449
+ ];
1450
+ }
1451
+ }
1452
+ },
1453
+ {
1454
+ step: 13,
1455
+ name: tr(lang, "steps", "featureDone"),
1456
+ checklist: { done: (f) => isFeatureDone(f) },
1457
+ current: {
1458
+ when: (f) => isFeatureDone(f),
1459
+ actions: () => [
1460
+ {
1461
+ type: "instruction",
1462
+ message: tr(lang, "messages", "featureDone")
1463
+ }
1464
+ ]
1465
+ }
1466
+ }
1467
+ ];
1468
+ }
1469
+ function getStepsMap(lang) {
1470
+ return Object.fromEntries(getStepDefinitions(lang).map((d) => [d.step, d.name]));
1471
+ }
1472
+ getStepDefinitions("ko");
1473
+ getStepsMap("ko");
1474
+
1475
+ // src/utils/context/progress.ts
1476
+ function resolveFeatureProgress(feature, stepDefinitions, lang) {
1477
+ const ordered = [...stepDefinitions].sort((a, b) => a.step - b.step);
1478
+ for (const definition of ordered) {
1479
+ if (!definition.current) continue;
1480
+ if (definition.current.when(feature)) {
1481
+ const actions = definition.current.actions(feature);
1482
+ return {
1483
+ currentStep: definition.step,
1484
+ actions,
1485
+ nextAction: actions.map((a) => a.type === "command" ? a.cmd : a.message).join("\n")
1486
+ };
1487
+ }
1488
+ }
1489
+ const lastStep = ordered[ordered.length - 1];
1490
+ return {
1491
+ currentStep: lastStep?.step ?? 10,
1492
+ actions: [
1493
+ {
1494
+ type: "instruction",
1495
+ message: tr(lang, "messages", "fallbackRerunContext")
1496
+ }
1497
+ ],
1498
+ nextAction: tr(lang, "messages", "fallbackRerunContext")
1499
+ };
1500
+ }
1501
+ function getCurrentBranch(cwd) {
1502
+ try {
1503
+ return execSync("git rev-parse --abbrev-ref HEAD", {
1504
+ cwd,
1505
+ encoding: "utf-8",
1506
+ stdio: ["ignore", "pipe", "pipe"]
1507
+ }).trim();
1508
+ } catch {
1509
+ return "";
1510
+ }
1511
+ }
1512
+ function getGitStatusPorcelain(cwd, relativePaths) {
1513
+ try {
1514
+ const args = relativePaths.length > 0 ? ` -- ${relativePaths.map((p) => `"${p}"`).join(" ")}` : "";
1515
+ return execSync(`git status --porcelain=v1${args}`, {
1516
+ cwd,
1517
+ encoding: "utf-8",
1518
+ stdio: ["ignore", "pipe", "pipe"]
1519
+ });
1520
+ } catch {
1521
+ return void 0;
1522
+ }
1523
+ }
1524
+ function getGitTopLevel(cwd) {
1525
+ try {
1526
+ return execSync("git rev-parse --show-toplevel", {
1527
+ cwd,
1528
+ encoding: "utf-8",
1529
+ stdio: ["ignore", "pipe", "pipe"]
1530
+ }).trim();
1531
+ } catch {
1532
+ return null;
1533
+ }
1534
+ }
1535
+ function resolveProjectGitCwd(config, repo) {
1536
+ const docsRepo = config.docsRepo;
1537
+ if (docsRepo !== "standalone") {
1538
+ const topLevel = getGitTopLevel(process.cwd());
1539
+ return { cwd: topLevel || process.cwd() };
1540
+ }
1541
+ if (!config.projectRoot) {
1542
+ return {
1543
+ cwd: null,
1544
+ warning: "standalone \uBAA8\uB4DC\uC785\uB2C8\uB2E4. projectRoot\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58 \uD655\uC778\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)"
1545
+ };
1546
+ }
1547
+ if (config.projectType === "fullstack") {
1548
+ if (typeof config.projectRoot === "string") {
1549
+ return {
1550
+ cwd: null,
1551
+ warning: 'fullstack standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "fe": "...", "be": "..." })'
1552
+ };
1553
+ }
1554
+ const root = config.projectRoot[repo];
1555
+ if (!root) {
1556
+ return {
1557
+ cwd: null,
1558
+ warning: `projectRoot.${repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --repo ${repo})`
1559
+ };
1560
+ }
1561
+ return { cwd: getGitTopLevel(root) || root };
1562
+ }
1563
+ if (typeof config.projectRoot !== "string") {
1564
+ return {
1565
+ cwd: null,
1566
+ warning: 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")'
1567
+ };
1568
+ }
1569
+ return { cwd: getGitTopLevel(config.projectRoot) || config.projectRoot };
1570
+ }
1571
+ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1572
+ if (!branchName || !issueNumber) return false;
1573
+ const match = branchName.match(new RegExp(`^feat\\/${issueNumber}-(.+)$`));
1574
+ if (!match) return false;
1575
+ const rest = match[1];
1576
+ return rest === slug || rest === folderName;
1577
+ }
1578
+ function escapeRegExp(value) {
1579
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1580
+ }
1581
+ function extractSpecValue2(content, key) {
1582
+ const regex = new RegExp(
1583
+ `^\\s*-\\s*\\*\\*${escapeRegExp(key)}\\*\\*\\s*:\\s*(.*)$`,
1584
+ "m"
1585
+ );
1586
+ const match = content.match(regex);
1587
+ return match ? match[1].trim() : void 0;
1588
+ }
1589
+ function extractFirstSpecValue(content, keys) {
1590
+ for (const key of keys) {
1591
+ const value = extractSpecValue2(content, key);
1592
+ if (value) return value;
1593
+ }
1594
+ return void 0;
1595
+ }
1596
+ function parseDocStatus(value) {
1597
+ if (!value) return void 0;
1598
+ const trimmed = value.trim();
1599
+ if (trimmed.includes("|")) return void 0;
1600
+ const match = trimmed.match(/\b(Draft|Review|Approved)\b/i);
1601
+ if (!match) return void 0;
1602
+ const normalized = match[1].toLowerCase();
1603
+ if (normalized === "draft") return "Draft";
1604
+ if (normalized === "review") return "Review";
1605
+ return "Approved";
1606
+ }
1607
+ function parseIssueNumber(value) {
1608
+ if (!value) return void 0;
1609
+ const match = value.match(/#?(\d+)/);
1610
+ return match ? match[1] : void 0;
1611
+ }
1612
+ function parsePrLink(value) {
1613
+ if (!value) return void 0;
1614
+ const trimmed = value.trim();
1615
+ if (!trimmed) return void 0;
1616
+ if (trimmed === "#" || trimmed === "-") return void 0;
1617
+ if (trimmed.includes("{") || trimmed.includes("}")) return void 0;
1618
+ return trimmed;
1619
+ }
1620
+ function parseTasks(content) {
1621
+ const summary = { total: 0, todo: 0, doing: 0, done: 0 };
1622
+ let activeTask;
1623
+ let nextTodoTask;
1624
+ const lines = content.split("\n");
1625
+ for (const line of lines) {
1626
+ const match = line.match(/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/);
1627
+ if (!match) continue;
1628
+ const status = match[1].toUpperCase();
1629
+ const title = match[3].trim();
1630
+ summary.total++;
1631
+ if (status === "DONE") summary.done++;
1632
+ else if (status === "DOING" || status === "REVIEW") summary.doing++;
1633
+ else if (status === "TODO") summary.todo++;
1634
+ if (!activeTask && (status === "DOING" || status === "REVIEW")) {
1635
+ activeTask = { status, title };
1636
+ }
1637
+ if (!nextTodoTask && status === "TODO") {
1638
+ nextTodoTask = { status: "TODO", title };
1639
+ }
1640
+ }
1641
+ return { summary, activeTask, nextTodoTask };
1642
+ }
1643
+ function parseCompletionChecklist(content) {
1644
+ const lines = content.split("\n");
1645
+ const startIndex = lines.findIndex(
1646
+ (line) => /^\s*##\s+(완료 조건|Completion Criteria)\s*$/.test(line)
1647
+ );
1648
+ if (startIndex === -1) return void 0;
1649
+ let total = 0;
1650
+ let checked = 0;
1651
+ for (let i = startIndex + 1; i < lines.length; i++) {
1652
+ const line = lines[i];
1653
+ if (/^\s*##\s+/.test(line)) break;
1654
+ const match = line.match(/^\s*-\s*\[([ xX])\]\s+/);
1655
+ if (!match) continue;
1656
+ total++;
1657
+ if (match[1].toLowerCase() === "x") checked++;
1658
+ }
1659
+ return total > 0 ? { total, checked } : void 0;
1660
+ }
1661
+ async function parseFeature(featurePath, type, context, options) {
1662
+ const lang = options.lang;
1663
+ const folderName = path4.basename(featurePath);
1664
+ const match = folderName.match(/^(F\\d+)-(.+)$/);
1665
+ const id = match?.[1];
1666
+ const slug = match?.[2] || folderName;
1667
+ const specPath = path4.join(featurePath, "spec.md");
1668
+ const planPath = path4.join(featurePath, "plan.md");
1669
+ const tasksPath = path4.join(featurePath, "tasks.md");
1670
+ let specStatus;
1671
+ let issueNumber;
1672
+ const specExists = await fs6.pathExists(specPath);
1673
+ if (specExists) {
1674
+ const content = await fs6.readFile(specPath, "utf-8");
1675
+ const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1676
+ specStatus = parseDocStatus(statusValue);
1677
+ const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1678
+ issueNumber = parseIssueNumber(issueValue);
1679
+ }
1680
+ let planStatus;
1681
+ const planExists = await fs6.pathExists(planPath);
1682
+ if (planExists) {
1683
+ const content = await fs6.readFile(planPath, "utf-8");
1684
+ const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1685
+ planStatus = parseDocStatus(statusValue);
1686
+ }
1687
+ const tasksExists = await fs6.pathExists(tasksPath);
1688
+ const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
1689
+ let activeTask;
1690
+ let nextTodoTask;
1691
+ let completionChecklist;
1692
+ let prLink;
1693
+ let prStatus;
1694
+ let prFieldExists = false;
1695
+ let prStatusFieldExists = false;
1696
+ if (tasksExists) {
1697
+ const content = await fs6.readFile(tasksPath, "utf-8");
1698
+ const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
1699
+ tasksSummary.total = summary.total;
1700
+ tasksSummary.todo = summary.todo;
1701
+ tasksSummary.doing = summary.doing;
1702
+ tasksSummary.done = summary.done;
1703
+ activeTask = active;
1704
+ nextTodoTask = nextTodo;
1705
+ completionChecklist = parseCompletionChecklist(content);
1706
+ if (!issueNumber) {
1707
+ const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1708
+ issueNumber = parseIssueNumber(issueValue);
1709
+ }
1710
+ const prValue = extractFirstSpecValue(content, ["PR", "Pull Request"]);
1711
+ prFieldExists = prValue !== void 0;
1712
+ prLink = parsePrLink(prValue);
1713
+ const prStatusValue = extractFirstSpecValue(content, ["PR \uC0C1\uD0DC", "PR Status"]);
1714
+ prStatusFieldExists = prStatusValue !== void 0;
1715
+ prStatus = parseDocStatus(prStatusValue);
1716
+ }
1717
+ const warnings = [];
1718
+ if (context.projectBranchAvailable === false) {
1719
+ warnings.push(tr(lang, "warnings", "projectBranchUnavailable"));
1720
+ }
1721
+ const onExpectedBranch = isExpectedFeatureBranch(
1722
+ context.projectBranch,
1723
+ issueNumber,
1724
+ slug,
1725
+ folderName
1726
+ );
1727
+ const relativeFeaturePathFromDocs = path4.relative(context.docsDir, featurePath);
1728
+ const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
1729
+ const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
1730
+ if (docsStatus === void 0) {
1731
+ warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
1732
+ }
1733
+ if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
1734
+ warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
1735
+ }
1736
+ const featureState = {
1737
+ id,
1738
+ slug,
1739
+ folderName,
1740
+ type,
1741
+ path: featurePath,
1742
+ issueNumber,
1743
+ specStatus,
1744
+ planStatus,
1745
+ tasks: tasksSummary,
1746
+ activeTask,
1747
+ nextTodoTask,
1748
+ completionChecklist,
1749
+ pr: { link: prLink, status: prStatus },
1750
+ git: {
1751
+ docsBranch: context.docsBranch,
1752
+ projectBranch: context.projectBranch,
1753
+ projectBranchAvailable: context.projectBranchAvailable,
1754
+ docsGitCwd: context.docsGitCwd,
1755
+ projectGitCwd: context.projectGitCwd,
1756
+ onExpectedBranch,
1757
+ docsHasUncommittedChanges
1758
+ },
1759
+ docs: {
1760
+ featurePathFromDocs: relativeFeaturePathFromDocs,
1761
+ specExists,
1762
+ planExists,
1763
+ tasksExists,
1764
+ prFieldExists,
1765
+ prStatusFieldExists
1766
+ }
1767
+ };
1768
+ const { currentStep, actions, nextAction } = resolveFeatureProgress(
1769
+ featureState,
1770
+ options.stepDefinitions,
1771
+ lang
1772
+ );
1773
+ return { ...featureState, currentStep, actions, nextAction, warnings };
1774
+ }
1775
+ async function scanFeatures(config) {
1776
+ const features = [];
1777
+ const warnings = [];
1778
+ const stepDefinitions = getStepDefinitions(config.lang);
1779
+ const docsBranch = getCurrentBranch(config.docsDir);
1780
+ const projectBranches = {
1781
+ single: "",
1782
+ fe: "",
1783
+ be: ""
1784
+ };
1785
+ let singleProject;
1786
+ let feProject;
1787
+ let beProject;
1788
+ if (config.projectType === "single") {
1789
+ singleProject = resolveProjectGitCwd(config, "single");
1790
+ if (singleProject.warning) warnings.push(singleProject.warning);
1791
+ projectBranches.single = singleProject.cwd ? getCurrentBranch(singleProject.cwd) : "";
1792
+ } else {
1793
+ feProject = resolveProjectGitCwd(config, "fe");
1794
+ beProject = resolveProjectGitCwd(config, "be");
1795
+ if (feProject.warning) warnings.push(feProject.warning);
1796
+ if (beProject.warning) warnings.push(beProject.warning);
1797
+ projectBranches.fe = feProject.cwd ? getCurrentBranch(feProject.cwd) : "";
1798
+ projectBranches.be = beProject.cwd ? getCurrentBranch(beProject.cwd) : "";
1799
+ }
1800
+ if (config.projectType === "single") {
1801
+ const featureDirs = await glob("features/*/", {
1802
+ cwd: config.docsDir,
1803
+ absolute: true,
1804
+ ignore: ["**/feature-base/**"]
1805
+ });
1806
+ for (const dir of featureDirs) {
1807
+ if ((await fs6.stat(dir)).isDirectory()) {
1808
+ features.push(
1809
+ await parseFeature(
1810
+ dir,
1811
+ "single",
1812
+ {
1813
+ projectBranch: projectBranches.single,
1814
+ docsBranch,
1815
+ docsGitCwd: config.docsDir,
1816
+ projectGitCwd: singleProject?.cwd ?? void 0,
1817
+ docsDir: config.docsDir,
1818
+ projectBranchAvailable: Boolean(singleProject?.cwd)
1819
+ },
1820
+ { lang: config.lang, stepDefinitions }
1821
+ )
1822
+ );
1823
+ }
1824
+ }
1825
+ } else {
1826
+ const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1827
+ const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1828
+ for (const dir of feDirs) {
1829
+ if ((await fs6.stat(dir)).isDirectory()) {
1830
+ features.push(
1831
+ await parseFeature(
1832
+ dir,
1833
+ "fe",
1834
+ {
1835
+ projectBranch: projectBranches.fe,
1836
+ docsBranch,
1837
+ docsGitCwd: config.docsDir,
1838
+ projectGitCwd: feProject?.cwd ?? void 0,
1839
+ docsDir: config.docsDir,
1840
+ projectBranchAvailable: Boolean(feProject?.cwd)
1841
+ },
1842
+ { lang: config.lang, stepDefinitions }
1843
+ )
1844
+ );
1845
+ }
1846
+ }
1847
+ for (const dir of beDirs) {
1848
+ if ((await fs6.stat(dir)).isDirectory()) {
1849
+ features.push(
1850
+ await parseFeature(
1851
+ dir,
1852
+ "be",
1853
+ {
1854
+ projectBranch: projectBranches.be,
1855
+ docsBranch,
1856
+ docsGitCwd: config.docsDir,
1857
+ projectGitCwd: beProject?.cwd ?? void 0,
1858
+ docsDir: config.docsDir,
1859
+ projectBranchAvailable: Boolean(beProject?.cwd)
1860
+ },
1861
+ { lang: config.lang, stepDefinitions }
1862
+ )
1863
+ );
1864
+ }
1865
+ }
1866
+ }
1867
+ return {
1868
+ features,
1869
+ branches: {
1870
+ docs: docsBranch,
1871
+ project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1872
+ },
1873
+ warnings
1874
+ };
1875
+ }
1876
+
1877
+ // src/commands/context.ts
1878
+ function contextCommand(program2) {
1879
+ program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Repository type for fullstack: fe | be").action(
1880
+ async (featureName, options) => {
1881
+ try {
1882
+ await runContext(featureName, options);
1883
+ } catch (error) {
1884
+ if (options.json) {
1885
+ console.log(
1886
+ JSON.stringify({
1887
+ status: "error",
1888
+ error: error instanceof Error ? error.message : String(error)
1889
+ })
1890
+ );
1891
+ } else {
1892
+ console.error(chalk6.red("\uC624\uB958:"), error);
1893
+ }
1894
+ process.exit(1);
1895
+ }
1896
+ }
1897
+ );
1898
+ }
1899
+ function matchesFeatureSelector(f, selector) {
1900
+ const s = selector.trim();
1901
+ if (!s) return false;
1902
+ if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
1903
+ if (f.slug.toLowerCase() === s.toLowerCase()) return true;
1904
+ if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
1905
+ return false;
1906
+ }
1907
+ function detectFromBranch(branchName, features) {
1908
+ const match = branchName.match(/^feat\/\d+-(.+)$/);
1909
+ if (!match) return [];
1910
+ const detected = match[1];
1911
+ return features.filter(
1912
+ (f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
1913
+ );
1914
+ }
1915
+ async function runContext(featureName, options) {
1916
+ const cwd = process.cwd();
1917
+ const config = await getConfig(cwd);
1918
+ const lang = config?.lang ?? "ko";
1919
+ if (!config) {
1920
+ throw new Error("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
1921
+ }
1922
+ const stepDefinitions = getStepDefinitions(lang);
1923
+ const stepsMap = getStepsMap(lang);
1924
+ const { features, branches, warnings } = await scanFeatures(config);
1925
+ let targetFeatures = [];
1926
+ if (featureName) {
1927
+ targetFeatures = features.filter((f2) => matchesFeatureSelector(f2, featureName));
1928
+ if (options.repo) {
1929
+ targetFeatures = targetFeatures.filter((f2) => f2.type === options.repo);
1930
+ }
1931
+ } else {
1932
+ if (config.projectType === "single") {
1933
+ const branchName = branches.project.single || "";
1934
+ targetFeatures = detectFromBranch(branchName, features);
1935
+ } else if (options.repo) {
1936
+ const branchName = branches.project[options.repo] || "";
1937
+ targetFeatures = detectFromBranch(
1938
+ branchName,
1939
+ features.filter((f2) => f2.type === options.repo)
1940
+ );
1941
+ } else {
1942
+ const feMatches = branches.project.fe ? detectFromBranch(
1943
+ branches.project.fe,
1944
+ features.filter((f2) => f2.type === "fe")
1945
+ ) : [];
1946
+ const beMatches = branches.project.be ? detectFromBranch(
1947
+ branches.project.be,
1948
+ features.filter((f2) => f2.type === "be")
1949
+ ) : [];
1950
+ targetFeatures = [...feMatches, ...beMatches];
1951
+ }
1952
+ if (targetFeatures.length === 0) targetFeatures = features;
1953
+ }
1954
+ if (options.json) {
1955
+ const result = {
1956
+ status: features.length === 0 ? "no_features" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
1957
+ branches,
1958
+ warnings,
1959
+ matchedFeature: targetFeatures.length === 1 ? targetFeatures[0] : null,
1960
+ candidates: targetFeatures.length > 1 ? targetFeatures : [],
1961
+ actions: targetFeatures.length === 1 ? targetFeatures[0].actions : [],
1962
+ recommendation: ""
1963
+ };
1964
+ if (result.status === "multiple_active") {
1965
+ result.recommendation = "Multiple features detected. Please specify feature name (slug | F001 | F001-slug) or use --repo.";
1966
+ } else if (result.status === "no_features") {
1967
+ result.recommendation = "No features found. Create a feature first.";
1968
+ } else if (result.status === "no_match") {
1969
+ result.recommendation = "No features found.";
1970
+ } else {
1971
+ result.recommendation = targetFeatures[0].nextAction;
1972
+ }
1973
+ console.log(JSON.stringify(result, null, 2));
1974
+ return;
1975
+ }
1976
+ console.log();
1977
+ console.log(chalk6.bold("\u{1F4CD} Current Context Check"));
1978
+ if (config.projectType === "single") {
1979
+ if (branches.project.single) {
1980
+ console.log(
1981
+ chalk6.gray(` (Detected from Project Branch: ${branches.project.single})`)
1982
+ );
1983
+ }
1984
+ } else if (options.repo) {
1985
+ const branchName = branches.project[options.repo] || "";
1986
+ if (branchName) {
1987
+ console.log(
1988
+ chalk6.gray(
1989
+ ` (Detected from Project Branch: ${options.repo.toUpperCase()} ${branchName})`
1990
+ )
1991
+ );
1992
+ }
1993
+ } else if (branches.project.fe || branches.project.be) {
1994
+ const parts = [
1995
+ branches.project.fe ? `FE ${branches.project.fe}` : null,
1996
+ branches.project.be ? `BE ${branches.project.be}` : null
1997
+ ].filter(Boolean);
1998
+ console.log(chalk6.gray(` (Detected from Project Branch: ${parts.join(" / ")})`));
1999
+ }
2000
+ if (config.docsRepo === "standalone" && branches.docs) {
2001
+ console.log(chalk6.gray(` (Docs Branch: ${branches.docs})`));
2002
+ }
2003
+ console.log(chalk6.gray("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
2004
+ console.log();
2005
+ if (features.length === 0) {
2006
+ console.log(chalk6.yellow("\u26A0\uFE0F \uC9C4\uD589 \uC911\uC778 Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2007
+ console.log();
2008
+ return;
2009
+ }
2010
+ if (warnings.length > 0) {
2011
+ console.log(chalk6.yellow("\u26A0\uFE0F \uD658\uACBD \uACBD\uACE0:"));
2012
+ warnings.forEach((w) => console.log(chalk6.yellow(` - ${w}`)));
2013
+ console.log();
2014
+ }
2015
+ if (targetFeatures.length > 1) {
2016
+ console.log(
2017
+ chalk6.blue(`\u{1F539} ${targetFeatures.length} Active Features Detected:`)
2018
+ );
2019
+ console.log();
2020
+ targetFeatures.forEach((f2) => {
2021
+ const stepName2 = stepsMap[f2.currentStep] || "Unknown";
2022
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2023
+ console.log(
2024
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2025
+ );
2026
+ });
2027
+ console.log();
2028
+ console.log(chalk6.gray("Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:"));
2029
+ console.log(
2030
+ chalk6.gray(" $ npx lee-spec-kit context <slug|F001|F001-slug> [--repo fe|be]")
2031
+ );
2032
+ console.log();
2033
+ return;
2034
+ }
2035
+ const f = targetFeatures[0];
2036
+ const stepName = stepsMap[f.currentStep] || "Unknown";
2037
+ const okTag = (requiresUserOk) => requiresUserOk ? chalk6.yellow(lang === "ko" ? "[OK \uD544\uC694] " : "[OK required] ") : "";
2038
+ console.log(
2039
+ `\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
2040
+ );
2041
+ if (f.issueNumber) {
2042
+ console.log(` \u2022 Issue: #${f.issueNumber}`);
2043
+ }
2044
+ console.log(` \u2022 Path: ${path4.relative(cwd, f.path)}`);
2045
+ if (f.git.projectBranch) {
2046
+ console.log(` \u2022 Project Branch: ${f.git.projectBranch}`);
2047
+ }
2048
+ console.log();
2049
+ console.log(
2050
+ `\u{1F539} Progress: ${chalk6.yellow(`Step ${f.currentStep}. ${stepName}`)}`
2051
+ );
2052
+ if (f.activeTask) {
2053
+ console.log(
2054
+ ` \u2022 Active Task: ${chalk6.yellow(`[${f.activeTask.status}]`)} ${f.activeTask.title}`
2055
+ );
2056
+ } else if (f.nextTodoTask && f.currentStep === 10) {
2057
+ console.log(
2058
+ ` \u2022 Next TODO: ${chalk6.gray(`[${f.nextTodoTask.status}]`)} ${f.nextTodoTask.title}`
2059
+ );
2060
+ }
2061
+ printChecklist(f, stepDefinitions);
2062
+ if (f.warnings.length > 0) {
2063
+ console.log();
2064
+ console.log(chalk6.yellow("\u26A0\uFE0F Feature Warnings:"));
2065
+ f.warnings.forEach((w) => console.log(chalk6.yellow(` - ${w}`)));
2066
+ }
2067
+ console.log();
2068
+ console.log(chalk6.gray("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
2069
+ if (!f.actions || f.actions.length === 0) {
2070
+ console.log(`\u{1F449} Next Action: ${chalk6.green(chalk6.bold(f.nextAction))}`);
2071
+ console.log();
2072
+ return;
2073
+ }
2074
+ if (f.actions.length === 1) {
2075
+ const action = f.actions[0];
2076
+ if (action.type === "command") {
2077
+ console.log(
2078
+ `\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.cmd))}`
2079
+ );
2080
+ } else {
2081
+ console.log(
2082
+ `\u{1F449} Next Action: ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.message))}`
2083
+ );
2084
+ }
2085
+ console.log();
2086
+ return;
2087
+ }
2088
+ console.log(chalk6.green(chalk6.bold("\u{1F449} Next Actions:")));
2089
+ f.actions.forEach((action) => {
2090
+ if (action.type === "command") {
2091
+ console.log(` \u2022 (${action.scope}) ${okTag(action.requiresUserOk)}${action.cmd}`);
2092
+ } else {
2093
+ console.log(` \u2022 ${okTag(action.requiresUserOk)}${action.message}`);
2094
+ }
2095
+ });
2096
+ console.log();
2097
+ }
2098
+ function printChecklist(f, stepDefinitions) {
2099
+ const checklistSteps = [...stepDefinitions].sort((a, b) => a.step - b.step);
2100
+ checklistSteps.forEach((definition) => {
2101
+ const done = definition.checklist.done(f);
2102
+ const detail = definition.checklist.detail?.(f) ?? "";
2103
+ const mark = done ? chalk6.green("\u2705") : chalk6.gray("\u25EF");
2104
+ const label = definition.step === f.currentStep ? chalk6.bold(definition.name) : definition.name;
2105
+ console.log(` ${mark} ${definition.step}. ${label} ${detail}`);
2106
+ });
2107
+ }
2108
+ var CACHE_FILE = path4.join(os.homedir(), ".lee-spec-kit-version-cache.json");
866
2109
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
867
2110
  function getCurrentVersion() {
868
2111
  try {
869
- const packageJsonPath = path7.join(__dirname$1, "..", "package.json");
2112
+ const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
870
2113
  if (fs6.existsSync(packageJsonPath)) {
871
2114
  const pkg = fs6.readJsonSync(packageJsonPath);
872
2115
  return pkg.version;
@@ -896,9 +2139,9 @@ function isNewerVersion(current, latest) {
896
2139
  function printUpdateNotice(current, latest) {
897
2140
  console.log();
898
2141
  console.log(
899
- chalk.yellow(`\u{1F4E6} lee-spec-kit v${latest} \uC0AC\uC6A9 \uAC00\uB2A5 (\uD604\uC7AC: v${current})`)
2142
+ chalk6.yellow(`\u{1F4E6} lee-spec-kit v${latest} \uC0AC\uC6A9 \uAC00\uB2A5 (\uD604\uC7AC: v${current})`)
900
2143
  );
901
- console.log(chalk.gray(" \uC5C5\uB370\uC774\uD2B8: npm update -g lee-spec-kit"));
2144
+ console.log(chalk6.gray(" \uC5C5\uB370\uC774\uD2B8: npm update -g lee-spec-kit"));
902
2145
  console.log();
903
2146
  }
904
2147
  function spawnBackgroundVersionCheck() {
@@ -943,11 +2186,24 @@ function checkForUpdates() {
943
2186
 
944
2187
  // src/index.ts
945
2188
  checkForUpdates();
2189
+ function getCliVersion() {
2190
+ try {
2191
+ const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2192
+ if (fs6.existsSync(packageJsonPath)) {
2193
+ const pkg = fs6.readJsonSync(packageJsonPath);
2194
+ if (pkg?.version) return String(pkg.version);
2195
+ }
2196
+ } catch {
2197
+ }
2198
+ return "0.0.0";
2199
+ }
946
2200
  program.name("lee-spec-kit").description(
947
2201
  "Project documentation structure generator for AI-assisted development"
948
- ).version("0.2.0");
2202
+ ).version(getCliVersion());
949
2203
  initCommand(program);
950
2204
  featureCommand(program);
951
2205
  statusCommand(program);
952
2206
  updateCommand(program);
2207
+ configCommand(program);
2208
+ contextCommand(program);
953
2209
  program.parse();