@specmarket/cli 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,12 +6,14 @@ import {
6
6
  REQUIRED_SPEC_FILES,
7
7
  REQUIRED_STDLIB_FILES,
8
8
  RUN_DEFAULTS,
9
+ SIDECAR_FILENAME,
9
10
  TOKEN_EXPIRY_MS,
10
11
  loadConfig,
11
12
  saveConfig,
12
13
  specYamlSchema,
14
+ specmarketSidecarSchema,
13
15
  transformInfrastructure
14
- } from "./chunk-MS2DYACY.js";
16
+ } from "./chunk-DLEMNRTH.js";
15
17
  import {
16
18
  api
17
19
  } from "./chunk-JEUDDJP7.js";
@@ -97,6 +99,11 @@ var debug2 = createDebug2("specmarket:convex");
97
99
  async function getConvexClient(token) {
98
100
  const config = await loadConfig();
99
101
  const url = process.env["CONVEX_URL"] ?? config.convexUrl ?? DEFAULT_CONVEX_URL;
102
+ if (url.includes("placeholder.convex.cloud")) {
103
+ throw new Error(
104
+ "CONVEX_URL is not configured. Set the CONVEX_URL environment variable or run `specmarket config set convexUrl <url>`."
105
+ );
106
+ }
100
107
  debug2("Creating Convex client for URL: %s", url);
101
108
  const client = new ConvexHttpClient(url);
102
109
  if (token) {
@@ -162,7 +169,7 @@ async function handleTokenLogin(token) {
162
169
  }
163
170
  }
164
171
  async function handleDeviceCodeLogin() {
165
- const config = await import("./config-R5KWZSJP.js").then((m) => m.loadConfig());
172
+ const config = await import("./config-OAU6SJLC.js").then((m) => m.loadConfig());
166
173
  const baseUrl = config.convexUrl ?? process.env["CONVEX_URL"] ?? "https://your-deployment.convex.cloud";
167
174
  const webUrl = baseUrl.replace("convex.cloud", "specmarket.dev");
168
175
  const client = await getConvexClient();
@@ -355,7 +362,7 @@ ${data.replacesSaas ? `replaces_saas: "${data.replacesSaas}"` : '# replaces_saas
355
362
  output_type: ${data.outputType}
356
363
  primary_stack: ${data.primaryStack}
357
364
  version: "1.0.0"
358
- runner: claude-code
365
+ runner: claude
359
366
  min_model: "claude-opus-4-5"
360
367
 
361
368
  estimated_tokens: 50000
@@ -399,9 +406,9 @@ Read the requirements in SPEC.md and implement the application step by step.
399
406
  ## Process
400
407
 
401
408
  1. Read SPEC.md completely before writing any code
402
- 2. Check fix_plan.md for outstanding items
409
+ 2. Check TASKS.md for outstanding items
403
410
  3. Implement features, run tests, iterate
404
- 4. Update fix_plan.md as you complete items
411
+ 4. Update TASKS.md as you complete items
405
412
  5. Verify SUCCESS_CRITERIA.md criteria are met
406
413
 
407
414
  ## Rules
@@ -409,7 +416,7 @@ Read the requirements in SPEC.md and implement the application step by step.
409
416
  - Follow stdlib/STACK.md for technology choices
410
417
  - Write tests for all business logic
411
418
  - Do not skip steps or take shortcuts
412
- - Update fix_plan.md after each significant change
419
+ - Update TASKS.md after each significant change
413
420
  `;
414
421
  var SPEC_MD_TEMPLATE = (data) => `# ${data.displayName} \u2014 Specification
415
422
 
@@ -468,12 +475,12 @@ ${primaryStack}
468
475
  - Vitest for unit tests
469
476
  - Playwright for E2E (optional)
470
477
  `;
471
- var FIX_PLAN_TEMPLATE = (displayName) => `# Fix Plan
478
+ var TASKS_MD_TEMPLATE = (displayName) => `# Tasks
472
479
 
473
480
  > This file tracks outstanding work. Update it after each change.
474
- > Empty = implementation complete.
481
+ > All items checked = implementation complete.
475
482
 
476
- ## ${displayName} \u2014 Initial Implementation
483
+ ## Phase 1: ${displayName} \u2014 Initial Implementation
477
484
 
478
485
  - [ ] Set up project structure and dependencies
479
486
  - [ ] Implement core data model
@@ -482,6 +489,8 @@ var FIX_PLAN_TEMPLATE = (displayName) => `# Fix Plan
482
489
  - [ ] Implement UI/interface
483
490
  - [ ] Write integration tests
484
491
  - [ ] Update README.md
492
+
493
+ ## Discovered Issues
485
494
  `;
486
495
  async function handleInit(opts) {
487
496
  const { default: inquirer } = await import("inquirer");
@@ -549,7 +558,7 @@ async function handleInit(opts) {
549
558
  writeFile2(join2(targetDir, "SPEC.md"), SPEC_MD_TEMPLATE(data)),
550
559
  writeFile2(join2(targetDir, "SUCCESS_CRITERIA.md"), SUCCESS_CRITERIA_TEMPLATE),
551
560
  writeFile2(join2(targetDir, "stdlib", "STACK.md"), STACK_MD_TEMPLATE(answers.primaryStack)),
552
- writeFile2(join2(targetDir, "fix_plan.md"), FIX_PLAN_TEMPLATE(answers.displayName))
561
+ writeFile2(join2(targetDir, "TASKS.md"), TASKS_MD_TEMPLATE(answers.displayName))
553
562
  ]);
554
563
  spinner.succeed(chalk4.green(`Spec created at ${targetDir}`));
555
564
  console.log("");
@@ -557,8 +566,8 @@ async function handleInit(opts) {
557
566
  console.log(` 1. ${chalk4.cyan(`cd ${answers.name}`)}`);
558
567
  console.log(` 2. Edit ${chalk4.cyan("SPEC.md")} with your application requirements`);
559
568
  console.log(` 3. Edit ${chalk4.cyan("SUCCESS_CRITERIA.md")} with specific pass/fail criteria`);
560
- console.log(` 4. Run ${chalk4.cyan(`specmarket validate ${answers.name}`)} to check your spec`);
561
- console.log(` 5. Run ${chalk4.cyan(`specmarket run ${answers.name}`)} to execute the spec`);
569
+ console.log(` 4. Run ${chalk4.cyan(`specmarket validate`)} to check your spec`);
570
+ console.log(` 5. Run ${chalk4.cyan(`specmarket run`)} to execute the spec`);
562
571
  } catch (err) {
563
572
  spinner.fail(chalk4.red(`Failed to create spec: ${err.message}`));
564
573
  throw err;
@@ -578,123 +587,173 @@ function createInitCommand() {
578
587
  // src/commands/validate.ts
579
588
  import { Command as Command5 } from "commander";
580
589
  import chalk5 from "chalk";
590
+ import { readFile as readFile3, readdir as readdir2, access as access2 } from "fs/promises";
591
+ import { join as join4, resolve as resolve2, relative, normalize } from "path";
592
+ import { parse as parseYaml2 } from "yaml";
593
+
594
+ // src/lib/format-detection.ts
581
595
  import { readFile as readFile2, readdir, access } from "fs/promises";
582
- import { join as join3, resolve as resolve2, relative, normalize } from "path";
596
+ import { join as join3 } from "path";
583
597
  import { parse as parseYaml } from "yaml";
584
- async function validateSpec(specPath) {
585
- const dir = resolve2(specPath);
586
- const errors = [];
587
- const warnings = [];
588
- for (const file of REQUIRED_SPEC_FILES) {
589
- const filePath = join3(dir, file);
590
- try {
591
- await access(filePath);
592
- const content = await readFile2(filePath, "utf-8");
593
- if (content.trim().length === 0) {
594
- errors.push(`${file} exists but is empty`);
595
- }
596
- } catch {
597
- errors.push(`Required file missing: ${file}`);
598
- }
598
+ async function fileExists(filePath) {
599
+ try {
600
+ await access(filePath);
601
+ return true;
602
+ } catch {
603
+ return false;
599
604
  }
600
- const stdlibDir = join3(dir, "stdlib");
601
- for (const file of REQUIRED_STDLIB_FILES) {
602
- const filePath = join3(stdlibDir, file);
603
- try {
604
- await access(filePath);
605
- const content = await readFile2(filePath, "utf-8");
606
- if (content.trim().length === 0) {
607
- errors.push(`stdlib/${file} exists but is empty`);
605
+ }
606
+ async function directoryExists(dirPath) {
607
+ try {
608
+ await access(dirPath);
609
+ return true;
610
+ } catch {
611
+ return false;
612
+ }
613
+ }
614
+ async function hasStoryFiles(dir) {
615
+ try {
616
+ const entries = await readdir(dir, { withFileTypes: true });
617
+ return entries.some(
618
+ (e) => e.isFile() && e.name.startsWith("story-") && e.name.endsWith(".md")
619
+ );
620
+ } catch {
621
+ return false;
622
+ }
623
+ }
624
+ async function hasMarkdownFiles(dir) {
625
+ try {
626
+ const entries = await readdir(dir, { withFileTypes: true });
627
+ for (const entry of entries) {
628
+ if (entry.isFile() && entry.name.endsWith(".md")) return true;
629
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
630
+ const found = await hasMarkdownFiles(join3(dir, entry.name));
631
+ if (found) return true;
608
632
  }
609
- } catch {
610
- errors.push(`Required file missing: stdlib/${file}`);
611
633
  }
634
+ return false;
635
+ } catch {
636
+ return false;
612
637
  }
613
- let specYaml = null;
614
- const specYamlPath = join3(dir, "spec.yaml");
638
+ }
639
+ async function tryReadSidecar(dir) {
640
+ const path = join3(dir, SIDECAR_FILENAME);
641
+ if (!await fileExists(path)) return null;
615
642
  try {
616
- const raw = await readFile2(specYamlPath, "utf-8");
617
- specYaml = parseYaml(raw);
618
- } catch (err) {
619
- errors.push(`spec.yaml: Failed to parse YAML: ${err.message}`);
620
- return { valid: false, errors, warnings };
621
- }
622
- const parseResult = specYamlSchema.safeParse(specYaml);
623
- if (!parseResult.success) {
624
- for (const issue of parseResult.error.issues) {
625
- errors.push(
626
- `spec.yaml: ${issue.path.join(".")} \u2014 ${issue.message}`
627
- );
643
+ const raw = await readFile2(path, "utf-8");
644
+ const parsed = parseYaml(raw);
645
+ if (parsed && typeof parsed === "object" && "spec_format" in parsed) {
646
+ const fmt = parsed.spec_format;
647
+ if (typeof fmt === "string" && fmt.length > 0) return { spec_format: fmt };
628
648
  }
629
- } else {
630
- const parsed = parseResult.data;
649
+ return null;
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ async function detectSpecFormat(dir) {
655
+ const sidecar = await tryReadSidecar(dir);
656
+ if (sidecar) {
657
+ return {
658
+ format: sidecar.spec_format,
659
+ detectedBy: "sidecar",
660
+ confidence: "high"
661
+ };
662
+ }
663
+ const hasSpecYaml = await fileExists(join3(dir, "spec.yaml"));
664
+ const hasPromptMd = await fileExists(join3(dir, "PROMPT.md"));
665
+ const hasSuccessCriteria = await fileExists(join3(dir, "SUCCESS_CRITERIA.md"));
666
+ if (hasSpecYaml && hasPromptMd && hasSuccessCriteria) {
667
+ return {
668
+ format: "specmarket-legacy",
669
+ detectedBy: "heuristic",
670
+ confidence: "high"
671
+ };
672
+ }
673
+ if (hasPromptMd && hasSuccessCriteria) {
674
+ return {
675
+ format: "specmarket-legacy",
676
+ detectedBy: "heuristic",
677
+ confidence: "high"
678
+ };
679
+ }
680
+ const hasSpecMd = await fileExists(join3(dir, "spec.md"));
681
+ const hasPlanMd = await fileExists(join3(dir, "plan.md"));
682
+ const hasTasksMd = await fileExists(join3(dir, "tasks.md"));
683
+ if (hasSpecMd && (hasPlanMd || hasTasksMd)) {
684
+ return {
685
+ format: "speckit",
686
+ detectedBy: "heuristic",
687
+ confidence: "high"
688
+ };
689
+ }
690
+ if (hasSpecMd) {
691
+ return {
692
+ format: "speckit",
693
+ detectedBy: "heuristic",
694
+ confidence: "high"
695
+ };
696
+ }
697
+ const hasPrdMd = await fileExists(join3(dir, "prd.md"));
698
+ const hasArchitectureMd = await fileExists(join3(dir, "architecture.md"));
699
+ const storyFiles = await hasStoryFiles(dir);
700
+ if (hasPrdMd && (hasArchitectureMd || storyFiles)) {
701
+ return {
702
+ format: "bmad",
703
+ detectedBy: "heuristic",
704
+ confidence: "high"
705
+ };
706
+ }
707
+ const prdJsonPath = join3(dir, "prd.json");
708
+ if (await fileExists(prdJsonPath)) {
631
709
  try {
632
- const criteriaContent = await readFile2(join3(dir, "SUCCESS_CRITERIA.md"), "utf-8");
633
- const hasCriterion = /^- \[[ x]\]/m.test(criteriaContent);
634
- if (!hasCriterion) {
635
- errors.push(
636
- "SUCCESS_CRITERIA.md: Must contain at least one criterion in format: - [ ] criterion text"
637
- );
710
+ const raw = await readFile2(prdJsonPath, "utf-8");
711
+ const data = JSON.parse(raw);
712
+ if (data && typeof data === "object") {
713
+ return {
714
+ format: "ralph",
715
+ detectedBy: "heuristic",
716
+ confidence: "high"
717
+ };
638
718
  }
639
719
  } catch {
640
720
  }
641
- const cycles = await detectCircularReferences(dir);
642
- for (const cycle of cycles) {
643
- errors.push(`Circular reference detected: ${cycle}`);
644
- }
645
- if (parsed.infrastructure) {
646
- const infra = parsed.infrastructure;
647
- if (["web-app", "api-service", "mobile-app"].includes(parsed.output_type) && infra.services.length === 0) {
648
- warnings.push(
649
- `${parsed.output_type} should typically define infrastructure services (database, auth, etc.)`
650
- );
651
- }
652
- if (!infra.setup_time_minutes) {
653
- warnings.push("infrastructure.setup_time_minutes is not set");
654
- }
655
- for (const service of infra.services) {
656
- if (service.default_provider) {
657
- const providerNames = service.providers.map((p) => p.name);
658
- if (!providerNames.includes(service.default_provider)) {
659
- errors.push(
660
- `infrastructure.services[${service.name}].default_provider "${service.default_provider}" does not match any defined provider (${providerNames.join(", ")})`
661
- );
662
- }
721
+ }
722
+ if (await hasMarkdownFiles(dir)) {
723
+ return {
724
+ format: "custom",
725
+ detectedBy: "heuristic",
726
+ confidence: "low"
727
+ };
728
+ }
729
+ return {
730
+ format: "custom",
731
+ detectedBy: "heuristic",
732
+ confidence: "low"
733
+ };
734
+ }
735
+
736
+ // src/commands/validate.ts
737
+ async function collectFiles(currentDir, baseDir, extensions) {
738
+ const results = [];
739
+ try {
740
+ const entries = await readdir2(currentDir, { withFileTypes: true });
741
+ for (const entry of entries) {
742
+ const fullPath = join4(currentDir, entry.name);
743
+ if (entry.isDirectory()) {
744
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
745
+ const subFiles = await collectFiles(fullPath, baseDir, extensions);
746
+ results.push(...subFiles);
747
+ } else if (entry.isFile()) {
748
+ const ext = entry.name.includes(".") ? "." + entry.name.split(".").pop() : "";
749
+ if (extensions.has(ext)) {
750
+ results.push(relative(baseDir, fullPath));
663
751
  }
664
752
  }
665
- } else {
666
- if (["web-app", "api-service", "mobile-app"].includes(parsed.output_type)) {
667
- warnings.push(
668
- "No infrastructure block defined. Consider adding infrastructure.services for database, auth, etc."
669
- );
670
- }
671
- }
672
- if (parsed.estimated_tokens < 1e3) {
673
- warnings.push(
674
- `estimated_tokens (${parsed.estimated_tokens}) seems very low. Most specs use 10,000+.`
675
- );
676
- }
677
- if (parsed.estimated_tokens > 1e7) {
678
- warnings.push(
679
- `estimated_tokens (${parsed.estimated_tokens}) seems very high.`
680
- );
681
- }
682
- if (parsed.estimated_cost_usd < 0.01) {
683
- warnings.push(
684
- `estimated_cost_usd ($${parsed.estimated_cost_usd}) seems very low.`
685
- );
686
- }
687
- if (parsed.estimated_time_minutes < 1) {
688
- warnings.push(
689
- `estimated_time_minutes (${parsed.estimated_time_minutes}) seems unrealistically low.`
690
- );
691
753
  }
754
+ } catch {
692
755
  }
693
- return {
694
- valid: errors.length === 0,
695
- errors,
696
- warnings
697
- };
756
+ return results;
698
757
  }
699
758
  async function detectCircularReferences(dir) {
700
759
  const textExtensions = /* @__PURE__ */ new Set([".md", ".yaml", ".yml"]);
@@ -704,7 +763,7 @@ async function detectCircularReferences(dir) {
704
763
  for (const file of files) {
705
764
  const refs = /* @__PURE__ */ new Set();
706
765
  try {
707
- const content = await readFile2(join3(dir, file), "utf-8");
766
+ const content = await readFile3(join4(dir, file), "utf-8");
708
767
  let match;
709
768
  while ((match = linkPattern.exec(content)) !== null) {
710
769
  const target = match[1];
@@ -713,7 +772,7 @@ async function detectCircularReferences(dir) {
713
772
  }
714
773
  const targetPath = target.split("#")[0];
715
774
  if (!targetPath) continue;
716
- const fileDir = join3(dir, file, "..");
775
+ const fileDir = join4(dir, file, "..");
717
776
  const resolvedTarget = normalize(relative(dir, resolve2(fileDir, targetPath)));
718
777
  if (!resolvedTarget.startsWith("..") && files.includes(resolvedTarget)) {
719
778
  refs.add(resolvedTarget);
@@ -760,31 +819,259 @@ async function detectCircularReferences(dir) {
760
819
  }
761
820
  return cycles;
762
821
  }
763
- async function collectFiles(currentDir, baseDir, extensions) {
764
- const results = [];
822
+ async function validateLegacySpec(dir, errors, warnings) {
823
+ for (const file of REQUIRED_SPEC_FILES) {
824
+ const filePath = join4(dir, file);
825
+ try {
826
+ await access2(filePath);
827
+ const content = await readFile3(filePath, "utf-8");
828
+ if (content.trim().length === 0) {
829
+ errors.push(`${file} exists but is empty`);
830
+ }
831
+ } catch {
832
+ errors.push(`Required file missing: ${file}`);
833
+ }
834
+ }
835
+ const stdlibDir = join4(dir, "stdlib");
836
+ for (const file of REQUIRED_STDLIB_FILES) {
837
+ const filePath = join4(stdlibDir, file);
838
+ try {
839
+ await access2(filePath);
840
+ const content = await readFile3(filePath, "utf-8");
841
+ if (content.trim().length === 0) {
842
+ errors.push(`stdlib/${file} exists but is empty`);
843
+ }
844
+ } catch {
845
+ errors.push(`Required file missing: stdlib/${file}`);
846
+ }
847
+ }
848
+ let specYaml = null;
849
+ const specYamlPath = join4(dir, "spec.yaml");
765
850
  try {
766
- const entries = await readdir(currentDir, { withFileTypes: true });
767
- for (const entry of entries) {
768
- const fullPath = join3(currentDir, entry.name);
769
- if (entry.isDirectory()) {
770
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
771
- const subFiles = await collectFiles(fullPath, baseDir, extensions);
772
- results.push(...subFiles);
773
- } else if (entry.isFile()) {
774
- const ext = entry.name.includes(".") ? "." + entry.name.split(".").pop() : "";
775
- if (extensions.has(ext)) {
776
- results.push(relative(baseDir, fullPath));
851
+ const raw = await readFile3(specYamlPath, "utf-8");
852
+ specYaml = parseYaml2(raw);
853
+ } catch (err) {
854
+ errors.push(`spec.yaml: Failed to parse YAML: ${err.message}`);
855
+ return;
856
+ }
857
+ const parseResult = specYamlSchema.safeParse(specYaml);
858
+ if (!parseResult.success) {
859
+ for (const issue of parseResult.error.issues) {
860
+ errors.push(`spec.yaml: ${issue.path.join(".")} \u2014 ${issue.message}`);
861
+ }
862
+ return;
863
+ }
864
+ const parsed = parseResult.data;
865
+ try {
866
+ const criteriaContent = await readFile3(join4(dir, "SUCCESS_CRITERIA.md"), "utf-8");
867
+ const hasCriterion = /^- \[[ x]\]/m.test(criteriaContent);
868
+ if (!hasCriterion) {
869
+ errors.push(
870
+ "SUCCESS_CRITERIA.md: Must contain at least one criterion in format: - [ ] criterion text"
871
+ );
872
+ }
873
+ } catch {
874
+ }
875
+ const cycles = await detectCircularReferences(dir);
876
+ for (const cycle of cycles) {
877
+ errors.push(`Circular reference detected: ${cycle}`);
878
+ }
879
+ if (parsed.infrastructure) {
880
+ const infra = parsed.infrastructure;
881
+ if (["web-app", "api-service", "mobile-app"].includes(parsed.output_type) && infra.services.length === 0) {
882
+ warnings.push(
883
+ `${parsed.output_type} should typically define infrastructure services (database, auth, etc.)`
884
+ );
885
+ }
886
+ if (!infra.setup_time_minutes) {
887
+ warnings.push("infrastructure.setup_time_minutes is not set");
888
+ }
889
+ for (const service of infra.services) {
890
+ if (service.default_provider) {
891
+ const providerNames = service.providers.map((p) => p.name);
892
+ if (!providerNames.includes(service.default_provider)) {
893
+ errors.push(
894
+ `infrastructure.services[${service.name}].default_provider "${service.default_provider}" does not match any defined provider (${providerNames.join(", ")})`
895
+ );
777
896
  }
778
897
  }
779
898
  }
899
+ } else {
900
+ if (["web-app", "api-service", "mobile-app"].includes(parsed.output_type)) {
901
+ warnings.push(
902
+ "No infrastructure block defined. Consider adding infrastructure.services for database, auth, etc."
903
+ );
904
+ }
905
+ }
906
+ if (parsed.estimated_tokens < 1e3) {
907
+ warnings.push(
908
+ `estimated_tokens (${parsed.estimated_tokens}) seems very low. Most specs use 10,000+.`
909
+ );
910
+ }
911
+ if (parsed.estimated_tokens > 1e7) {
912
+ warnings.push(`estimated_tokens (${parsed.estimated_tokens}) seems very high.`);
913
+ }
914
+ if (parsed.estimated_cost_usd < 0.01) {
915
+ warnings.push(
916
+ `estimated_cost_usd ($${parsed.estimated_cost_usd}) seems very low.`
917
+ );
918
+ }
919
+ if (parsed.estimated_time_minutes < 1) {
920
+ warnings.push(
921
+ `estimated_time_minutes (${parsed.estimated_time_minutes}) seems unrealistically low.`
922
+ );
923
+ }
924
+ }
925
+ async function validateSpec(specPath) {
926
+ const dir = resolve2(specPath);
927
+ const errors = [];
928
+ const warnings = [];
929
+ const detection = await detectSpecFormat(dir);
930
+ try {
931
+ const entries = await readdir2(dir, { withFileTypes: true });
932
+ const hasAnyFile = entries.some((e) => e.isFile());
933
+ if (!hasAnyFile) {
934
+ errors.push("Directory is empty or has no readable files");
935
+ }
780
936
  } catch {
937
+ errors.push("Directory is empty or unreadable");
781
938
  }
782
- return results;
939
+ const sidecarPath = join4(dir, SIDECAR_FILENAME);
940
+ if (await fileExists(sidecarPath)) {
941
+ try {
942
+ const raw = await readFile3(sidecarPath, "utf-8");
943
+ const parsed = parseYaml2(raw);
944
+ const sidecarResult = specmarketSidecarSchema.safeParse(parsed);
945
+ if (!sidecarResult.success) {
946
+ for (const issue of sidecarResult.error.issues) {
947
+ errors.push(
948
+ `${SIDECAR_FILENAME}: ${issue.path.join(".")} \u2014 ${issue.message}`
949
+ );
950
+ }
951
+ } else {
952
+ const sidecar = sidecarResult.data;
953
+ if (sidecar.estimated_tokens !== void 0) {
954
+ if (sidecar.estimated_tokens < 1e3) {
955
+ warnings.push(
956
+ `sidecar estimated_tokens (${sidecar.estimated_tokens}) seems very low.`
957
+ );
958
+ }
959
+ if (sidecar.estimated_tokens > 1e7) {
960
+ warnings.push(
961
+ `sidecar estimated_tokens (${sidecar.estimated_tokens}) seems very high.`
962
+ );
963
+ }
964
+ }
965
+ if (sidecar.estimated_cost_usd !== void 0 && sidecar.estimated_cost_usd < 0.01) {
966
+ warnings.push(
967
+ `sidecar estimated_cost_usd ($${sidecar.estimated_cost_usd}) seems very low.`
968
+ );
969
+ }
970
+ if (sidecar.estimated_time_minutes !== void 0 && sidecar.estimated_time_minutes < 1) {
971
+ warnings.push(
972
+ `sidecar estimated_time_minutes (${sidecar.estimated_time_minutes}) seems unrealistically low.`
973
+ );
974
+ }
975
+ }
976
+ } catch (err) {
977
+ errors.push(
978
+ `${SIDECAR_FILENAME}: Failed to read or parse: ${err.message}`
979
+ );
980
+ }
981
+ }
982
+ const hasSpecYaml = await fileExists(join4(dir, "spec.yaml"));
983
+ if (hasSpecYaml || detection.format === "specmarket-legacy") {
984
+ await validateLegacySpec(dir, errors, warnings);
985
+ }
986
+ switch (detection.format) {
987
+ case "specmarket-legacy":
988
+ break;
989
+ case "speckit": {
990
+ const hasSpecMd = await fileExists(join4(dir, "spec.md"));
991
+ const hasTasksMd = await fileExists(join4(dir, "tasks.md"));
992
+ const hasPlanMd = await fileExists(join4(dir, "plan.md"));
993
+ const hasSpecifyDir = await directoryExists(join4(dir, ".specify"));
994
+ if (!hasSpecMd) {
995
+ errors.push("speckit format requires spec.md");
996
+ }
997
+ if (!hasTasksMd && !hasPlanMd) {
998
+ errors.push("speckit format requires tasks.md or plan.md");
999
+ }
1000
+ if (!hasSpecifyDir) {
1001
+ warnings.push("speckit format: .specify/ directory is recommended");
1002
+ }
1003
+ break;
1004
+ }
1005
+ case "bmad": {
1006
+ const hasPrdMd = await fileExists(join4(dir, "prd.md"));
1007
+ const hasStory = await hasStoryFiles(dir);
1008
+ if (!hasPrdMd && !hasStory) {
1009
+ errors.push("bmad format requires prd.md or story-*.md files");
1010
+ }
1011
+ const hasArch = await fileExists(join4(dir, "architecture.md"));
1012
+ if (!hasArch) {
1013
+ warnings.push("bmad format: architecture.md is recommended");
1014
+ }
1015
+ break;
1016
+ }
1017
+ case "ralph": {
1018
+ const prdPath = join4(dir, "prd.json");
1019
+ if (!await fileExists(prdPath)) {
1020
+ errors.push("ralph format requires prd.json");
1021
+ break;
1022
+ }
1023
+ try {
1024
+ const raw = await readFile3(prdPath, "utf-8");
1025
+ const data = JSON.parse(raw);
1026
+ if (!data || typeof data !== "object" || !("userStories" in data) || !Array.isArray(data.userStories)) {
1027
+ errors.push("ralph format: prd.json must have userStories array");
1028
+ }
1029
+ } catch {
1030
+ errors.push("ralph format: prd.json must be valid JSON with userStories array");
1031
+ }
1032
+ break;
1033
+ }
1034
+ case "custom":
1035
+ default: {
1036
+ const hasMd = await hasMarkdownFiles(dir);
1037
+ if (!hasMd) {
1038
+ errors.push("custom format requires at least one .md file");
1039
+ break;
1040
+ }
1041
+ const textExtensions = /* @__PURE__ */ new Set([".md"]);
1042
+ const mdFiles = await collectFiles(dir, dir, textExtensions);
1043
+ let hasSubstantialMd = false;
1044
+ for (const f of mdFiles) {
1045
+ try {
1046
+ const content = await readFile3(join4(dir, f), "utf-8");
1047
+ if (content.length > 100) {
1048
+ hasSubstantialMd = true;
1049
+ break;
1050
+ }
1051
+ } catch {
1052
+ }
1053
+ }
1054
+ if (!hasSubstantialMd) {
1055
+ errors.push("custom format requires at least one .md file larger than 100 bytes");
1056
+ }
1057
+ break;
1058
+ }
1059
+ }
1060
+ return {
1061
+ valid: errors.length === 0,
1062
+ errors,
1063
+ warnings,
1064
+ format: detection.format,
1065
+ formatDetectedBy: detection.detectedBy
1066
+ };
783
1067
  }
784
1068
  function createValidateCommand() {
785
- return new Command5("validate").description("Validate a spec directory for completeness and schema compliance").argument("<path>", "Path to the spec directory").action(async (specPath) => {
1069
+ return new Command5("validate").description("Validate a spec directory for completeness and schema compliance").argument("[path]", "Path to the spec directory (defaults to current directory)", ".").action(async (specPath) => {
786
1070
  try {
787
1071
  const result = await validateSpec(specPath);
1072
+ if (result.format !== void 0) {
1073
+ console.log(chalk5.gray(`Detected format: ${result.format}`));
1074
+ }
788
1075
  if (result.warnings.length > 0) {
789
1076
  console.log(chalk5.yellow("\nWarnings:"));
790
1077
  for (const warning of result.warnings) {
@@ -819,9 +1106,9 @@ Validation failed with ${result.errors.length} error(s).`)
819
1106
  import { Command as Command6 } from "commander";
820
1107
  import chalk6 from "chalk";
821
1108
  import ora3 from "ora";
822
- import { readFile as readFile4, mkdir as mkdir4, writeFile as writeFileFn } from "fs/promises";
823
- import { join as join5, resolve as resolve4, isAbsolute } from "path";
824
- import { parse as parseYaml2 } from "yaml";
1109
+ import { readFile as readFile5, mkdir as mkdir4, writeFile as writeFileFn } from "fs/promises";
1110
+ import { join as join6, resolve as resolve4, isAbsolute } from "path";
1111
+ import { parse as parseYaml3 } from "yaml";
825
1112
 
826
1113
  // src/lib/telemetry.ts
827
1114
  import createDebug5 from "debug";
@@ -884,15 +1171,15 @@ async function promptTelemetryOptIn() {
884
1171
  default: false
885
1172
  }
886
1173
  ]);
887
- const { saveConfig: saveConfig2 } = await import("./config-R5KWZSJP.js");
1174
+ const { saveConfig: saveConfig2 } = await import("./config-OAU6SJLC.js");
888
1175
  await saveConfig2({ ...config, telemetry: optIn, telemetryPrompted: true });
889
1176
  return optIn;
890
1177
  }
891
1178
 
892
1179
  // src/lib/ralph-loop.ts
893
1180
  import { spawn } from "child_process";
894
- import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile3, access as access2 } from "fs/promises";
895
- import { join as join4 } from "path";
1181
+ import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4, access as access3 } from "fs/promises";
1182
+ import { join as join5 } from "path";
896
1183
  import { homedir as homedir2 } from "os";
897
1184
  import { randomUUID } from "crypto";
898
1185
  import { exec } from "child_process";
@@ -900,12 +1187,27 @@ import { promisify } from "util";
900
1187
  import createDebug6 from "debug";
901
1188
  var debug6 = createDebug6("specmarket:runner");
902
1189
  var execAsync = promisify(exec);
1190
+ async function checkClaudeCliInstalled() {
1191
+ try {
1192
+ await execAsync("which claude");
1193
+ } catch {
1194
+ throw new Error(
1195
+ `Claude CLI is not installed or not in your PATH.
1196
+
1197
+ Installation instructions:
1198
+ npm install -g @anthropic-ai/claude-code
1199
+
1200
+ Or visit: https://www.anthropic.com/claude-code
1201
+ `
1202
+ );
1203
+ }
1204
+ }
903
1205
  async function runSpec(specDir, specYaml, opts, onProgress) {
904
1206
  const maxLoops = opts.maxLoops ?? RUN_DEFAULTS.MAX_LOOPS;
905
1207
  const budgetTokens = opts.maxBudgetUsd ? opts.maxBudgetUsd / specYaml.estimatedCostUsd * specYaml.estimatedTokens : specYaml.estimatedTokens * RUN_DEFAULTS.BUDGET_MULTIPLIER;
906
1208
  const runId = opts.resumeRunId ?? randomUUID();
907
- const runsBaseDir = join4(homedir2(), CONFIG_PATHS.RUNS_DIR);
908
- const runDir = opts.outputDir ?? join4(runsBaseDir, runId);
1209
+ const runsBaseDir = join5(homedir2(), CONFIG_PATHS.RUNS_DIR);
1210
+ const runDir = opts.outputDir ?? join5(runsBaseDir, runId);
909
1211
  await mkdir3(runDir, { recursive: true });
910
1212
  debug6("Run directory: %s", runDir);
911
1213
  if (opts.dryRun) {
@@ -967,7 +1269,7 @@ async function runSpec(specDir, specYaml, opts, onProgress) {
967
1269
  iterations.push(iteration);
968
1270
  onProgress?.(iteration);
969
1271
  await writeFile3(
970
- join4(runDir, `iteration-${i}.json`),
1272
+ join5(runDir, `iteration-${i}.json`),
971
1273
  JSON.stringify(iteration, null, 2)
972
1274
  );
973
1275
  await stageAllChanges(runDir);
@@ -1030,7 +1332,7 @@ async function runSpec(specDir, specYaml, opts, onProgress) {
1030
1332
  cliVersion: opts.cliVersion
1031
1333
  };
1032
1334
  await writeFile3(
1033
- join4(runDir, "run-report.json"),
1335
+ join5(runDir, "run-report.json"),
1034
1336
  JSON.stringify(report, null, 2)
1035
1337
  );
1036
1338
  debug6("Run complete: %s (status=%s, loops=%d)", runId, finalStatus, iterations.length);
@@ -1038,7 +1340,7 @@ async function runSpec(specDir, specYaml, opts, onProgress) {
1038
1340
  }
1039
1341
  async function copySpecFiles(srcDir, destDir) {
1040
1342
  const { cp } = await import("fs/promises");
1041
- await cp(srcDir, join4(destDir, "spec"), { recursive: true });
1343
+ await cp(srcDir, join5(destDir, "spec"), { recursive: true });
1042
1344
  await cp(srcDir, destDir, { recursive: true, force: false });
1043
1345
  debug6("Spec files copied from %s to %s", srcDir, destDir);
1044
1346
  }
@@ -1072,7 +1374,7 @@ async function executeClaudeLoop(dir, model) {
1072
1374
  if (model) {
1073
1375
  args.push("--model", model);
1074
1376
  }
1075
- const proc = spawn("sh", ["-c", `cat PROMPT.md | claude-code ${args.join(" ")}`], {
1377
+ const proc = spawn("sh", ["-c", `cat PROMPT.md | claude ${args.join(" ")}`], {
1076
1378
  cwd: dir,
1077
1379
  stdio: ["inherit", "pipe", "pipe"]
1078
1380
  });
@@ -1089,7 +1391,7 @@ async function executeClaudeLoop(dir, model) {
1089
1391
  resolve7({ stdout, exitCode: code ?? 0 });
1090
1392
  });
1091
1393
  proc.on("error", (err) => {
1092
- debug6("claude-code spawn error: %O", err);
1394
+ debug6("claude spawn error: %O", err);
1093
1395
  resolve7({ stdout: "", exitCode: 1 });
1094
1396
  });
1095
1397
  });
@@ -1176,7 +1478,7 @@ async function checkCompletion(dir) {
1176
1478
  }
1177
1479
  async function isFixPlanEmpty(dir) {
1178
1480
  try {
1179
- const content = await readFile3(join4(dir, "fix_plan.md"), "utf-8");
1481
+ const content = await readFile4(join5(dir, "TASKS.md"), "utf-8");
1180
1482
  const hasUncheckedItems = /^- \[ \]/m.test(content);
1181
1483
  return !hasUncheckedItems;
1182
1484
  } catch {
@@ -1185,22 +1487,29 @@ async function isFixPlanEmpty(dir) {
1185
1487
  }
1186
1488
  async function runTests(dir) {
1187
1489
  const testRunners = [
1188
- { file: "package.json", cmd: "npm test -- --run 2>&1 || true" },
1189
- { file: "vitest.config.ts", cmd: "npx vitest run 2>&1 || true" },
1190
- { file: "pytest.ini", cmd: "python -m pytest --tb=no -q 2>&1 || true" },
1191
- { file: "Makefile", cmd: "make test 2>&1 || true" }
1490
+ { file: "package.json", cmd: "npm test -- --run 2>&1" },
1491
+ { file: "vitest.config.ts", cmd: "npx vitest run 2>&1" },
1492
+ { file: "pytest.ini", cmd: "python -m pytest --tb=no -q 2>&1" },
1493
+ { file: "Makefile", cmd: "make test 2>&1" }
1192
1494
  ];
1193
1495
  for (const runner of testRunners) {
1194
1496
  try {
1195
- await access2(join4(dir, runner.file));
1497
+ await access3(join5(dir, runner.file));
1498
+ } catch {
1499
+ continue;
1500
+ }
1501
+ try {
1196
1502
  const { stdout, stderr } = await execAsync(runner.cmd, {
1197
1503
  cwd: dir,
1198
1504
  timeout: 12e4
1199
1505
  });
1200
1506
  const combined = stdout + stderr;
1201
- const hasFailed = /\d+ failed|\d+ error|FAILED|ERROR/i.test(combined);
1507
+ const hasFailed = /\d+ failed|\d+ error/i.test(combined);
1202
1508
  return !hasFailed;
1203
- } catch {
1509
+ } catch (err) {
1510
+ if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
1511
+ return false;
1512
+ }
1204
1513
  continue;
1205
1514
  }
1206
1515
  }
@@ -1208,7 +1517,7 @@ async function runTests(dir) {
1208
1517
  }
1209
1518
  async function evaluateSuccessCriteria(dir) {
1210
1519
  try {
1211
- const content = await readFile3(join4(dir, "SUCCESS_CRITERIA.md"), "utf-8");
1520
+ const content = await readFile4(join5(dir, "SUCCESS_CRITERIA.md"), "utf-8");
1212
1521
  const lines = content.split("\n");
1213
1522
  const results = [];
1214
1523
  for (const line of lines) {
@@ -1227,7 +1536,7 @@ async function evaluateSuccessCriteria(dir) {
1227
1536
  }
1228
1537
  async function loadExistingReport(dir) {
1229
1538
  try {
1230
- const raw = await readFile3(join4(dir, "run-report.json"), "utf-8");
1539
+ const raw = await readFile4(join5(dir, "run-report.json"), "utf-8");
1231
1540
  return JSON.parse(raw);
1232
1541
  } catch {
1233
1542
  return null;
@@ -1265,8 +1574,8 @@ async function handleRun(specPathOrId, opts) {
1265
1574
  console.log(chalk6.yellow(` \u26A0 ${warning}`));
1266
1575
  }
1267
1576
  }
1268
- const specYamlContent = await readFile4(join5(specDir, "spec.yaml"), "utf-8");
1269
- const specYamlRaw = parseYaml2(specYamlContent);
1577
+ const specYamlContent = await readFile5(join6(specDir, "spec.yaml"), "utf-8");
1578
+ const specYamlRaw = parseYaml3(specYamlContent);
1270
1579
  const specYaml = specYamlSchema.parse(specYamlRaw);
1271
1580
  console.log("");
1272
1581
  console.log(chalk6.yellow("\u26A0 SECURITY WARNING"));
@@ -1284,6 +1593,13 @@ async function handleRun(specPathOrId, opts) {
1284
1593
  if (authed && !opts.noTelemetry) {
1285
1594
  await promptTelemetryOptIn();
1286
1595
  }
1596
+ try {
1597
+ await checkClaudeCliInstalled();
1598
+ } catch (err) {
1599
+ console.log(chalk6.red(`
1600
+ \u2717 ${err.message}`));
1601
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
1602
+ }
1287
1603
  const maxLoops = opts.maxLoops ? parseInt(opts.maxLoops, 10) : void 0;
1288
1604
  const maxBudget = opts.maxBudget ? parseFloat(opts.maxBudget) : void 0;
1289
1605
  console.log(chalk6.cyan(`
@@ -1375,10 +1691,10 @@ async function resolveSpecPath(pathOrId) {
1375
1691
  debug7("Treating %s as local path (no registry pattern match)", pathOrId);
1376
1692
  return { specDir: resolve4(pathOrId) };
1377
1693
  }
1378
- const { access: access3 } = await import("fs/promises");
1694
+ const { access: access4 } = await import("fs/promises");
1379
1695
  const localPath = resolve4(pathOrId);
1380
1696
  try {
1381
- await access3(localPath);
1697
+ await access4(localPath);
1382
1698
  debug7("Found local directory %s \u2014 using as local spec", localPath);
1383
1699
  return { specDir: localPath };
1384
1700
  } catch {
@@ -1442,7 +1758,7 @@ async function resolveSpecPath(pathOrId) {
1442
1758
  debug7("Got download URL for %s@%s", scopedName, resolvedVersion);
1443
1759
  const { tmpdir } = await import("os");
1444
1760
  const { randomUUID: randomUUID2 } = await import("crypto");
1445
- const tempDir = join5(tmpdir(), `specmarket-${randomUUID2()}`);
1761
+ const tempDir = join6(tmpdir(), `specmarket-${randomUUID2()}`);
1446
1762
  await mkdir4(tempDir, { recursive: true });
1447
1763
  let response;
1448
1764
  try {
@@ -1459,7 +1775,7 @@ async function resolveSpecPath(pathOrId) {
1459
1775
  throw err;
1460
1776
  }
1461
1777
  const buffer = Buffer.from(await response.arrayBuffer());
1462
- const zipPath = join5(tempDir, "spec.zip");
1778
+ const zipPath = join6(tempDir, "spec.zip");
1463
1779
  await writeFileFn(zipPath, buffer);
1464
1780
  const { execAsync: execAsync2 } = await import("./exec-K3BOXX3C.js");
1465
1781
  await execAsync2(`unzip -q "${zipPath}" -d "${tempDir}"`);
@@ -1471,7 +1787,7 @@ async function resolveSpecPath(pathOrId) {
1471
1787
  return { specDir: tempDir, registrySpecId };
1472
1788
  }
1473
1789
  function createRunCommand() {
1474
- return new Command6("run").description("Execute a spec locally using the Ralph Loop").argument("<path-or-id>", "Local path to spec directory or registry ID (@user/name[@version])").option("--max-loops <n>", "Maximum loop iterations (default: 50)").option("--max-budget <usd>", "Maximum budget in USD (default: 2x estimated)").option("--no-telemetry", "Disable telemetry submission for this run").option("--model <model>", "Override AI model (default: spec's min_model)").option("--dry-run", "Validate and show config without executing").option("--resume <run-id>", "Resume a previous run from where it left off").option("--output <dir>", "Custom output directory for run artifacts").action(async (pathOrId, opts) => {
1790
+ return new Command6("run").description("Execute a spec locally using the Ralph Loop").argument("[path-or-id]", "Local path to spec directory or registry ID (@user/name[@version])", ".").option("--max-loops <n>", "Maximum loop iterations (default: 50)").option("--max-budget <usd>", "Maximum budget in USD (default: 2x estimated)").option("--no-telemetry", "Disable telemetry submission for this run").option("--model <model>", "Override AI model (default: spec's min_model)").option("--dry-run", "Validate and show config without executing").option("--resume <run-id>", "Resume a previous run from where it left off").option("--output <dir>", "Custom output directory for run artifacts").action(async (pathOrId, opts) => {
1475
1791
  try {
1476
1792
  await handleRun(pathOrId, opts);
1477
1793
  } catch (err) {
@@ -1649,11 +1965,43 @@ async function handleInfo(specId) {
1649
1965
  const spinner = (await import("ora")).default(`Loading info for ${specId}...`).start();
1650
1966
  try {
1651
1967
  const isScopedName = specId.startsWith("@") || specId.includes("/");
1652
- const [spec, stats, versions] = await Promise.all([
1968
+ const [spec, stats, versionsResult] = await Promise.all([
1653
1969
  client.query(api2.specs.get, isScopedName ? { scopedName: specId } : { specId }),
1654
1970
  client.query(api2.runs.getStats, { specId }).catch(() => null),
1655
- client.query(api2.specs.getVersions, { specId }).catch(() => [])
1971
+ client.query(api2.specs.getVersions, { specId, paginationOpts: { numItems: 25, cursor: null } }).catch(() => ({ page: [] }))
1656
1972
  ]);
1973
+ const versions = versionsResult.page;
1974
+ let openIssueCount = 0;
1975
+ let maintainers = [];
1976
+ let commentCount = 0;
1977
+ if (spec) {
1978
+ const [issuesResult, maintainersResult, commentsResult] = await Promise.all([
1979
+ client.query(api2.issues.list, {
1980
+ specId: spec._id,
1981
+ status: "open",
1982
+ paginationOpts: { numItems: 1, cursor: null }
1983
+ }).catch(() => null),
1984
+ client.query(api2.specMaintainers.list, { specId: spec._id }).catch(() => []),
1985
+ client.query(api2.comments.list, {
1986
+ targetType: "spec",
1987
+ targetId: spec._id,
1988
+ paginationOpts: { numItems: 1, cursor: null }
1989
+ }).catch(() => null)
1990
+ ]);
1991
+ if (issuesResult) {
1992
+ openIssueCount = issuesResult.page.length;
1993
+ if (!issuesResult.isDone && openIssueCount > 0) {
1994
+ openIssueCount = -1;
1995
+ }
1996
+ }
1997
+ maintainers = maintainersResult;
1998
+ if (commentsResult) {
1999
+ commentCount = commentsResult.page.length;
2000
+ if (!commentsResult.isDone && commentCount > 0) {
2001
+ commentCount = -1;
2002
+ }
2003
+ }
2004
+ }
1657
2005
  spinner.stop();
1658
2006
  if (!spec) {
1659
2007
  console.log(chalk8.red(`Spec not found: ${specId}`));
@@ -1701,6 +2049,18 @@ async function handleInfo(specId) {
1701
2049
  if (spec.forkedFromId) {
1702
2050
  console.log(chalk8.gray(` (Forked from v${spec.forkedFromVersion})`));
1703
2051
  }
2052
+ const issueDisplay = openIssueCount === -1 ? "many" : String(openIssueCount);
2053
+ const commentDisplay = commentCount === -1 ? "many" : String(commentCount);
2054
+ console.log(
2055
+ ` Open Issues: ${issueDisplay}`
2056
+ );
2057
+ console.log(
2058
+ ` Comments: ${commentDisplay}`
2059
+ );
2060
+ if (maintainers.length > 0) {
2061
+ const names = maintainers.filter((m) => m.user).map((m) => `@${m.user.username}`).join(", ");
2062
+ console.log(` Maintainers: ${names}`);
2063
+ }
1704
2064
  if (author) {
1705
2065
  console.log("");
1706
2066
  console.log(chalk8.bold("Creator:"));
@@ -1785,9 +2145,9 @@ function createInfoCommand() {
1785
2145
  import { Command as Command9 } from "commander";
1786
2146
  import chalk9 from "chalk";
1787
2147
  import ora4 from "ora";
1788
- import { readFile as readFile5 } from "fs/promises";
1789
- import { join as join6, resolve as resolve5 } from "path";
1790
- import { parse as parseYaml3 } from "yaml";
2148
+ import { readFile as readFile6 } from "fs/promises";
2149
+ import { join as join7, resolve as resolve5 } from "path";
2150
+ import { parse as parseYaml4 } from "yaml";
1791
2151
  import { createWriteStream } from "fs";
1792
2152
  async function handlePublish(specPath, opts = {}) {
1793
2153
  const creds = await requireAuth();
@@ -1808,8 +2168,8 @@ async function handlePublish(specPath, opts = {}) {
1808
2168
  spinner.succeed("Spec validated");
1809
2169
  }
1810
2170
  spinner.start("Reading spec metadata...");
1811
- const specYamlContent = await readFile5(join6(dir, "spec.yaml"), "utf-8");
1812
- const specYamlRaw = parseYaml3(specYamlContent);
2171
+ const specYamlContent = await readFile6(join7(dir, "spec.yaml"), "utf-8");
2172
+ const specYamlRaw = parseYaml4(specYamlContent);
1813
2173
  const specYaml = specYamlSchema.parse(specYamlRaw);
1814
2174
  spinner.succeed(`Loaded spec: ${specYaml.display_name} v${specYaml.version}`);
1815
2175
  const client = await getConvexClient(creds.token);
@@ -1824,7 +2184,7 @@ async function handlePublish(specPath, opts = {}) {
1824
2184
  spinner.succeed("Spec archive created");
1825
2185
  spinner.start("Uploading spec to registry...");
1826
2186
  const uploadUrl = await client.mutation(api2.specs.generateUploadUrl, {});
1827
- const zipContent = await readFile5(zipPath);
2187
+ const zipContent = await readFile6(zipPath);
1828
2188
  const uploadResponse = await fetch(uploadUrl, {
1829
2189
  method: "POST",
1830
2190
  headers: { "Content-Type": "application/zip" },
@@ -1836,7 +2196,7 @@ async function handlePublish(specPath, opts = {}) {
1836
2196
  const { storageId } = await uploadResponse.json();
1837
2197
  spinner.succeed("Spec uploaded");
1838
2198
  spinner.start("Publishing to registry...");
1839
- const readme = await readFile5(join6(dir, "SPEC.md"), "utf-8").catch(() => void 0);
2199
+ const readme = await readFile6(join7(dir, "SPEC.md"), "utf-8").catch(() => void 0);
1840
2200
  const publishResult = await client.mutation(api2.specs.publish, {
1841
2201
  slug: specYaml.name,
1842
2202
  displayName: specYaml.display_name,
@@ -1878,7 +2238,7 @@ async function handlePublish(specPath, opts = {}) {
1878
2238
  async function createSpecZip(dir) {
1879
2239
  const { tmpdir } = await import("os");
1880
2240
  const { randomUUID: randomUUID2 } = await import("crypto");
1881
- const zipPath = join6(tmpdir(), `spec-${randomUUID2()}.zip`);
2241
+ const zipPath = join7(tmpdir(), `spec-${randomUUID2()}.zip`);
1882
2242
  const archiver = (await import("archiver")).default;
1883
2243
  const output = createWriteStream(zipPath);
1884
2244
  const archive = archiver("zip", { zlib: { level: 9 } });
@@ -1909,9 +2269,9 @@ function createPublishCommand() {
1909
2269
  import { Command as Command10 } from "commander";
1910
2270
  import chalk10 from "chalk";
1911
2271
  import ora5 from "ora";
1912
- import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile6 } from "fs/promises";
1913
- import { join as join7, resolve as resolve6 } from "path";
1914
- import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
2272
+ import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile7 } from "fs/promises";
2273
+ import { join as join8, resolve as resolve6 } from "path";
2274
+ import { parse as parseYaml5, stringify as stringifyYaml } from "yaml";
1915
2275
  async function handleFork(specId, targetPath) {
1916
2276
  const creds = await requireAuth();
1917
2277
  const spinner = ora5("Loading spec info...").start();
@@ -1939,9 +2299,9 @@ async function handleFork(specId, targetPath) {
1939
2299
  const targetDir = resolve6(targetPath ?? spec.slug);
1940
2300
  spinner.text = `Extracting to ${targetDir}...`;
1941
2301
  await downloadAndExtract(url, targetDir);
1942
- const specYamlPath = join7(targetDir, "spec.yaml");
1943
- const specYamlContent = await readFile6(specYamlPath, "utf-8");
1944
- const specYamlData = parseYaml4(specYamlContent);
2302
+ const specYamlPath = join8(targetDir, "spec.yaml");
2303
+ const specYamlContent = await readFile7(specYamlPath, "utf-8");
2304
+ const specYamlData = parseYaml5(specYamlContent);
1945
2305
  specYamlData["forked_from_id"] = spec._id;
1946
2306
  specYamlData["forked_from_version"] = spec.currentVersion;
1947
2307
  specYamlData["version"] = "1.0.0";
@@ -1974,7 +2334,7 @@ async function downloadAndExtract(url, targetDir) {
1974
2334
  const buffer = Buffer.from(await response.arrayBuffer());
1975
2335
  const { tmpdir } = await import("os");
1976
2336
  const { randomUUID: randomUUID2 } = await import("crypto");
1977
- const zipPath = join7(tmpdir(), `fork-${randomUUID2()}.zip`);
2337
+ const zipPath = join8(tmpdir(), `fork-${randomUUID2()}.zip`);
1978
2338
  const { writeFile: writeFileFn2, unlink: unlink2 } = await import("fs/promises");
1979
2339
  await writeFileFn2(zipPath, buffer);
1980
2340
  await mkdir5(targetDir, { recursive: true });
@@ -2001,15 +2361,15 @@ function createForkCommand() {
2001
2361
  // src/commands/report.ts
2002
2362
  import { Command as Command11 } from "commander";
2003
2363
  import chalk11 from "chalk";
2004
- import { readFile as readFile7 } from "fs/promises";
2005
- import { join as join8 } from "path";
2364
+ import { readFile as readFile8 } from "fs/promises";
2365
+ import { join as join9 } from "path";
2006
2366
  import { homedir as homedir3 } from "os";
2007
2367
  async function handleReport(runId) {
2008
- const localPath = join8(homedir3(), CONFIG_PATHS.RUNS_DIR, runId, "run-report.json");
2368
+ const localPath = join9(homedir3(), CONFIG_PATHS.RUNS_DIR, runId, "run-report.json");
2009
2369
  let report = null;
2010
2370
  let source = "local";
2011
2371
  try {
2012
- const raw = await readFile7(localPath, "utf-8");
2372
+ const raw = await readFile8(localPath, "utf-8");
2013
2373
  report = JSON.parse(raw);
2014
2374
  source = "local";
2015
2375
  } catch {
@@ -2257,6 +2617,443 @@ function createConfigCommand() {
2257
2617
  return configCmd;
2258
2618
  }
2259
2619
 
2620
+ // src/commands/issues.ts
2621
+ import { Command as Command13 } from "commander";
2622
+ import chalk13 from "chalk";
2623
+ import Table2 from "cli-table3";
2624
+ async function loadApi() {
2625
+ try {
2626
+ return (await import("./api-GIDUNUXG.js")).api;
2627
+ } catch {
2628
+ console.error(
2629
+ chalk13.red("Error: Could not load Convex API bindings. Is CONVEX_URL configured?")
2630
+ );
2631
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2632
+ }
2633
+ }
2634
+ async function resolveSpec(client, api2, specRef) {
2635
+ const isScopedName = specRef.startsWith("@") || specRef.includes("/");
2636
+ const spec = await client.query(
2637
+ api2.specs.get,
2638
+ isScopedName ? { scopedName: specRef } : { specId: specRef }
2639
+ );
2640
+ if (!spec) {
2641
+ console.error(chalk13.red(`Spec not found: ${specRef}`));
2642
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2643
+ }
2644
+ return spec;
2645
+ }
2646
+ function relativeTime(timestamp) {
2647
+ const diff = Date.now() - timestamp;
2648
+ const seconds = Math.floor(diff / 1e3);
2649
+ const minutes = Math.floor(seconds / 60);
2650
+ const hours = Math.floor(minutes / 60);
2651
+ const days = Math.floor(hours / 24);
2652
+ if (days > 0) return `${days}d ago`;
2653
+ if (hours > 0) return `${hours}h ago`;
2654
+ if (minutes > 0) return `${minutes}m ago`;
2655
+ return "just now";
2656
+ }
2657
+ async function handleIssuesList(specRef, opts) {
2658
+ const api2 = await loadApi();
2659
+ const client = await getConvexClient();
2660
+ const spinner = (await import("ora")).default("Loading issues...").start();
2661
+ try {
2662
+ const spec = await resolveSpec(client, api2, specRef);
2663
+ const statusFilter = opts.status === "all" ? void 0 : opts.status ?? "open";
2664
+ const result = await client.query(api2.issues.list, {
2665
+ specId: spec._id,
2666
+ status: statusFilter,
2667
+ paginationOpts: { numItems: 50, cursor: null }
2668
+ });
2669
+ spinner.stop();
2670
+ let issues = result.page;
2671
+ if (opts.label) {
2672
+ const label = opts.label.toLowerCase();
2673
+ issues = issues.filter(
2674
+ (i) => i.labels.some((l) => l.toLowerCase() === label)
2675
+ );
2676
+ }
2677
+ if (issues.length === 0) {
2678
+ const statusLabel = statusFilter ?? "any";
2679
+ console.log(chalk13.gray(`No ${statusLabel} issues for ${spec.scopedName}`));
2680
+ return;
2681
+ }
2682
+ console.log(
2683
+ chalk13.bold(`
2684
+ ${issues.length} issue(s) for ${spec.scopedName}:
2685
+ `)
2686
+ );
2687
+ const table = new Table2({
2688
+ head: [
2689
+ chalk13.cyan("#"),
2690
+ chalk13.cyan("Title"),
2691
+ chalk13.cyan("Author"),
2692
+ chalk13.cyan("Age"),
2693
+ chalk13.cyan("Labels")
2694
+ ],
2695
+ style: { compact: true },
2696
+ colWidths: [6, 40, 16, 10, 20],
2697
+ wordWrap: true
2698
+ });
2699
+ for (const issue of issues) {
2700
+ const statusIcon = issue.status === "open" ? chalk13.green("\u25CF") : chalk13.gray("\u25CB");
2701
+ table.push([
2702
+ `${statusIcon} ${issue.number}`,
2703
+ issue.title.slice(0, 60),
2704
+ issue.author ? `@${issue.author.username}` : chalk13.gray("unknown"),
2705
+ relativeTime(issue.createdAt),
2706
+ issue.labels.length > 0 ? issue.labels.join(", ") : chalk13.gray("\u2014")
2707
+ ]);
2708
+ }
2709
+ console.log(table.toString());
2710
+ console.log(
2711
+ chalk13.gray(
2712
+ `
2713
+ View: ${chalk13.cyan(`specmarket issues ${specRef} <number>`)}`
2714
+ )
2715
+ );
2716
+ } catch (err) {
2717
+ spinner.fail(chalk13.red(`Failed to load issues: ${err.message}`));
2718
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2719
+ }
2720
+ }
2721
+ async function handleIssuesCreate(specRef) {
2722
+ const creds = await requireAuth();
2723
+ const api2 = await loadApi();
2724
+ const client = await getConvexClient(creds.token);
2725
+ const spec = await resolveSpec(client, api2, specRef);
2726
+ const { default: inquirer } = await import("inquirer");
2727
+ const answers = await inquirer.prompt([
2728
+ {
2729
+ type: "input",
2730
+ name: "title",
2731
+ message: "Issue title:",
2732
+ validate: (v) => v.trim().length > 0 || "Title cannot be empty"
2733
+ },
2734
+ {
2735
+ type: "editor",
2736
+ name: "body",
2737
+ message: "Issue body (markdown):",
2738
+ validate: (v) => v.trim().length > 0 || "Body cannot be empty"
2739
+ }
2740
+ ]);
2741
+ const spinner = (await import("ora")).default("Creating issue...").start();
2742
+ try {
2743
+ const result = await client.mutation(api2.issues.create, {
2744
+ specId: spec._id,
2745
+ title: answers.title.trim(),
2746
+ body: answers.body.trim(),
2747
+ labels: []
2748
+ });
2749
+ spinner.succeed(
2750
+ chalk13.green(`Issue #${result.number} created on ${spec.scopedName}`)
2751
+ );
2752
+ } catch (err) {
2753
+ spinner.fail(chalk13.red(`Failed to create issue: ${err.message}`));
2754
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2755
+ }
2756
+ }
2757
+ async function handleIssuesView(specRef, issueNumber) {
2758
+ const api2 = await loadApi();
2759
+ const client = await getConvexClient();
2760
+ const spinner = (await import("ora")).default(`Loading issue #${issueNumber}...`).start();
2761
+ try {
2762
+ const spec = await resolveSpec(client, api2, specRef);
2763
+ const issue = await client.query(api2.issues.get, {
2764
+ specId: spec._id,
2765
+ number: issueNumber
2766
+ });
2767
+ if (!issue) {
2768
+ spinner.fail(chalk13.red(`Issue #${issueNumber} not found on ${spec.scopedName}`));
2769
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2770
+ }
2771
+ const commentsResult = await client.query(api2.comments.list, {
2772
+ targetType: "issue",
2773
+ targetId: issue._id,
2774
+ paginationOpts: { numItems: 10, cursor: null }
2775
+ });
2776
+ spinner.stop();
2777
+ const statusBadge = issue.status === "open" ? chalk13.green.bold(" OPEN ") : chalk13.gray.bold(" CLOSED ");
2778
+ console.log("");
2779
+ console.log(
2780
+ `${statusBadge} ${chalk13.bold(`#${issue.number}: ${issue.title}`)}`
2781
+ );
2782
+ console.log(chalk13.gray("\u2500".repeat(60)));
2783
+ console.log(
2784
+ `${chalk13.bold("Author:")} ${issue.author ? `@${issue.author.username}` : "unknown"} ${chalk13.bold("Created:")} ${new Date(issue.createdAt).toLocaleDateString()}`
2785
+ );
2786
+ if (issue.labels.length > 0) {
2787
+ console.log(`${chalk13.bold("Labels:")} ${issue.labels.join(", ")}`);
2788
+ }
2789
+ if (issue.closedAt) {
2790
+ console.log(
2791
+ `${chalk13.bold("Closed:")} ${new Date(issue.closedAt).toLocaleDateString()}`
2792
+ );
2793
+ }
2794
+ console.log("");
2795
+ console.log(issue.body);
2796
+ console.log("");
2797
+ if (commentsResult.page.length > 0) {
2798
+ console.log(
2799
+ chalk13.bold(`Comments (${issue.commentCount}):`)
2800
+ );
2801
+ console.log(chalk13.gray("\u2500".repeat(40)));
2802
+ for (const comment of commentsResult.page) {
2803
+ const author = comment.author ? `@${comment.author.username}` : "unknown";
2804
+ const edited = comment.editedAt ? chalk13.gray(" (edited)") : "";
2805
+ console.log(
2806
+ ` ${chalk13.bold(author)} \u2014 ${relativeTime(comment.createdAt)}${edited}`
2807
+ );
2808
+ console.log(` ${comment.body}`);
2809
+ if (comment.replies && comment.replies.length > 0) {
2810
+ for (const reply of comment.replies) {
2811
+ const replyAuthor = reply.author ? `@${reply.author.username}` : "unknown";
2812
+ const replyEdited = reply.editedAt ? chalk13.gray(" (edited)") : "";
2813
+ console.log(
2814
+ ` ${chalk13.bold(replyAuthor)} \u2014 ${relativeTime(reply.createdAt)}${replyEdited}`
2815
+ );
2816
+ console.log(` ${reply.body}`);
2817
+ }
2818
+ }
2819
+ console.log("");
2820
+ }
2821
+ } else {
2822
+ console.log(chalk13.gray("No comments yet."));
2823
+ }
2824
+ } catch (err) {
2825
+ spinner.fail(
2826
+ chalk13.red(`Failed to load issue: ${err.message}`)
2827
+ );
2828
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2829
+ }
2830
+ }
2831
+ async function handleIssuesClose(specRef, issueNumber) {
2832
+ const creds = await requireAuth();
2833
+ const api2 = await loadApi();
2834
+ const client = await getConvexClient(creds.token);
2835
+ const spinner = (await import("ora")).default(`Closing issue #${issueNumber}...`).start();
2836
+ try {
2837
+ const spec = await resolveSpec(client, api2, specRef);
2838
+ const issue = await client.query(api2.issues.get, {
2839
+ specId: spec._id,
2840
+ number: issueNumber
2841
+ });
2842
+ if (!issue) {
2843
+ spinner.fail(chalk13.red(`Issue #${issueNumber} not found on ${spec.scopedName}`));
2844
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2845
+ }
2846
+ await client.mutation(api2.issues.close, {
2847
+ issueId: issue._id
2848
+ });
2849
+ spinner.succeed(
2850
+ chalk13.green(`Issue #${issueNumber} closed on ${spec.scopedName}`)
2851
+ );
2852
+ } catch (err) {
2853
+ spinner.fail(chalk13.red(`Failed to close issue: ${err.message}`));
2854
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2855
+ }
2856
+ }
2857
+ async function handleIssuesReopen(specRef, issueNumber) {
2858
+ const creds = await requireAuth();
2859
+ const api2 = await loadApi();
2860
+ const client = await getConvexClient(creds.token);
2861
+ const spinner = (await import("ora")).default(`Reopening issue #${issueNumber}...`).start();
2862
+ try {
2863
+ const spec = await resolveSpec(client, api2, specRef);
2864
+ const issue = await client.query(api2.issues.get, {
2865
+ specId: spec._id,
2866
+ number: issueNumber
2867
+ });
2868
+ if (!issue) {
2869
+ spinner.fail(chalk13.red(`Issue #${issueNumber} not found on ${spec.scopedName}`));
2870
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2871
+ }
2872
+ await client.mutation(api2.issues.reopen, {
2873
+ issueId: issue._id
2874
+ });
2875
+ spinner.succeed(
2876
+ chalk13.green(`Issue #${issueNumber} reopened on ${spec.scopedName}`)
2877
+ );
2878
+ } catch (err) {
2879
+ spinner.fail(
2880
+ chalk13.red(`Failed to reopen issue: ${err.message}`)
2881
+ );
2882
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2883
+ }
2884
+ }
2885
+ function createIssuesCommand() {
2886
+ return new Command13("issues").description("Manage issues on a spec").argument("<spec-id>", "Spec scoped name (@user/name) or document ID").argument("[action-or-number]", 'Issue number or "create"').argument("[action]", '"close" or "reopen" (with issue number)').option(
2887
+ "-s, --status <status>",
2888
+ "Filter by status: open, closed, all (default: open)"
2889
+ ).option("--label <label>", "Filter by label").action(
2890
+ async (specId, actionOrNumber, action, opts) => {
2891
+ try {
2892
+ if (!actionOrNumber) {
2893
+ await handleIssuesList(specId, opts);
2894
+ } else if (actionOrNumber === "create") {
2895
+ await handleIssuesCreate(specId);
2896
+ } else {
2897
+ const issueNumber = parseInt(actionOrNumber, 10);
2898
+ if (isNaN(issueNumber) || issueNumber < 1) {
2899
+ console.error(
2900
+ chalk13.red(
2901
+ `Invalid issue number or action: "${actionOrNumber}". Use a number or "create".`
2902
+ )
2903
+ );
2904
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2905
+ }
2906
+ if (!action) {
2907
+ await handleIssuesView(specId, issueNumber);
2908
+ } else if (action === "close") {
2909
+ await handleIssuesClose(specId, issueNumber);
2910
+ } else if (action === "reopen") {
2911
+ await handleIssuesReopen(specId, issueNumber);
2912
+ } else {
2913
+ console.error(
2914
+ chalk13.red(
2915
+ `Unknown action: "${action}". Use "close" or "reopen".`
2916
+ )
2917
+ );
2918
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2919
+ }
2920
+ }
2921
+ } catch (err) {
2922
+ const error = err;
2923
+ if (error.code === String(EXIT_CODES.AUTH_ERROR)) {
2924
+ console.error(chalk13.red(error.message));
2925
+ process.exit(EXIT_CODES.AUTH_ERROR);
2926
+ }
2927
+ console.error(chalk13.red(`Error: ${error.message}`));
2928
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2929
+ }
2930
+ }
2931
+ );
2932
+ }
2933
+
2934
+ // src/commands/comment.ts
2935
+ import { Command as Command14 } from "commander";
2936
+ import chalk14 from "chalk";
2937
+ async function handleComment(targetType, targetRef, body, opts) {
2938
+ const creds = await requireAuth();
2939
+ let api2;
2940
+ try {
2941
+ api2 = (await import("./api-GIDUNUXG.js")).api;
2942
+ } catch {
2943
+ console.error(
2944
+ chalk14.red("Error: Could not load Convex API bindings. Is CONVEX_URL configured?")
2945
+ );
2946
+ process.exit(EXIT_CODES.NETWORK_ERROR);
2947
+ }
2948
+ const client = await getConvexClient(creds.token);
2949
+ const spinner = (await import("ora")).default("Posting comment...").start();
2950
+ try {
2951
+ let resolvedTargetType;
2952
+ let resolvedTargetId;
2953
+ if (targetType === "spec") {
2954
+ resolvedTargetType = "spec";
2955
+ const isScopedName = targetRef.startsWith("@") || targetRef.includes("/");
2956
+ const spec = await client.query(
2957
+ api2.specs.get,
2958
+ isScopedName ? { scopedName: targetRef } : { specId: targetRef }
2959
+ );
2960
+ if (!spec) {
2961
+ spinner.fail(chalk14.red(`Spec not found: ${targetRef}`));
2962
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2963
+ }
2964
+ resolvedTargetId = spec._id;
2965
+ } else if (targetType === "issue") {
2966
+ resolvedTargetType = "issue";
2967
+ const hashIndex = targetRef.lastIndexOf("#");
2968
+ if (hashIndex === -1) {
2969
+ spinner.fail(
2970
+ chalk14.red(
2971
+ "Invalid issue reference. Use format: @user/spec#<number>"
2972
+ )
2973
+ );
2974
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2975
+ }
2976
+ const specRef = targetRef.slice(0, hashIndex);
2977
+ const issueNumber = parseInt(targetRef.slice(hashIndex + 1), 10);
2978
+ if (isNaN(issueNumber) || issueNumber < 1) {
2979
+ spinner.fail(
2980
+ chalk14.red(`Invalid issue number in "${targetRef}". Use format: @user/spec#<number>`)
2981
+ );
2982
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2983
+ }
2984
+ const isScopedName = specRef.startsWith("@") || specRef.includes("/");
2985
+ const spec = await client.query(
2986
+ api2.specs.get,
2987
+ isScopedName ? { scopedName: specRef } : { specId: specRef }
2988
+ );
2989
+ if (!spec) {
2990
+ spinner.fail(chalk14.red(`Spec not found: ${specRef}`));
2991
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
2992
+ }
2993
+ const issue = await client.query(api2.issues.get, {
2994
+ specId: spec._id,
2995
+ number: issueNumber
2996
+ });
2997
+ if (!issue) {
2998
+ spinner.fail(
2999
+ chalk14.red(`Issue #${issueNumber} not found on ${spec.scopedName}`)
3000
+ );
3001
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
3002
+ }
3003
+ resolvedTargetId = issue._id;
3004
+ } else if (targetType === "bounty") {
3005
+ resolvedTargetType = "bounty";
3006
+ resolvedTargetId = targetRef;
3007
+ } else {
3008
+ spinner.fail(
3009
+ chalk14.red(
3010
+ `Invalid target type: "${targetType}". Use "spec", "issue", or "bounty".`
3011
+ )
3012
+ );
3013
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
3014
+ }
3015
+ const args = {
3016
+ targetType: resolvedTargetType,
3017
+ targetId: resolvedTargetId,
3018
+ body: body.trim()
3019
+ };
3020
+ if (opts.reply) {
3021
+ args.parentId = opts.reply;
3022
+ }
3023
+ await client.mutation(api2.comments.create, args);
3024
+ spinner.succeed(chalk14.green(`Comment posted on ${targetType} ${targetRef}`));
3025
+ } catch (err) {
3026
+ spinner.fail(chalk14.red(`Failed to post comment: ${err.message}`));
3027
+ process.exit(EXIT_CODES.NETWORK_ERROR);
3028
+ }
3029
+ }
3030
+ function createCommentCommand() {
3031
+ return new Command14("comment").description("Post a comment on a spec, issue, or bounty (requires login)").argument(
3032
+ "<target-type>",
3033
+ "Target type: spec, issue, or bounty"
3034
+ ).argument(
3035
+ "<target-ref>",
3036
+ "Target reference (e.g., @user/spec, @user/spec#3, bounty-id)"
3037
+ ).argument("<body>", "Comment body text").option(
3038
+ "--reply <comment-id>",
3039
+ "Reply to a specific comment (threading)"
3040
+ ).action(
3041
+ async (targetType, targetRef, body, opts) => {
3042
+ try {
3043
+ await handleComment(targetType, targetRef, body, opts);
3044
+ } catch (err) {
3045
+ const error = err;
3046
+ if (error.code === String(EXIT_CODES.AUTH_ERROR)) {
3047
+ console.error(chalk14.red(error.message));
3048
+ process.exit(EXIT_CODES.AUTH_ERROR);
3049
+ }
3050
+ console.error(chalk14.red(`Error: ${error.message}`));
3051
+ process.exit(EXIT_CODES.NETWORK_ERROR);
3052
+ }
3053
+ }
3054
+ );
3055
+ }
3056
+
2260
3057
  // src/index.ts
2261
3058
  import { createRequire as createRequire2 } from "module";
2262
3059
  var _require2 = createRequire2(import.meta.url);
@@ -2277,6 +3074,8 @@ program.addCommand(createSearchCommand());
2277
3074
  program.addCommand(createInfoCommand());
2278
3075
  program.addCommand(createPublishCommand());
2279
3076
  program.addCommand(createForkCommand());
3077
+ program.addCommand(createIssuesCommand());
3078
+ program.addCommand(createCommentCommand());
2280
3079
  program.addCommand(createReportCommand());
2281
3080
  program.addCommand(createConfigCommand());
2282
3081
  program.parseAsync(process.argv).catch((err) => {