kiro-spec-engine 1.37.0 → 1.38.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/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.38.0] - 2026-02-10
11
+
12
+ ### Added
13
+ - **Scene Registry Statistics**: Dashboard for local scene package registry metrics
14
+ - `kse scene stats` show aggregate statistics (packages, versions, tags, ownership, deprecation, last publish)
15
+ - `--registry <dir>` custom registry directory
16
+ - `--json` structured JSON output
17
+ - **Scene Version Locking**: Protect specific package versions from accidental unpublish
18
+ - `kse scene lock set --name <pkg> --version <ver>` lock a version
19
+ - `kse scene lock rm --name <pkg> --version <ver>` unlock a version
20
+ - `kse scene lock ls --name <pkg>` list locked versions
21
+ - `--registry <dir>` custom registry directory
22
+ - `--json` structured JSON output
23
+ - Lock state stored as `locked: true` on version entries in `registry-index.json`
24
+
10
25
  ## [1.37.0] - 2026-02-10
11
26
 
12
27
  ### Added
@@ -681,6 +681,53 @@ function registerSceneCommands(program) {
681
681
  .action(async (options) => {
682
682
  await runSceneTagCommand({ ...options, action: 'ls' });
683
683
  });
684
+
685
+ // ── scene stats ──
686
+ sceneCmd
687
+ .command('stats')
688
+ .description('Show aggregate statistics about the local scene package registry')
689
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
690
+ .option('--json', 'Print result as JSON')
691
+ .action(async (options) => {
692
+ await runSceneStatsCommand(options);
693
+ });
694
+
695
+ // ── scene lock ──
696
+ const lockCmd = sceneCmd
697
+ .command('lock')
698
+ .description('Manage version locks on scene packages');
699
+
700
+ lockCmd
701
+ .command('set')
702
+ .description('Lock a specific version of a package')
703
+ .requiredOption('-n, --name <name>', 'Package name')
704
+ .requiredOption('-v, --version <version>', 'Version to lock')
705
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
706
+ .option('--json', 'Print result as JSON')
707
+ .action(async (options) => {
708
+ await runSceneLockCommand({ ...options, action: 'set' });
709
+ });
710
+
711
+ lockCmd
712
+ .command('rm')
713
+ .description('Unlock a specific version of a package')
714
+ .requiredOption('-n, --name <name>', 'Package name')
715
+ .requiredOption('-v, --version <version>', 'Version to unlock')
716
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
717
+ .option('--json', 'Print result as JSON')
718
+ .action(async (options) => {
719
+ await runSceneLockCommand({ ...options, action: 'rm' });
720
+ });
721
+
722
+ lockCmd
723
+ .command('ls')
724
+ .description('List all locked versions for a package')
725
+ .requiredOption('-n, --name <name>', 'Package name')
726
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
727
+ .option('--json', 'Print result as JSON')
728
+ .action(async (options) => {
729
+ await runSceneLockCommand({ ...options, action: 'ls' });
730
+ });
684
731
  }
685
732
 
686
733
  function normalizeSourceOptions(options = {}) {
@@ -11698,6 +11745,254 @@ function printSceneTagSummary(options, payload) {
11698
11745
  }
11699
11746
  }
11700
11747
 
11748
+ // ── Scene Stats ───────────────────────────────────────────────────────────
11749
+
11750
+ function normalizeSceneStatsOptions(options = {}) {
11751
+ return {
11752
+ registry: options.registry ? String(options.registry).trim() : '.kiro/registry',
11753
+ json: options.json === true
11754
+ };
11755
+ }
11756
+
11757
+ function validateSceneStatsOptions(options) {
11758
+ return null;
11759
+ }
11760
+
11761
+ async function runSceneStatsCommand(rawOptions = {}, dependencies = {}) {
11762
+ const projectRoot = dependencies.projectRoot || process.cwd();
11763
+ const fileSystem = dependencies.fileSystem || fs;
11764
+
11765
+ const options = normalizeSceneStatsOptions(rawOptions);
11766
+ const validationError = validateSceneStatsOptions(options);
11767
+
11768
+ if (validationError) {
11769
+ console.error(chalk.red(`Scene stats failed: ${validationError}`));
11770
+ process.exitCode = 1;
11771
+ return null;
11772
+ }
11773
+
11774
+ try {
11775
+ const registryRoot = path.isAbsolute(options.registry)
11776
+ ? options.registry
11777
+ : path.join(projectRoot, options.registry);
11778
+
11779
+ const index = await loadRegistryIndex(registryRoot, fileSystem);
11780
+ const packages = index.packages || {};
11781
+ const packageNames = Object.keys(packages);
11782
+
11783
+ let totalVersions = 0;
11784
+ let totalTags = 0;
11785
+ let packagesWithOwner = 0;
11786
+ let deprecatedPackages = 0;
11787
+ let mostRecent = null;
11788
+
11789
+ for (const name of packageNames) {
11790
+ const pkg = packages[name];
11791
+ const versions = pkg.versions || {};
11792
+ const versionKeys = Object.keys(versions);
11793
+ totalVersions += versionKeys.length;
11794
+ totalTags += Object.keys(pkg.tags || {}).length;
11795
+
11796
+ if (pkg.owner && String(pkg.owner).trim() !== '') {
11797
+ packagesWithOwner++;
11798
+ }
11799
+ if (pkg.deprecated) {
11800
+ deprecatedPackages++;
11801
+ }
11802
+
11803
+ for (const ver of versionKeys) {
11804
+ const publishedAt = versions[ver].published_at;
11805
+ if (publishedAt && (!mostRecent || publishedAt > mostRecent.publishedAt)) {
11806
+ mostRecent = { package: name, version: ver, publishedAt };
11807
+ }
11808
+ }
11809
+ }
11810
+
11811
+ const payload = {
11812
+ success: true,
11813
+ totalPackages: packageNames.length,
11814
+ totalVersions,
11815
+ totalTags,
11816
+ packagesWithOwner,
11817
+ packagesWithoutOwner: packageNames.length - packagesWithOwner,
11818
+ deprecatedPackages,
11819
+ mostRecentlyPublished: mostRecent,
11820
+ registry: options.registry
11821
+ };
11822
+
11823
+ printSceneStatsSummary(options, payload);
11824
+ return payload;
11825
+ } catch (error) {
11826
+ console.error(chalk.red('Scene stats failed:'), error.message);
11827
+ process.exitCode = 1;
11828
+ return null;
11829
+ }
11830
+ }
11831
+
11832
+ function printSceneStatsSummary(options, payload) {
11833
+ if (options.json) {
11834
+ console.log(JSON.stringify(payload, null, 2));
11835
+ return;
11836
+ }
11837
+
11838
+ console.log(chalk.bold('Registry Statistics'));
11839
+ console.log(` Packages: ${payload.totalPackages}`);
11840
+ console.log(` Versions: ${payload.totalVersions}`);
11841
+ console.log(` Tags: ${payload.totalTags}`);
11842
+ console.log(` With owner: ${payload.packagesWithOwner}`);
11843
+ console.log(` No owner: ${payload.packagesWithoutOwner}`);
11844
+ console.log(` Deprecated: ${payload.deprecatedPackages}`);
11845
+
11846
+ if (payload.mostRecentlyPublished) {
11847
+ const mr = payload.mostRecentlyPublished;
11848
+ console.log(` Last publish: ${mr.package}@${mr.version} (${mr.publishedAt})`);
11849
+ } else {
11850
+ console.log(' Last publish: (none)');
11851
+ }
11852
+ }
11853
+
11854
+ // ── Scene Lock ────────────────────────────────────────────────────────────
11855
+
11856
+ function normalizeSceneLockOptions(options = {}) {
11857
+ return {
11858
+ action: options.action ? String(options.action).trim() : undefined,
11859
+ name: options.name ? String(options.name).trim() : undefined,
11860
+ version: options.version ? String(options.version).trim() : undefined,
11861
+ registry: options.registry ? String(options.registry).trim() : '.kiro/registry',
11862
+ json: options.json === true
11863
+ };
11864
+ }
11865
+
11866
+ function validateSceneLockOptions(options) {
11867
+ if (!options.action) return '--action is required';
11868
+ const validActions = ['set', 'rm', 'ls'];
11869
+ if (!validActions.includes(options.action)) return `invalid action "${options.action}"`;
11870
+
11871
+ if (options.action === 'set') {
11872
+ if (!options.name) return '--name is required';
11873
+ if (!options.version) return '--version is required';
11874
+ }
11875
+ if (options.action === 'rm') {
11876
+ if (!options.name) return '--name is required';
11877
+ if (!options.version) return '--version is required';
11878
+ }
11879
+ if (options.action === 'ls') {
11880
+ if (!options.name) return '--name is required';
11881
+ }
11882
+ return null;
11883
+ }
11884
+
11885
+ async function runSceneLockCommand(rawOptions = {}, dependencies = {}) {
11886
+ const projectRoot = dependencies.projectRoot || process.cwd();
11887
+ const fileSystem = dependencies.fileSystem || fs;
11888
+
11889
+ const options = normalizeSceneLockOptions(rawOptions);
11890
+ const validationError = validateSceneLockOptions(options);
11891
+
11892
+ if (validationError) {
11893
+ console.error(chalk.red(`Scene lock failed: ${validationError}`));
11894
+ process.exitCode = 1;
11895
+ return null;
11896
+ }
11897
+
11898
+ try {
11899
+ const registryRoot = path.isAbsolute(options.registry)
11900
+ ? options.registry
11901
+ : path.join(projectRoot, options.registry);
11902
+
11903
+ const index = await loadRegistryIndex(registryRoot, fileSystem);
11904
+ const packages = index.packages || {};
11905
+ let payload;
11906
+
11907
+ if (options.action === 'set') {
11908
+ if (!packages[options.name]) {
11909
+ throw new Error(`package "${options.name}" not found in registry`);
11910
+ }
11911
+ const pkg = packages[options.name];
11912
+ if (!pkg.versions || !pkg.versions[options.version]) {
11913
+ throw new Error(`version "${options.version}" not found for package "${options.name}"`);
11914
+ }
11915
+ const versionEntry = pkg.versions[options.version];
11916
+ if (versionEntry.locked === true) {
11917
+ throw new Error(`version "${options.version}" of package "${options.name}" is already locked`);
11918
+ }
11919
+ versionEntry.locked = true;
11920
+ await saveRegistryIndex(registryRoot, index, fileSystem);
11921
+ payload = {
11922
+ success: true,
11923
+ action: 'set',
11924
+ package: options.name,
11925
+ version: options.version,
11926
+ registry: options.registry
11927
+ };
11928
+ } else if (options.action === 'rm') {
11929
+ if (!packages[options.name]) {
11930
+ throw new Error(`package "${options.name}" not found in registry`);
11931
+ }
11932
+ const pkg = packages[options.name];
11933
+ if (!pkg.versions || !pkg.versions[options.version]) {
11934
+ throw new Error(`version "${options.version}" not found for package "${options.name}"`);
11935
+ }
11936
+ const versionEntry = pkg.versions[options.version];
11937
+ if (!versionEntry.locked) {
11938
+ throw new Error(`version "${options.version}" of package "${options.name}" is not locked`);
11939
+ }
11940
+ delete versionEntry.locked;
11941
+ await saveRegistryIndex(registryRoot, index, fileSystem);
11942
+ payload = {
11943
+ success: true,
11944
+ action: 'rm',
11945
+ package: options.name,
11946
+ version: options.version,
11947
+ registry: options.registry
11948
+ };
11949
+ } else if (options.action === 'ls') {
11950
+ if (!packages[options.name]) {
11951
+ throw new Error(`package "${options.name}" not found in registry`);
11952
+ }
11953
+ const pkg = packages[options.name];
11954
+ const versions = pkg.versions || {};
11955
+ const lockedVersions = Object.keys(versions).filter(v => versions[v].locked === true);
11956
+ payload = {
11957
+ success: true,
11958
+ action: 'ls',
11959
+ package: options.name,
11960
+ lockedVersions,
11961
+ registry: options.registry
11962
+ };
11963
+ }
11964
+
11965
+ printSceneLockSummary(options, payload);
11966
+ return payload;
11967
+ } catch (error) {
11968
+ console.error(chalk.red('Scene lock failed:'), error.message);
11969
+ process.exitCode = 1;
11970
+ return null;
11971
+ }
11972
+ }
11973
+
11974
+ function printSceneLockSummary(options, payload) {
11975
+ if (options.json) {
11976
+ console.log(JSON.stringify(payload, null, 2));
11977
+ return;
11978
+ }
11979
+
11980
+ if (payload.action === 'set') {
11981
+ console.log(chalk.green(`Version "${payload.version}" of package "${payload.package}" is now locked`));
11982
+ } else if (payload.action === 'rm') {
11983
+ console.log(chalk.green(`Version "${payload.version}" of package "${payload.package}" is now unlocked`));
11984
+ } else if (payload.action === 'ls') {
11985
+ if (payload.lockedVersions.length === 0) {
11986
+ console.log(`No locked versions for package "${payload.package}"`);
11987
+ } else {
11988
+ console.log(`Locked versions for package "${payload.package}":`);
11989
+ for (const version of payload.lockedVersions) {
11990
+ console.log(` ${version}`);
11991
+ }
11992
+ }
11993
+ }
11994
+ }
11995
+
11701
11996
  module.exports = {
11702
11997
  RUN_MODES,
11703
11998
  SCAFFOLD_TYPES,
@@ -11882,5 +12177,13 @@ module.exports = {
11882
12177
  normalizeSceneTagOptions,
11883
12178
  validateSceneTagOptions,
11884
12179
  runSceneTagCommand,
11885
- printSceneTagSummary
12180
+ printSceneTagSummary,
12181
+ normalizeSceneStatsOptions,
12182
+ validateSceneStatsOptions,
12183
+ runSceneStatsCommand,
12184
+ printSceneStatsSummary,
12185
+ normalizeSceneLockOptions,
12186
+ validateSceneLockOptions,
12187
+ runSceneLockCommand,
12188
+ printSceneLockSummary
11886
12189
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-spec-engine",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "kiro-spec-engine (kse) - A CLI tool and npm package for spec-driven development with AI coding assistants. NOT the Kiro IDE desktop application.",
5
5
  "main": "index.js",
6
6
  "bin": {