kiro-spec-engine 1.36.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,33 @@ 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
+
25
+ ## [1.37.0] - 2026-02-10
26
+
27
+ ### Added
28
+ - **Scene Distribution Tags**: Manage distribution tags on scene packages in local registry
29
+ - `kse scene tag add --name <pkg> --tag <tag> --version <ver>` add a distribution tag
30
+ - `kse scene tag rm --name <pkg> --tag <tag>` remove a distribution tag
31
+ - `kse scene tag ls --name <pkg>` list all tags and latest version
32
+ - `--registry <dir>` custom registry directory
33
+ - `--json` structured JSON output
34
+ - Tags stored as `tags` object on package entry, separate from `latest` field
35
+ - "latest" tag is protected — managed automatically by publish
36
+
10
37
  ## [1.36.0] - 2026-02-10
11
38
 
12
39
  ### Added
@@ -643,6 +643,91 @@ function registerSceneCommands(program) {
643
643
  .action(async (options) => {
644
644
  await runSceneOwnerCommand({ ...options, action: 'transfer' });
645
645
  });
646
+
647
+ // ── scene tag ──
648
+ const tagCmd = sceneCmd
649
+ .command('tag')
650
+ .description('Manage distribution tags on scene packages');
651
+
652
+ tagCmd
653
+ .command('add')
654
+ .description('Add a distribution tag to a package version')
655
+ .requiredOption('-n, --name <name>', 'Package name')
656
+ .requiredOption('-t, --tag <tag>', 'Tag name')
657
+ .requiredOption('-v, --version <version>', 'Version to tag')
658
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
659
+ .option('--json', 'Print result as JSON')
660
+ .action(async (options) => {
661
+ await runSceneTagCommand({ ...options, action: 'add' });
662
+ });
663
+
664
+ tagCmd
665
+ .command('rm')
666
+ .description('Remove a distribution tag from a package')
667
+ .requiredOption('-n, --name <name>', 'Package name')
668
+ .requiredOption('-t, --tag <tag>', 'Tag name to remove')
669
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
670
+ .option('--json', 'Print result as JSON')
671
+ .action(async (options) => {
672
+ await runSceneTagCommand({ ...options, action: 'rm' });
673
+ });
674
+
675
+ tagCmd
676
+ .command('ls')
677
+ .description('List all distribution tags for a package')
678
+ .requiredOption('-n, --name <name>', 'Package name')
679
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
680
+ .option('--json', 'Print result as JSON')
681
+ .action(async (options) => {
682
+ await runSceneTagCommand({ ...options, action: 'ls' });
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
+ });
646
731
  }
647
732
 
648
733
  function normalizeSourceOptions(options = {}) {
@@ -11516,6 +11601,398 @@ function printSceneOwnerSummary(options, payload) {
11516
11601
  }
11517
11602
  }
11518
11603
 
11604
+ // ── Scene Tag ──────────────────────────────────────────────────────────────
11605
+
11606
+ function normalizeSceneTagOptions(options = {}) {
11607
+ return {
11608
+ action: options.action ? String(options.action).trim() : undefined,
11609
+ name: options.name ? String(options.name).trim() : undefined,
11610
+ tag: options.tag ? String(options.tag).trim() : undefined,
11611
+ version: options.version ? String(options.version).trim() : undefined,
11612
+ registry: options.registry ? String(options.registry).trim() : '.kiro/registry',
11613
+ json: options.json === true
11614
+ };
11615
+ }
11616
+
11617
+ function validateSceneTagOptions(options) {
11618
+ if (!options.action) return '--action is required';
11619
+ const validActions = ['add', 'rm', 'ls'];
11620
+ if (!validActions.includes(options.action)) return `invalid action "${options.action}"`;
11621
+
11622
+ if (options.action === 'add') {
11623
+ if (!options.name) return '--name is required';
11624
+ if (!options.tag) return '--tag is required';
11625
+ if (!options.version) return '--version is required';
11626
+ if (options.tag === 'latest') return '"latest" tag is managed automatically by publish';
11627
+ }
11628
+ if (options.action === 'rm') {
11629
+ if (!options.name) return '--name is required';
11630
+ if (!options.tag) return '--tag is required';
11631
+ if (options.tag === 'latest') return '"latest" tag is managed automatically by publish';
11632
+ }
11633
+ if (options.action === 'ls') {
11634
+ if (!options.name) return '--name is required';
11635
+ }
11636
+ return null;
11637
+ }
11638
+
11639
+ async function runSceneTagCommand(rawOptions = {}, dependencies = {}) {
11640
+ const projectRoot = dependencies.projectRoot || process.cwd();
11641
+ const fileSystem = dependencies.fileSystem || fs;
11642
+
11643
+ const options = normalizeSceneTagOptions(rawOptions);
11644
+ const validationError = validateSceneTagOptions(options);
11645
+
11646
+ if (validationError) {
11647
+ console.error(chalk.red(`Scene tag failed: ${validationError}`));
11648
+ process.exitCode = 1;
11649
+ return null;
11650
+ }
11651
+
11652
+ try {
11653
+ const registryRoot = path.isAbsolute(options.registry)
11654
+ ? options.registry
11655
+ : path.join(projectRoot, options.registry);
11656
+
11657
+ const index = await loadRegistryIndex(registryRoot, fileSystem);
11658
+ const packages = index.packages || {};
11659
+ let payload;
11660
+
11661
+ if (options.action === 'add') {
11662
+ if (!packages[options.name]) {
11663
+ throw new Error(`package "${options.name}" not found in registry`);
11664
+ }
11665
+ const pkg = packages[options.name];
11666
+ if (!pkg.versions || !pkg.versions[options.version]) {
11667
+ throw new Error(`version "${options.version}" not found for package "${options.name}"`);
11668
+ }
11669
+ if (!pkg.tags) pkg.tags = {};
11670
+ pkg.tags[options.tag] = options.version;
11671
+ await saveRegistryIndex(registryRoot, index, fileSystem);
11672
+ payload = {
11673
+ success: true,
11674
+ action: 'add',
11675
+ package: options.name,
11676
+ tag: options.tag,
11677
+ version: options.version,
11678
+ registry: options.registry
11679
+ };
11680
+ } else if (options.action === 'rm') {
11681
+ if (!packages[options.name]) {
11682
+ throw new Error(`package "${options.name}" not found in registry`);
11683
+ }
11684
+ const pkg = packages[options.name];
11685
+ if (!pkg.tags || !pkg.tags[options.tag]) {
11686
+ throw new Error(`tag "${options.tag}" not found for package "${options.name}"`);
11687
+ }
11688
+ delete pkg.tags[options.tag];
11689
+ await saveRegistryIndex(registryRoot, index, fileSystem);
11690
+ payload = {
11691
+ success: true,
11692
+ action: 'rm',
11693
+ package: options.name,
11694
+ tag: options.tag,
11695
+ registry: options.registry
11696
+ };
11697
+ } else if (options.action === 'ls') {
11698
+ if (!packages[options.name]) {
11699
+ throw new Error(`package "${options.name}" not found in registry`);
11700
+ }
11701
+ const pkg = packages[options.name];
11702
+ const tags = { ...(pkg.tags || {}) };
11703
+ payload = {
11704
+ success: true,
11705
+ action: 'ls',
11706
+ package: options.name,
11707
+ latest: pkg.latest || null,
11708
+ tags,
11709
+ registry: options.registry
11710
+ };
11711
+ }
11712
+
11713
+ printSceneTagSummary(options, payload);
11714
+ return payload;
11715
+ } catch (error) {
11716
+ console.error(chalk.red('Scene tag failed:'), error.message);
11717
+ process.exitCode = 1;
11718
+ return null;
11719
+ }
11720
+ }
11721
+
11722
+ function printSceneTagSummary(options, payload) {
11723
+ if (options.json) {
11724
+ console.log(JSON.stringify(payload, null, 2));
11725
+ return;
11726
+ }
11727
+
11728
+ if (payload.action === 'add') {
11729
+ console.log(chalk.green(`Tag "${payload.tag}" set to version "${payload.version}" for package "${payload.package}"`));
11730
+ } else if (payload.action === 'rm') {
11731
+ console.log(chalk.green(`Tag "${payload.tag}" removed from package "${payload.package}"`));
11732
+ } else if (payload.action === 'ls') {
11733
+ const tagEntries = Object.entries(payload.tags);
11734
+ if (tagEntries.length === 0 && !payload.latest) {
11735
+ console.log(`No tags set for package "${payload.package}"`);
11736
+ } else {
11737
+ console.log(`Tags for package "${payload.package}":`);
11738
+ if (payload.latest) {
11739
+ console.log(` latest: ${payload.latest}`);
11740
+ }
11741
+ for (const [tag, version] of tagEntries) {
11742
+ console.log(` ${tag}: ${version}`);
11743
+ }
11744
+ }
11745
+ }
11746
+ }
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
+
11519
11996
  module.exports = {
11520
11997
  RUN_MODES,
11521
11998
  SCAFFOLD_TYPES,
@@ -11696,5 +12173,17 @@ module.exports = {
11696
12173
  normalizeSceneOwnerOptions,
11697
12174
  validateSceneOwnerOptions,
11698
12175
  runSceneOwnerCommand,
11699
- printSceneOwnerSummary
12176
+ printSceneOwnerSummary,
12177
+ normalizeSceneTagOptions,
12178
+ validateSceneTagOptions,
12179
+ runSceneTagCommand,
12180
+ printSceneTagSummary,
12181
+ normalizeSceneStatsOptions,
12182
+ validateSceneStatsOptions,
12183
+ runSceneStatsCommand,
12184
+ printSceneStatsSummary,
12185
+ normalizeSceneLockOptions,
12186
+ validateSceneLockOptions,
12187
+ runSceneLockCommand,
12188
+ printSceneLockSummary
11700
12189
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-spec-engine",
3
- "version": "1.36.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": {