@clawplays/ospec-cli 0.3.9 → 0.3.10

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.
@@ -1,8 +1,14 @@
1
1
  #!/bin/sh
2
2
 
3
- if [ ! -f "build-index-auto.js" ]; then
4
- echo "[ospec] build-index-auto.js not found, skip hook check"
3
+ if [ -f ".ospec/tools/build-index-auto.cjs" ]; then
4
+ OSPEC_BUILD_INDEX_SCRIPT=".ospec/tools/build-index-auto.cjs"
5
+ elif [ -f "build-index-auto.cjs" ]; then
6
+ OSPEC_BUILD_INDEX_SCRIPT="build-index-auto.cjs"
7
+ elif [ -f "build-index-auto.js" ]; then
8
+ OSPEC_BUILD_INDEX_SCRIPT="build-index-auto.js"
9
+ else
10
+ echo "[ospec] .ospec/tools/build-index-auto.cjs not found, skip hook check"
5
11
  exit 0
6
12
  fi
7
13
 
8
- node build-index-auto.js hook-check post-merge
14
+ node "$OSPEC_BUILD_INDEX_SCRIPT" hook-check post-merge
@@ -1,8 +1,14 @@
1
1
  #!/bin/sh
2
2
 
3
- if [ ! -f "build-index-auto.js" ]; then
4
- echo "[ospec] build-index-auto.js not found, skip hook check"
3
+ if [ -f ".ospec/tools/build-index-auto.cjs" ]; then
4
+ OSPEC_BUILD_INDEX_SCRIPT=".ospec/tools/build-index-auto.cjs"
5
+ elif [ -f "build-index-auto.cjs" ]; then
6
+ OSPEC_BUILD_INDEX_SCRIPT="build-index-auto.cjs"
7
+ elif [ -f "build-index-auto.js" ]; then
8
+ OSPEC_BUILD_INDEX_SCRIPT="build-index-auto.js"
9
+ else
10
+ echo "[ospec] .ospec/tools/build-index-auto.cjs not found, skip hook check"
5
11
  exit 0
6
12
  fi
7
13
 
8
- node build-index-auto.js hook-check pre-commit
14
+ node "$OSPEC_BUILD_INDEX_SCRIPT" hook-check pre-commit
package/dist/cli.js CHANGED
@@ -224,7 +224,7 @@ const services_1 = require("./services");
224
224
 
225
225
 
226
226
 
227
- const CLI_VERSION = '0.3.9';
227
+ const CLI_VERSION = '0.3.10';
228
228
 
229
229
  function showInitUsage() {
230
230
  console.log('Usage: ospec init [root-dir] [--summary "..."] [--tech-stack node,react] [--architecture "..."] [--document-language en-US|zh-CN|ja-JP|ar]');
@@ -4,7 +4,6 @@ const fs = require('fs');
4
4
  const fsp = require('fs/promises');
5
5
  const path = require('path');
6
6
  const { spawnSync } = require('child_process');
7
- const matter = require('gray-matter');
8
7
 
9
8
  const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'changes', 'for-ai']);
10
9
  const INDEX_FILE = 'SKILL.index.json';
@@ -469,7 +468,7 @@ function analyzeWorkflowChecklistDocument(content, options) {
469
468
 
470
469
  if (hasFrontmatter) {
471
470
  try {
472
- parsed = matter(content);
471
+ parsed = parseFrontmatter(content, { strict: true });
473
472
  } catch (error) {
474
473
  parseError = error;
475
474
  }
@@ -484,8 +483,8 @@ function analyzeWorkflowChecklistDocument(content, options) {
484
483
  const missingActivatedSteps = optionalStepsFieldValid
485
484
  ? options.activatedSteps.filter(step => !optionalSteps.includes(step))
486
485
  : [...options.activatedSteps];
487
- const checklistItems = parsed?.content.match(/^\s*-\s+\[(?: |x|X)\]\s+.+$/gm) ?? [];
488
- const uncheckedItems = parsed?.content.match(/^\s*-\s+\[ \]\s+.+$/gm) ?? [];
486
+ const checklistItems = parsed?.body.match(/^\s*-\s+\[(?: |x|X)\]\s+.+$/gm) ?? [];
487
+ const uncheckedItems = parsed?.body.match(/^\s*-\s+\[ \]\s+.+$/gm) ?? [];
489
488
  const checklistStructureValid = checklistItems.length > 0;
490
489
 
491
490
  let frontmatterMessage = `${options.name} frontmatter parsed successfully`;
@@ -553,40 +552,61 @@ function normalizeLineEndings(content) {
553
552
  return String(content || '').replace(/\r\n?/g, '\n');
554
553
  }
555
554
 
556
- function parseFrontmatter(content) {
557
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
555
+ function parseFrontmatter(content, options = {}) {
556
+ const normalizedContent = normalizeLineEndings(content);
557
+ const match = normalizedContent.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
558
558
  if (!match) {
559
- return { data: {}, body: content };
559
+ return { data: {}, body: normalizedContent };
560
560
  }
561
561
 
562
562
  const data = {};
563
- const lines = match[1].split(/\r?\n/);
563
+ const lines = match[1].split('\n');
564
564
  let currentKey = null;
565
565
 
566
- for (const line of lines) {
566
+ for (let index = 0; index < lines.length; index += 1) {
567
+ const line = lines[index];
568
+ const lineNumber = index + 1;
569
+ const trimmed = line.trim();
570
+
571
+ if (trimmed === '' || trimmed.startsWith('#')) {
572
+ continue;
573
+ }
574
+
567
575
  if (/^\s*-\s+/.test(line) && currentKey) {
568
576
  if (!Array.isArray(data[currentKey])) {
569
577
  data[currentKey] = [];
570
578
  }
571
- data[currentKey].push(parseValue(line.replace(/^\s*-\s+/, '').trim()));
579
+ data[currentKey].push(
580
+ parseValue(line.replace(/^\s*-\s+/, '').trim(), options, {
581
+ key: currentKey,
582
+ lineNumber,
583
+ })
584
+ );
572
585
  continue;
573
586
  }
574
587
 
588
+ if (/^\s*-\s+/.test(line) && options.strict) {
589
+ throw createFrontmatterParseError('Unexpected list item outside an array field', lineNumber);
590
+ }
591
+
575
592
  const keyMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
576
593
  if (!keyMatch) {
594
+ if (options.strict) {
595
+ throw createFrontmatterParseError(`Invalid frontmatter line: ${trimmed}`, lineNumber);
596
+ }
577
597
  currentKey = null;
578
598
  continue;
579
599
  }
580
600
 
581
601
  const key = keyMatch[1];
582
602
  const rawValue = keyMatch[2].trim();
583
- data[key] = parseValue(rawValue);
603
+ data[key] = parseValue(rawValue, options, { key, lineNumber });
584
604
  currentKey = Array.isArray(data[key]) && rawValue === '' ? key : null;
585
605
  }
586
606
 
587
607
  return {
588
608
  data,
589
- body: content.slice(match[0].length),
609
+ body: normalizedContent.slice(match[0].length),
590
610
  };
591
611
  }
592
612
 
@@ -609,7 +629,7 @@ function isValidFrontmatterField(value, type) {
609
629
  return false;
610
630
  }
611
631
 
612
- function parseValue(rawValue) {
632
+ function parseValue(rawValue, options = {}, context = {}) {
613
633
  if (rawValue === '') {
614
634
  return [];
615
635
  }
@@ -622,25 +642,102 @@ function parseValue(rawValue) {
622
642
  if (rawValue === 'false') {
623
643
  return false;
624
644
  }
645
+ if (options.strict) {
646
+ validateFrontmatterValue(rawValue, context);
647
+ }
625
648
  if (/^\[(.*)\]$/.test(rawValue)) {
626
649
  const inner = rawValue.slice(1, -1).trim();
627
650
  if (!inner) {
628
651
  return [];
629
652
  }
630
653
 
631
- return inner
632
- .split(',')
633
- .map(item => stripQuotes(item.trim()))
634
- .filter(Boolean);
654
+ return splitInlineArray(inner, options, context);
635
655
  }
636
656
 
637
657
  return stripQuotes(rawValue);
638
658
  }
639
659
 
660
+ function validateFrontmatterValue(rawValue, context) {
661
+ const startsArray = rawValue.startsWith('[');
662
+ const endsArray = rawValue.endsWith(']');
663
+ if (startsArray !== endsArray) {
664
+ throw createFrontmatterParseError(
665
+ `Unterminated inline array for ${context.key || 'field'}`,
666
+ context.lineNumber
667
+ );
668
+ }
669
+
670
+ if (!rawValue) {
671
+ return;
672
+ }
673
+
674
+ const quote = rawValue[0];
675
+ if ((quote === '"' || quote === "'") && rawValue[rawValue.length - 1] !== quote) {
676
+ throw createFrontmatterParseError(
677
+ `Unterminated quoted string for ${context.key || 'field'}`,
678
+ context.lineNumber
679
+ );
680
+ }
681
+ }
682
+
683
+ function splitInlineArray(inner, options = {}, context = {}) {
684
+ const values = [];
685
+ let current = '';
686
+ let activeQuote = null;
687
+
688
+ for (let index = 0; index < inner.length; index += 1) {
689
+ const char = inner[index];
690
+ if (activeQuote) {
691
+ current += char;
692
+ if (char === activeQuote && inner[index - 1] !== '\\') {
693
+ activeQuote = null;
694
+ }
695
+ continue;
696
+ }
697
+
698
+ if (char === '"' || char === "'") {
699
+ activeQuote = char;
700
+ current += char;
701
+ continue;
702
+ }
703
+
704
+ if (char === ',') {
705
+ const parsed = parseValue(current.trim(), {}, context);
706
+ if (parsed !== '') {
707
+ values.push(parsed);
708
+ }
709
+ current = '';
710
+ continue;
711
+ }
712
+
713
+ current += char;
714
+ }
715
+
716
+ if (activeQuote && options.strict) {
717
+ throw createFrontmatterParseError(
718
+ `Unterminated quoted string in inline array for ${context.key || 'field'}`,
719
+ context.lineNumber
720
+ );
721
+ }
722
+
723
+ const parsed = parseValue(current.trim(), {}, context);
724
+ if (parsed !== '') {
725
+ values.push(parsed);
726
+ }
727
+
728
+ return values.filter(value => value !== '');
729
+ }
730
+
640
731
  function stripQuotes(value) {
641
732
  return value.replace(/^['"]|['"]$/g, '');
642
733
  }
643
734
 
735
+ function createFrontmatterParseError(message, lineNumber) {
736
+ const error = new Error(lineNumber ? `line ${lineNumber}: ${message}` : message);
737
+ error.name = 'FrontmatterParseError';
738
+ return error;
739
+ }
740
+
644
741
  function extractSections(content) {
645
742
  const sections = {};
646
743
  const matches = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawplays/ospec-cli",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Official OSpec CLI package for spec-driven development (SDD) and document-driven development in AI coding agent and CLI workflows.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",