norn-cli 1.10.2 → 1.10.4

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/AGENTS.md CHANGED
@@ -17,10 +17,10 @@ Before considering any feature complete, verify it works in the CLI. The CLI sha
17
17
  **ALWAYS use the local compiled CLI:**
18
18
  ```bash
19
19
  # Correct - runs your local development version
20
- node ./dist/cli.js run tests/file.norn --env prelive
20
+ node ./dist/cli.js tests/file.norn --env prelive
21
21
 
22
22
  # WRONG - runs the published npm package, ignores your changes
23
- npx norn run tests/file.norn
23
+ npx norn tests/file.norn
24
24
  ```
25
25
 
26
26
  Before testing CLI changes:
@@ -33,6 +33,7 @@ When implementing features, check the `.github/skills/` directory for relevant s
33
33
 
34
34
  - **If a skill is incorrect or outdated:** Update it with the correct information
35
35
  - **If a skill is missing:** Create a new one following the Agent Skills format
36
+ - After creating or editing skills, run `npm run validate:skills`
36
37
 
37
38
  Skills should capture lessons learned and patterns discovered during implementation to help future development.
38
39
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to the "Norn" extension will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.10.4] - 2026-03-21
8
+
9
+ ### Improved
10
+ - **Request Failure Response Panel**:
11
+ - Refreshed request-failure views with a clearer error summary, request context bar, structured issue cards, and common-cause guidance.
12
+
13
+ ### Changed
14
+ - **Regression + Contributor Tooling**:
15
+ - Standardized regression test docs and scripts around `node ./dist/cli.js`, added `npm run validate:skills`, and refreshed bundled skill/demo assets.
16
+
17
+ ### Fixed
18
+ - **SQL Server Live Regression Fixture**:
19
+ - Made the SQL Server Docker regression setup safe to rerun during repeated local release verification.
20
+
7
21
  ## [1.10.2] - 2026-03-15
8
22
 
9
23
  ### Fixed
@@ -677,8 +691,8 @@ All notable changes to the "Norn" extension will be documented in this file.
677
691
  - **Tag Diagnostics**: Warning when tags are placed incorrectly (not before `sequence`)
678
692
 
679
693
  ### Changed
680
- - **Regression Test Runner**: `run-all.sh` now supports `--tag` and `--tags` options
681
- - Example: `./run-all.sh --tag smoke` runs only smoke-tagged sequences
694
+ - **Regression Test Filtering**: Regression CLI runs support `--tag` and `--tags` options
695
+ - Example: `node ./dist/cli.js tests/Regression/ --env prelive --tag smoke` runs only smoke-tagged sequences
682
696
 
683
697
  ## [1.0.25] - 2026-01-28
684
698
 
@@ -0,0 +1,31 @@
1
+
2
+ GET https://httpbin.org/uuid
3
+
4
+ GET https://httpbin.org/anything/catalog?region=uk&segment=enterprise
5
+
6
+
7
+
8
+
9
+
10
+ POST https://httpbin.org/post
11
+ Content-Type: application/json
12
+ Accept: application/json
13
+ X-Demo-Stage: exploratory-json
14
+ {
15
+ "customerId": "C-1001",
16
+ "workspace": "Northwind",
17
+ "plan": "Enterprise"
18
+ }
19
+
20
+
21
+ POST https://httpbin.org/post
22
+ Content-Type: application/json
23
+ Accept: application/json
24
+ X-Demo-Stage: exploratory-quote
25
+ X-Demo-Audience: exploratory-testers
26
+ {
27
+ "customerId": "C-1001",
28
+ "step": "quote-preview",
29
+ "seats": 250,
30
+ "currency": "GBP"
31
+ }
@@ -0,0 +1,54 @@
1
+
2
+
3
+ @demo
4
+ test sequence SimpleRequestChain
5
+ print "Simple requests" | "Bare URLs become a repeatable flow with assertions."
6
+
7
+ GET https://httpbin.org/uuid
8
+ assert $1.status == 200
9
+ var traceId = $1.body.uuid
10
+
11
+ GET https://httpbin.org/anything/catalog?region=uk&segment=enterprise&trace={{traceId}}
12
+ assert $2.status == 200
13
+ assert $2.body.args.region == "uk"
14
+ assert $2.body.args.segment == "enterprise"
15
+ assert $2.body.url contains "{{traceId}}"
16
+
17
+ print "Trace id" | "{{traceId}}"
18
+ end sequence
19
+
20
+ @demo
21
+ test sequence RichRequestChain
22
+ print "Headers and payloads" | "The same idea works when the requests carry real data."
23
+
24
+ var draft = POST https://httpbin.org/post
25
+ Content-Type: application/json
26
+ Accept: application/json
27
+ X-Demo-Stage: sequence-draft
28
+ {
29
+ "customerId": "C-1001",
30
+ "workspace": "Northwind",
31
+ "plan": "Enterprise"
32
+ }
33
+
34
+ assert draft.status == 200
35
+ assert draft.body.json.customerId == "C-1001"
36
+ var customerId = draft.body.json.customerId
37
+
38
+ var quote = POST https://httpbin.org/post
39
+ Content-Type: application/json
40
+ Accept: application/json
41
+ X-Demo-Stage: sequence-quote
42
+ {
43
+ "customerId": "{{customerId}}",
44
+ "step": "quote-preview",
45
+ "seats": 250,
46
+ "currency": "GBP"
47
+ }
48
+
49
+ assert quote.status == 200
50
+ assert quote.body.json.customerId == "C-1001"
51
+ assert quote.body.json.step == "quote-preview"
52
+
53
+ print "Chained value" | "customerId={{customerId}}"
54
+ end sequence
@@ -0,0 +1,42 @@
1
+ import "./demo-api.nornapi"
2
+
3
+ @demo
4
+ test sequence CleanerExplorationFlow
5
+ print "Cleaner requests" | "Shared setup moved into .nornapi and .nornenv."
6
+
7
+ var trace = GET GetTraceId
8
+ assert trace.status == 200
9
+
10
+ var catalog = GET BrowseCustomer({{demoCustomerId}}) DemoHeaders
11
+ assert catalog.status == 200
12
+ assert catalog.body.args.region == "uk"
13
+ assert catalog.body.url contains "/customers/C-1001"
14
+
15
+ print "Why this helps" | "The request lines stay focused on intent, not plumbing."
16
+ end sequence
17
+
18
+ @demo
19
+ test sequence CleanerPayloadFlow
20
+ var draft = POST SaveDraft Json DemoHeaders
21
+ {
22
+ "customerId": "{{demoCustomerId}}",
23
+ "workspace": "{{workspaceName}}",
24
+ "owner": "{{ownerName}}"
25
+ }
26
+
27
+ assert draft.status == 200
28
+ assert draft.body.json.workspace == "Northwind-Workspace"
29
+
30
+ var quote = POST QuotePreview Json DemoHeaders
31
+ {
32
+ "customerId": "{{demoCustomerId}}",
33
+ "traceId": "{{tracePrefix}}-001",
34
+ "step": "quote-preview",
35
+ "seats": 250
36
+ }
37
+
38
+ assert quote.status == 200
39
+ assert quote.body.json.step == "quote-preview"
40
+
41
+ print "Cleaner file" | "Base URL, reusable headers, and shared names are all sidecars now."
42
+ end sequence
@@ -0,0 +1,27 @@
1
+ import "./demo-api.nornapi"
2
+ import "./db//testDb.nornsql"
3
+
4
+ @demo
5
+ test sequence ApiPlusSqlDemo
6
+ print "API plus SQL" | "Show the service call and the data-side check in one workflow."
7
+
8
+ var quote = POST QuotePreview Json DemoHeaders
9
+ {
10
+ "customerId": "{{demoCustomerId}}",
11
+ "step": "quote-preview",
12
+ "seats": 250,
13
+ "currency": "GBP"
14
+ }
15
+
16
+ assert quote.status == 200
17
+ assert quote.body.json.customerId == "C-1001"
18
+
19
+ var buyers = run sql ListBuyerAccounts("Enterprise")
20
+ assert buyers[0].Segment == "Enterprise"
21
+ assert buyers[0].Company == "Northwind Retail"
22
+
23
+ var audit = run sql RecordDemoRun("northwind@example.com", "buyer-showcase")
24
+ assert audit.affectedRows == 1
25
+
26
+ print "SQL sidecar" | "HTTP stays in .norn, SQL stays in .nornsql, config stays in sidecars."
27
+ end sequence
@@ -0,0 +1,12 @@
1
+ connection buyerDemoDb
2
+
3
+ query ListBuyerAccounts(segment)
4
+ select Id, Company, Email, Segment
5
+ from Buyers
6
+ where Segment = :segment
7
+ end query
8
+
9
+ command RecordDemoRun(email, scenario)
10
+ insert into DemoAudit (Email, Scenario)
11
+ values (:email, :scenario)
12
+ end command
@@ -0,0 +1,17 @@
1
+ headers Json
2
+ Content-Type: application/json
3
+ Accept: application/json
4
+ end headers
5
+
6
+ headers DemoHeaders
7
+ X-Demo-Stage: {{demoStage}}
8
+ X-Demo-Audience: {{audience}}
9
+ X-Demo-Owner: {{ownerName}}
10
+ end headers
11
+
12
+ endpoints
13
+ GetTraceId: GET {{baseUrl}}/uuid
14
+ BrowseCustomer: GET {{baseUrl}}/anything/customers/{customerId}?region={{region}}
15
+ SaveDraft: POST {{baseUrl}}/post
16
+ QuotePreview: POST {{baseUrl}}/post
17
+ end endpoints
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "adapters": {
4
+ "buyer-demo-sql-adapter": {
5
+ "command": ["node", "./scripts/fake-sql-adapter.js"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": 1,
3
+ "connections": {
4
+ "buyerDemoDb": {
5
+ "adapter": "buyer-demo-sql-adapter",
6
+ "profile": "buyerDemoDb"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,70 @@
1
+ const chunks = [];
2
+
3
+ process.stdin.on('data', chunk => {
4
+ chunks.push(chunk);
5
+ });
6
+
7
+ process.stdin.on('end', () => {
8
+ try {
9
+ const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
10
+ const values = request.connection && request.connection.values ? request.connection.values : {};
11
+
12
+ if (!values.server || !values.database || !values.user || !values.password) {
13
+ process.stdout.write(JSON.stringify({
14
+ success: false,
15
+ error: 'Missing expected buyerDemoDb connection values'
16
+ }));
17
+ process.exit(0);
18
+ return;
19
+ }
20
+
21
+ switch (request.operation.name) {
22
+ case 'ListBuyerAccounts': {
23
+ const segment = request.params.segment;
24
+ process.stdout.write(JSON.stringify({
25
+ success: true,
26
+ result: {
27
+ kind: 'rows',
28
+ rowCount: 2,
29
+ rows: [
30
+ {
31
+ Id: 1,
32
+ Company: 'Northwind Retail',
33
+ Email: 'northwind@example.com',
34
+ Segment: segment
35
+ },
36
+ {
37
+ Id: 2,
38
+ Company: 'Contoso Services',
39
+ Email: 'contoso@example.com',
40
+ Segment: segment
41
+ }
42
+ ]
43
+ }
44
+ }));
45
+ return;
46
+ }
47
+
48
+ case 'RecordDemoRun':
49
+ process.stdout.write(JSON.stringify({
50
+ success: true,
51
+ result: {
52
+ kind: 'exec',
53
+ affectedRows: 1
54
+ }
55
+ }));
56
+ return;
57
+
58
+ default:
59
+ process.stdout.write(JSON.stringify({
60
+ success: false,
61
+ error: `Unknown buyer showcase SQL operation: ${request.operation.name}`
62
+ }));
63
+ }
64
+ } catch (error) {
65
+ process.stdout.write(JSON.stringify({
66
+ success: false,
67
+ error: error instanceof Error ? error.message : String(error)
68
+ }));
69
+ }
70
+ });
package/dist/cli.js CHANGED
@@ -109460,6 +109460,9 @@ function getConnectionValue(values, ...keys) {
109460
109460
  function getConnectionString(values) {
109461
109461
  return getConnectionValue(values, "connectionString");
109462
109462
  }
109463
+ function usesWindowsIntegratedSqlServerAuth(connectionString) {
109464
+ return /(?:^|;)\s*Integrated\s+Security\s*=\s*(?:true|yes|sspi)\s*(?:;|$)/i.test(connectionString) || /(?:^|;)\s*Trusted_Connection\s*=\s*(?:true|yes|sspi)\s*(?:;|$)/i.test(connectionString);
109465
+ }
109463
109466
  function buildMissingConnectionError(adapterLabel, profile) {
109464
109467
  return `Missing required ${adapterLabel} connection string. Expected 'connectionString ${profile} = ...' in .nornenv.`;
109465
109468
  }
@@ -109517,7 +109520,7 @@ if ([string]::IsNullOrWhiteSpace($raw)) {
109517
109520
  throw 'Missing SQL adapter payload.'
109518
109521
  }
109519
109522
 
109520
- $payload = $raw | ConvertFrom-Json -Depth 100
109523
+ $payload = $raw | ConvertFrom-Json
109521
109524
  $connectionString = [string]$payload.connection.values.connectionString
109522
109525
  if ([string]::IsNullOrWhiteSpace($connectionString)) {
109523
109526
  throw 'Missing SQL Server Integrated Auth connection string.'
@@ -109548,9 +109551,13 @@ $resolveHandler = [System.ResolveEventHandler]{
109548
109551
  $sqlText = [string]$payload.operation.sql
109549
109552
  $mode = [string]$payload.mode
109550
109553
  $paramsObject = $payload.params
109554
+ $connectionStringHasUserId = $connectionString -match '(?i)(?:^|;)s*(?:User ID|UID)s*='
109555
+ $connectionStringHasPassword = $connectionString -match '(?i)(?:^|;)s*(?:Password|PWD)s*='
109551
109556
 
109552
109557
  $connection = $null
109558
+ $connectionBuilder = $null
109553
109559
  try {
109560
+ $connectionBuilder = New-Object Microsoft.Data.SqlClient.SqlConnectionStringBuilder $connectionString
109554
109561
  $connection = New-Object Microsoft.Data.SqlClient.SqlConnection $connectionString
109555
109562
  $command = $connection.CreateCommand()
109556
109563
  $command.CommandText = $sqlText
@@ -109622,18 +109629,97 @@ try {
109622
109629
  $response | ConvertTo-Json -Depth 100 -Compress
109623
109630
  }
109624
109631
  catch {
109625
- $details = @()
109632
+ $details = New-Object System.Collections.Generic.List[string]
109626
109633
  if ($_.Exception -and $_.Exception.GetType()) {
109627
- $details += "type: $($_.Exception.GetType().FullName)"
109634
+ $details.Add("type: $($_.Exception.GetType().FullName)")
109628
109635
  }
109629
109636
  if ($_.InvocationInfo -and $_.InvocationInfo.ScriptLineNumber) {
109630
- $details += "line: $($_.InvocationInfo.ScriptLineNumber)"
109637
+ $details.Add("line: $($_.InvocationInfo.ScriptLineNumber)")
109631
109638
  }
109632
109639
  if ($_.FullyQualifiedErrorId) {
109633
- $details += "id: $($_.FullyQualifiedErrorId)"
109640
+ $details.Add("id: $($_.FullyQualifiedErrorId)")
109641
+ }
109642
+
109643
+ try {
109644
+ $details.Add("adapter: sqlserver-windows")
109645
+ $details.Add("powershell: $($PSVersionTable.PSVersion)")
109646
+ $details.Add("sqlClientVersion: $(([Microsoft.Data.SqlClient.SqlConnection]).Assembly.GetName().Version)")
109647
+ $details.Add("windowsIdentity: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)")
109648
+ } catch {
109649
+ }
109650
+
109651
+ if ($null -ne $connectionBuilder) {
109652
+ if (-not [string]::IsNullOrWhiteSpace([string]$connectionBuilder.DataSource)) {
109653
+ $details.Add("dataSource: $($connectionBuilder.DataSource)")
109654
+ }
109655
+ if (-not [string]::IsNullOrWhiteSpace([string]$connectionBuilder.InitialCatalog)) {
109656
+ $details.Add("database: $($connectionBuilder.InitialCatalog)")
109657
+ }
109658
+ $details.Add("integratedSecurity: $($connectionBuilder.IntegratedSecurity)")
109659
+ $details.Add("encrypt: $($connectionBuilder.Encrypt)")
109660
+ $details.Add("trustServerCertificate: $($connectionBuilder.TrustServerCertificate)")
109661
+ $details.Add("applicationIntent: $($connectionBuilder.ApplicationIntent)")
109662
+ $details.Add("multiSubnetFailover: $($connectionBuilder.MultiSubnetFailover)")
109663
+
109664
+ try {
109665
+ $hostNameInCertificate = [string]$connectionBuilder['HostNameInCertificate']
109666
+ if (-not [string]::IsNullOrWhiteSpace($hostNameInCertificate)) {
109667
+ $details.Add("hostNameInCertificate: $hostNameInCertificate")
109668
+ }
109669
+ } catch {
109670
+ }
109671
+ }
109672
+
109673
+ $details.Add("userIdPresent: $connectionStringHasUserId")
109674
+ $details.Add("passwordPresent: $connectionStringHasPassword")
109675
+
109676
+ $sqlException = $null
109677
+ if ($_.Exception -is [Microsoft.Data.SqlClient.SqlException]) {
109678
+ $sqlException = [Microsoft.Data.SqlClient.SqlException]$_.Exception
109679
+ } elseif ($_.Exception -and $_.Exception.InnerException -is [Microsoft.Data.SqlClient.SqlException]) {
109680
+ $sqlException = [Microsoft.Data.SqlClient.SqlException]$_.Exception.InnerException
109681
+ }
109682
+
109683
+ if ($null -ne $sqlException) {
109684
+ $details.Add("sqlNumber: $($sqlException.Number)")
109685
+ $details.Add("sqlState: $($sqlException.State)")
109686
+ $details.Add("sqlClass: $($sqlException.Class)")
109687
+ if (-not [string]::IsNullOrWhiteSpace([string]$sqlException.Server)) {
109688
+ $details.Add("sqlServer: $($sqlException.Server)")
109689
+ }
109690
+ if (-not [string]::IsNullOrWhiteSpace([string]$sqlException.Procedure)) {
109691
+ $details.Add("sqlProcedure: $($sqlException.Procedure)")
109692
+ }
109693
+ if ($sqlException.LineNumber -gt 0) {
109694
+ $details.Add("sqlLineNumber: $($sqlException.LineNumber)")
109695
+ }
109696
+ if ($sqlException.ClientConnectionId -ne [guid]::Empty) {
109697
+ $details.Add("clientConnectionId: $($sqlException.ClientConnectionId)")
109698
+ }
109699
+ if ($sqlException.Errors -and $sqlException.Errors.Count -gt 0) {
109700
+ $messages = @()
109701
+ foreach ($sqlError in $sqlException.Errors) {
109702
+ if ($sqlError -and -not [string]::IsNullOrWhiteSpace([string]$sqlError.Message)) {
109703
+ $messages += [string]$sqlError.Message
109704
+ }
109705
+ }
109706
+ if ($messages.Count -gt 0) {
109707
+ $details.Add("sqlErrors: $($messages -join ' | ')")
109708
+ }
109709
+ }
109710
+ }
109711
+
109712
+ if ($_.Exception -and $_.Exception.InnerException) {
109713
+ $details.Add("innerType: $($_.Exception.InnerException.GetType().FullName)")
109714
+ if (-not [string]::IsNullOrWhiteSpace([string]$_.Exception.InnerException.Message)) {
109715
+ $details.Add("innerMessage: $($_.Exception.InnerException.Message)")
109716
+ }
109717
+ }
109718
+
109719
+ [Console]::Error.WriteLine("$($_.Exception.Message)")
109720
+ foreach ($detail in $details) {
109721
+ [Console]::Error.WriteLine(" $detail")
109634
109722
  }
109635
- $suffix = if ($details.Count -gt 0) { " ($($details -join ', '))" } else { '' }
109636
- [Console]::Error.WriteLine("$($_.Exception.Message)$suffix")
109637
109723
  exit 1
109638
109724
  }
109639
109725
  finally {
@@ -109794,6 +109880,9 @@ async function runBuiltInSqlServerAdapter(request) {
109794
109880
  if (!connectionString) {
109795
109881
  fail(buildMissingConnectionError("SQL Server", request.connection.profile));
109796
109882
  }
109883
+ if (usesWindowsIntegratedSqlServerAuth(connectionString)) {
109884
+ fail("This SQL Server connection string uses Windows/Integrated authentication. Use adapter 'sqlserver-windows' in norn.sql.json.");
109885
+ }
109797
109886
  const pool = new mssql.ConnectionPool(connectionString);
109798
109887
  try {
109799
109888
  await pool.connect();
@@ -109832,7 +109921,7 @@ async function runBuiltInSqlServerIntegratedAdapter(request) {
109832
109921
  const values = request.connection.values;
109833
109922
  const connectionString = getConnectionString(values);
109834
109923
  if (!connectionString) {
109835
- fail(buildMissingConnectionError("SQL Server Integrated Auth", request.connection.profile));
109924
+ fail(buildMissingConnectionError("SQL Server (Windows)", request.connection.profile));
109836
109925
  }
109837
109926
  const compiled = compileSqlServerSql(request.operation.sql, request.params || {});
109838
109927
  const compiledRequest = {
@@ -109846,7 +109935,7 @@ async function runBuiltInSqlServerIntegratedAdapter(request) {
109846
109935
  try {
109847
109936
  return await runSqlServerIntegratedViaPowerShell(compiledRequest);
109848
109937
  } catch (error) {
109849
- throw formatBuiltInDriverError("SQL Server Integrated Auth", error);
109938
+ throw formatBuiltInDriverError("SQL Server (Windows)", error);
109850
109939
  }
109851
109940
  }
109852
109941
  function isBuiltInSqlAdapter(adapterId) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "norn-cli",
3
3
  "displayName": "Norn - REST Client",
4
4
  "description": "A powerful REST client for making HTTP requests with sequences, variables, scripts, and cookie support",
5
- "version": "1.10.2",
5
+ "version": "1.10.4",
6
6
  "publisher": "Norn-PeterKrustanov",
7
7
  "author": {
8
8
  "name": "Peter Krastanov"
@@ -386,8 +386,9 @@
386
386
  "pretest": "npm run compile-tests && npm run compile && npm run lint",
387
387
  "check-types": "tsc --noEmit",
388
388
  "lint": "eslint src",
389
+ "validate:skills": "node ./scripts/validate-skills.mjs",
389
390
  "test": "vscode-test",
390
- "test:regression": "./tests/Regression/run-all.sh",
391
+ "test:regression": "node ./dist/cli.js ./tests/Regression/ -e prelive",
391
392
  "publish:npm": "node -e \"const p=require('./package.json');p.name='norn-cli';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npm publish && node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\"",
392
393
  "publish:vsce": "node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npx vsce publish",
393
394
  "publish:all": "npm run publish:npm && npm run publish:vsce"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync, readdirSync, statSync } from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+
8
+ const repoRoot = process.cwd();
9
+ const skillsRoot = path.join(repoRoot, '.github', 'skills');
10
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
11
+ const validatorPath = path.join(
12
+ codexHome,
13
+ 'skills',
14
+ '.system',
15
+ 'skill-creator',
16
+ 'scripts',
17
+ 'quick_validate.py'
18
+ );
19
+
20
+ if (!existsSync(skillsRoot)) {
21
+ console.error(`Skills directory not found: ${skillsRoot}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!existsSync(validatorPath)) {
26
+ console.error(`Skill validator not found: ${validatorPath}`);
27
+ console.error('Install the Codex skill-creator system skill or set CODEX_HOME correctly.');
28
+ process.exit(1);
29
+ }
30
+
31
+ const skillDirs = readdirSync(skillsRoot)
32
+ .map((name) => path.join(skillsRoot, name))
33
+ .filter((dir) => statSync(dir).isDirectory() && existsSync(path.join(dir, 'SKILL.md')))
34
+ .sort();
35
+
36
+ if (skillDirs.length === 0) {
37
+ console.log('No skills found to validate.');
38
+ process.exit(0);
39
+ }
40
+
41
+ for (const skillDir of skillDirs) {
42
+ const label = path.relative(repoRoot, skillDir);
43
+ console.log(`Validating ${label}`);
44
+ const result = spawnSync('python3', [validatorPath, skillDir], { stdio: 'inherit' });
45
+ if (result.status !== 0) {
46
+ process.exit(result.status ?? 1);
47
+ }
48
+ }
49
+
50
+ console.log(`Validated ${skillDirs.length} skills.`);