kiro-spec-engine 1.27.0 → 1.29.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,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.29.0] - 2026-02-10
11
+
12
+ ### Added
13
+ - **Scene Registry Query**: List and search scene packages in local registry
14
+ - `kse scene list` list all packages in registry
15
+ - `--registry <dir>` custom registry directory
16
+ - `--json` structured JSON output
17
+ - `kse scene search --query <term>` search packages by keyword
18
+ - Case-insensitive substring matching on name, description, and group
19
+ - `--registry <dir>` custom registry directory
20
+ - `--json` structured JSON output
21
+ - Shared helpers: `buildRegistryPackageList`, `filterRegistryPackages`
22
+ - Follows normalize → validate → run → print pattern
23
+ - Implements Spec 79-00-scene-registry-query
24
+
25
+ ## [1.28.0] - 2026-02-10
26
+
27
+ ### Added
28
+ - **Scene Package Install**: Install published scene packages from local registry
29
+ - `kse scene install --name <packageName>` install scene package from registry
30
+ - `--version <version>` exact version or omit for latest
31
+ - `--out <dir>` custom target directory (default: `./{packageName}`)
32
+ - `--registry <dir>` custom registry directory
33
+ - `--force` overwrite existing installation
34
+ - `--dry-run` preview without writing files
35
+ - `--json` structured JSON output
36
+ - SHA-256 integrity verification before extraction
37
+ - Tarball decompression and file extraction preserving relative paths
38
+ - Install manifest (`scene-install-manifest.json`) with package metadata, timestamp, file list
39
+ - Automatic latest version resolution from registry index
40
+ - Completes publish → install lifecycle for scene packages
41
+ - Implements Spec 78-00-scene-package-install
42
+
10
43
  ## [1.27.0] - 2026-02-10
11
44
 
12
45
  ### Added
@@ -503,6 +503,39 @@ function registerSceneCommands(program) {
503
503
  .action(async (options) => {
504
504
  await runSceneUnpublishCommand(options);
505
505
  });
506
+
507
+ sceneCmd
508
+ .command('install')
509
+ .description('Install scene package from local registry')
510
+ .requiredOption('-n, --name <name>', 'Package name to install')
511
+ .option('-v, --version <version>', 'Package version (default: latest)')
512
+ .option('-o, --out <dir>', 'Target output directory')
513
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
514
+ .option('--force', 'Overwrite existing installation')
515
+ .option('--dry-run', 'Preview install without writing files')
516
+ .option('--json', 'Print install payload as JSON')
517
+ .action(async (options) => {
518
+ await runSceneInstallCommand(options);
519
+ });
520
+
521
+ sceneCmd
522
+ .command('list')
523
+ .description('List all packages in the local scene registry')
524
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
525
+ .option('--json', 'Print result as JSON')
526
+ .action(async (options) => {
527
+ await runSceneListCommand(options);
528
+ });
529
+
530
+ sceneCmd
531
+ .command('search')
532
+ .description('Search packages in the local scene registry')
533
+ .requiredOption('-q, --query <term>', 'Search term (substring match on name, description, group)')
534
+ .option('-r, --registry <path>', 'Registry root directory', '.kiro/registry')
535
+ .option('--json', 'Print result as JSON')
536
+ .action(async (options) => {
537
+ await runSceneSearchCommand(options);
538
+ });
506
539
  }
507
540
 
508
541
  function normalizeSourceOptions(options = {}) {
@@ -9847,6 +9880,358 @@ function validateSceneUnpublishOptions(options) {
9847
9880
  return null;
9848
9881
  }
9849
9882
 
9883
+ function normalizeSceneInstallOptions(options = {}) {
9884
+ return {
9885
+ name: options.name ? String(options.name).trim() : undefined,
9886
+ version: options.version ? String(options.version).trim() : undefined,
9887
+ out: options.out ? String(options.out).trim() : undefined,
9888
+ registry: options.registry ? String(options.registry).trim() : '.kiro/registry',
9889
+ force: options.force === true,
9890
+ dryRun: options.dryRun === true,
9891
+ json: options.json === true
9892
+ };
9893
+ }
9894
+
9895
+ function validateSceneInstallOptions(options) {
9896
+ if (!options.name || typeof options.name !== 'string') {
9897
+ return '--name is required';
9898
+ }
9899
+ if (options.version && options.version !== 'latest' && !semver.valid(options.version)) {
9900
+ return `--version "${options.version}" is not valid semver`;
9901
+ }
9902
+ return null;
9903
+ }
9904
+
9905
+ function buildInstallManifest(packageName, version, registryDir, integrity, files) {
9906
+ return {
9907
+ packageName,
9908
+ version,
9909
+ installedAt: new Date().toISOString(),
9910
+ registryDir,
9911
+ integrity,
9912
+ files
9913
+ };
9914
+ }
9915
+
9916
+ function printSceneInstallSummary(options, payload, projectRoot = process.cwd()) {
9917
+ if (options.json) {
9918
+ console.log(JSON.stringify(payload, null, 2));
9919
+ return;
9920
+ }
9921
+
9922
+ const dryRunLabel = payload.dry_run ? chalk.yellow(' [dry-run]') : '';
9923
+ console.log(chalk.blue('Scene Package Install') + dryRunLabel);
9924
+ console.log(` Package: ${payload.coordinate}`);
9925
+ console.log(` Target: ${chalk.gray(payload.target_dir)}`);
9926
+ console.log(` Files: ${payload.file_count}`);
9927
+ console.log(` Integrity: ${payload.integrity}`);
9928
+ if (payload.overwritten) {
9929
+ console.log(` Overwritten: ${chalk.yellow('yes')}`);
9930
+ }
9931
+ }
9932
+
9933
+ async function runSceneInstallCommand(rawOptions = {}, dependencies = {}) {
9934
+ const projectRoot = dependencies.projectRoot || process.cwd();
9935
+ const fileSystem = dependencies.fileSystem || fs;
9936
+
9937
+ const options = normalizeSceneInstallOptions(rawOptions);
9938
+ const validationError = validateSceneInstallOptions(options);
9939
+
9940
+ if (validationError) {
9941
+ console.error(chalk.red(`Scene package install failed: ${validationError}`));
9942
+ process.exitCode = 1;
9943
+ return null;
9944
+ }
9945
+
9946
+ const readFile = typeof fileSystem.readFile === 'function'
9947
+ ? fileSystem.readFile.bind(fileSystem)
9948
+ : fs.readFile.bind(fs);
9949
+
9950
+ try {
9951
+ // 1. Resolve registry root
9952
+ const registryRoot = path.isAbsolute(options.registry)
9953
+ ? options.registry
9954
+ : path.join(projectRoot, options.registry);
9955
+
9956
+ // 2. Load registry index
9957
+ const index = await loadRegistryIndex(registryRoot, fileSystem);
9958
+
9959
+ // 3. Resolve package in index
9960
+ if (!index.packages || !index.packages[options.name]) {
9961
+ throw new Error(`package "${options.name}" not found in registry`);
9962
+ }
9963
+ const pkg = index.packages[options.name];
9964
+
9965
+ // 4. Resolve version (default to latest)
9966
+ const version = (!options.version || options.version === 'latest')
9967
+ ? pkg.latest
9968
+ : options.version;
9969
+
9970
+ if (!version) {
9971
+ throw new Error(`no latest version found for package "${options.name}"`);
9972
+ }
9973
+
9974
+ if (!pkg.versions || !pkg.versions[version]) {
9975
+ throw new Error(`version "${version}" not found for package "${options.name}"`);
9976
+ }
9977
+ const versionEntry = pkg.versions[version];
9978
+
9979
+ // 5. Read tarball from disk
9980
+ const tarballRelativePath = buildRegistryTarballPath(options.name, version);
9981
+ const tarballAbsolutePath = path.join(registryRoot, tarballRelativePath);
9982
+ const tarballBuffer = await readFile(tarballAbsolutePath);
9983
+
9984
+ // 6. Verify integrity
9985
+ const computedHash = 'sha256-' + crypto.createHash('sha256').update(tarballBuffer).digest('hex');
9986
+ if (computedHash !== versionEntry.integrity) {
9987
+ throw new Error(`integrity verification failed: expected ${versionEntry.integrity}, got ${computedHash}`);
9988
+ }
9989
+
9990
+ // 7. Resolve target directory
9991
+ const targetDir = options.out
9992
+ ? (path.isAbsolute(options.out) ? options.out : path.join(projectRoot, options.out))
9993
+ : path.join(projectRoot, options.name);
9994
+
9995
+ // 8. Decompress and extract tarball
9996
+ const decompressed = zlib.gunzipSync(tarballBuffer);
9997
+ const files = extractTarBuffer(decompressed);
9998
+ const fileNames = files.map(f => f.relativePath);
9999
+
10000
+ // 9. Build coordinate
10001
+ const coordinate = `kse.scene/${options.name}@${version}`;
10002
+
10003
+ // 10. Dry-run: build payload without writing, print, return
10004
+ if (options.dryRun) {
10005
+ const payload = {
10006
+ installed: false,
10007
+ dry_run: true,
10008
+ overwritten: false,
10009
+ coordinate,
10010
+ package: {
10011
+ name: options.name,
10012
+ version
10013
+ },
10014
+ target_dir: formatScenePackagePath(projectRoot, targetDir),
10015
+ file_count: fileNames.length,
10016
+ files: fileNames,
10017
+ integrity: computedHash,
10018
+ registry: {
10019
+ index_path: formatScenePackagePath(projectRoot, path.join(registryRoot, 'registry-index.json'))
10020
+ }
10021
+ };
10022
+
10023
+ printSceneInstallSummary(options, payload, projectRoot);
10024
+ return payload;
10025
+ }
10026
+
10027
+ // 11. Check target dir conflict (unless --force)
10028
+ const ensureDirSync = typeof fileSystem.ensureDirSync === 'function'
10029
+ ? fileSystem.ensureDirSync.bind(fileSystem)
10030
+ : fs.ensureDirSync.bind(fs);
10031
+ const writeFileSync = typeof fileSystem.writeFileSync === 'function'
10032
+ ? fileSystem.writeFileSync.bind(fileSystem)
10033
+ : fs.writeFileSync.bind(fs);
10034
+ const pathExistsSync = typeof fileSystem.pathExistsSync === 'function'
10035
+ ? fileSystem.pathExistsSync.bind(fileSystem)
10036
+ : fs.pathExistsSync.bind(fs);
10037
+
10038
+ const targetExists = pathExistsSync(targetDir);
10039
+ if (targetExists && !options.force) {
10040
+ throw new Error(`target directory already exists: ${formatScenePackagePath(projectRoot, targetDir)} (use --force to overwrite)`);
10041
+ }
10042
+
10043
+ // 12. Write extracted files preserving relative paths
10044
+ ensureDirSync(targetDir);
10045
+ for (const file of files) {
10046
+ const filePath = path.join(targetDir, file.relativePath);
10047
+ ensureDirSync(path.dirname(filePath));
10048
+ writeFileSync(filePath, file.content);
10049
+ }
10050
+
10051
+ // 13. Write install manifest
10052
+ const manifest = buildInstallManifest(options.name, version, options.registry, computedHash, fileNames);
10053
+ const manifestPath = path.join(targetDir, 'scene-install-manifest.json');
10054
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
10055
+
10056
+ // 14. Build payload
10057
+ const payload = {
10058
+ installed: true,
10059
+ dry_run: false,
10060
+ overwritten: targetExists && options.force,
10061
+ coordinate,
10062
+ package: {
10063
+ name: options.name,
10064
+ version
10065
+ },
10066
+ target_dir: formatScenePackagePath(projectRoot, targetDir),
10067
+ file_count: fileNames.length,
10068
+ files: fileNames,
10069
+ integrity: computedHash,
10070
+ registry: {
10071
+ index_path: formatScenePackagePath(projectRoot, path.join(registryRoot, 'registry-index.json'))
10072
+ }
10073
+ };
10074
+
10075
+ printSceneInstallSummary(options, payload, projectRoot);
10076
+ return payload;
10077
+ } catch (error) {
10078
+ console.error(chalk.red('Scene package install failed:'), error.message);
10079
+ process.exitCode = 1;
10080
+ return null;
10081
+ }
10082
+ }
10083
+
10084
+ // --- Scene Registry Query Helpers ---
10085
+
10086
+ function buildRegistryPackageList(registryPackages) {
10087
+ return Object.values(registryPackages || {})
10088
+ .map(pkg => ({
10089
+ name: pkg.name || '',
10090
+ group: pkg.group || '',
10091
+ description: pkg.description || '',
10092
+ latest: pkg.latest || '',
10093
+ version_count: Object.keys(pkg.versions || {}).length
10094
+ }))
10095
+ .sort((a, b) => a.name.localeCompare(b.name));
10096
+ }
10097
+
10098
+ function filterRegistryPackages(packageList, query) {
10099
+ if (!query) return packageList;
10100
+ const lowerQuery = query.toLowerCase();
10101
+ return packageList.filter(pkg =>
10102
+ pkg.name.toLowerCase().includes(lowerQuery) ||
10103
+ pkg.description.toLowerCase().includes(lowerQuery) ||
10104
+ pkg.group.toLowerCase().includes(lowerQuery)
10105
+ );
10106
+ }
10107
+
10108
+ // --- Scene List Command ---
10109
+
10110
+ function normalizeSceneListOptions(options = {}) {
10111
+ return {
10112
+ registry: options.registry ? String(options.registry).trim() : '.kiro/registry',
10113
+ json: options.json === true
10114
+ };
10115
+ }
10116
+
10117
+ function validateSceneListOptions(options) {
10118
+ return null;
10119
+ }
10120
+
10121
+ async function runSceneListCommand(rawOptions = {}, dependencies = {}) {
10122
+ const projectRoot = dependencies.projectRoot || process.cwd();
10123
+ const fileSystem = dependencies.fileSystem || fs;
10124
+
10125
+ const options = normalizeSceneListOptions(rawOptions);
10126
+ const validationError = validateSceneListOptions(options);
10127
+
10128
+ if (validationError) {
10129
+ console.error(chalk.red(`Scene list failed: ${validationError}`));
10130
+ process.exitCode = 1;
10131
+ return null;
10132
+ }
10133
+
10134
+ try {
10135
+ const registryRoot = path.isAbsolute(options.registry)
10136
+ ? options.registry
10137
+ : path.join(projectRoot, options.registry);
10138
+
10139
+ const index = await loadRegistryIndex(registryRoot, fileSystem);
10140
+ const packages = buildRegistryPackageList(index.packages);
10141
+ const payload = { packages, total: packages.length };
10142
+
10143
+ printSceneListSummary(options, payload, projectRoot);
10144
+ return payload;
10145
+ } catch (error) {
10146
+ console.error(chalk.red('Scene list failed:'), error.message);
10147
+ process.exitCode = 1;
10148
+ return null;
10149
+ }
10150
+ }
10151
+
10152
+ function printSceneListSummary(options, payload, projectRoot = process.cwd()) {
10153
+ if (options.json) {
10154
+ console.log(JSON.stringify(payload, null, 2));
10155
+ return;
10156
+ }
10157
+
10158
+ if (payload.total === 0) {
10159
+ console.log('No packages found');
10160
+ return;
10161
+ }
10162
+
10163
+ console.log(chalk.blue(`Scene Registry (${payload.total} package${payload.total !== 1 ? 's' : ''})`));
10164
+ for (const pkg of payload.packages) {
10165
+ console.log(` ${pkg.name.padEnd(20)} ${pkg.latest.padEnd(10)} ${String(pkg.version_count).padEnd(10)} ${pkg.description}`);
10166
+ }
10167
+ }
10168
+
10169
+ function normalizeSceneSearchOptions(options = {}) {
10170
+ return {
10171
+ query: options.query ? String(options.query).trim() : '',
10172
+ registry: options.registry ? String(options.registry).trim() : '.kiro/registry',
10173
+ json: options.json === true
10174
+ };
10175
+ }
10176
+
10177
+ function validateSceneSearchOptions(options) {
10178
+ return null; // Empty query is valid (returns all)
10179
+ }
10180
+
10181
+ async function runSceneSearchCommand(rawOptions = {}, dependencies = {}) {
10182
+ const projectRoot = dependencies.projectRoot || process.cwd();
10183
+ const fileSystem = dependencies.fileSystem || fs;
10184
+
10185
+ const options = normalizeSceneSearchOptions(rawOptions);
10186
+ const validationError = validateSceneSearchOptions(options);
10187
+
10188
+ if (validationError) {
10189
+ console.error(chalk.red(`Scene search failed: ${validationError}`));
10190
+ process.exitCode = 1;
10191
+ return null;
10192
+ }
10193
+
10194
+ try {
10195
+ const registryRoot = path.isAbsolute(options.registry)
10196
+ ? options.registry
10197
+ : path.join(projectRoot, options.registry);
10198
+
10199
+ const index = await loadRegistryIndex(registryRoot, fileSystem);
10200
+ const allPackages = buildRegistryPackageList(index.packages);
10201
+ const packages = filterRegistryPackages(allPackages, options.query);
10202
+ const payload = { query: options.query, packages, total: packages.length };
10203
+
10204
+ printSceneSearchSummary(options, payload, projectRoot);
10205
+ return payload;
10206
+ } catch (error) {
10207
+ console.error(chalk.red('Scene search failed:'), error.message);
10208
+ process.exitCode = 1;
10209
+ return null;
10210
+ }
10211
+ }
10212
+
10213
+ function printSceneSearchSummary(options, payload, projectRoot = process.cwd()) {
10214
+ if (options.json) {
10215
+ console.log(JSON.stringify(payload, null, 2));
10216
+ return;
10217
+ }
10218
+
10219
+ if (payload.total === 0) {
10220
+ if (payload.query) {
10221
+ console.log(`No packages matching '${payload.query}'`);
10222
+ } else {
10223
+ console.log('No packages found');
10224
+ }
10225
+ return;
10226
+ }
10227
+
10228
+ const matchLabel = payload.total === 1 ? 'match' : 'matches';
10229
+ console.log(chalk.blue(`Scene Search: "${payload.query}" (${payload.total} ${matchLabel})`));
10230
+ for (const pkg of payload.packages) {
10231
+ console.log(` ${pkg.name.padEnd(20)} ${pkg.latest.padEnd(10)} ${String(pkg.version_count).padEnd(10)} ${pkg.description}`);
10232
+ }
10233
+ }
10234
+
9850
10235
  function printSceneUnpublishSummary(options, payload, projectRoot = process.cwd()) {
9851
10236
  if (options.json) {
9852
10237
  console.log(JSON.stringify(payload, null, 2));
@@ -10070,5 +10455,20 @@ module.exports = {
10070
10455
  normalizeSceneUnpublishOptions,
10071
10456
  validateSceneUnpublishOptions,
10072
10457
  printSceneUnpublishSummary,
10073
- runSceneUnpublishCommand
10458
+ runSceneUnpublishCommand,
10459
+ buildInstallManifest,
10460
+ normalizeSceneInstallOptions,
10461
+ printSceneInstallSummary,
10462
+ runSceneInstallCommand,
10463
+ validateSceneInstallOptions,
10464
+ buildRegistryPackageList,
10465
+ filterRegistryPackages,
10466
+ normalizeSceneListOptions,
10467
+ validateSceneListOptions,
10468
+ runSceneListCommand,
10469
+ printSceneListSummary,
10470
+ normalizeSceneSearchOptions,
10471
+ validateSceneSearchOptions,
10472
+ runSceneSearchCommand,
10473
+ printSceneSearchSummary
10074
10474
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-spec-engine",
3
- "version": "1.27.0",
3
+ "version": "1.29.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": {