agentproc 0.2.1 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentproc",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "AgentProc Protocol SDK + CLI for Node.js — connect any Agent CLI to a messaging platform",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  "agentproc": "src/cli.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node --test src/index.test.js src/runner.test.js"
11
+ "test": "node --test src/index.test.js src/runner.test.js src/hub.test.js src/conformance.test.js"
12
12
  },
13
13
  "keywords": ["agentproc", "agent", "bridge", "protocol", "cli", "ai", "runner"],
14
14
  "license": "MIT",
package/src/cli.js CHANGED
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  /**
4
- * agentproc CLI — run any AgentProc profile against a message.
4
+ * agentproc CLI — drive any AgentProc profile against a message.
5
5
  *
6
- * Usage:
6
+ * Quick start (hub):
7
+ * agentproc hub list
8
+ * agentproc hub run echo-agent -p "hello"
9
+ * cd ~/projects/my-app && agentproc hub run claude-code -p "explain this"
10
+ *
11
+ * Advanced (local profile YAML, no hub fetch):
7
12
  * agentproc --profile <path.yaml> --prompt "hello" [options]
8
13
  *
9
- * Options:
14
+ * Options (local-profile mode):
10
15
  * --profile, -p <path> Profile YAML path (required)
11
16
  * --prompt <text> User message (required, unless --stdin)
12
17
  * --session <id> Previous session id for multi-turn
@@ -41,6 +46,7 @@ const fs = require('node:fs');
41
46
  const path = require('node:path');
42
47
 
43
48
  const runner = require('./runner.js');
49
+ const hub = require('./hub.js');
44
50
  const { PROTOCOL_VERSION } = runner;
45
51
 
46
52
  const PKG_VERSION = require('../package.json').version;
@@ -57,7 +63,7 @@ function parseArgs(argv) {
57
63
  sessionName: 'default',
58
64
  from: '',
59
65
  cwd: null,
60
- env: {},
66
+ env: [], // array of "KEY=VALUE" strings; --env can repeat
61
67
  timeout: null,
62
68
  stream: true,
63
69
  verbose: true,
@@ -83,20 +89,21 @@ function parseArgs(argv) {
83
89
  case '--session-name': opts.sessionName = next(); break;
84
90
  case '--from': opts.from = next(); break;
85
91
  case '--cwd': opts.cwd = next(); break;
86
- case '--env': {
87
- const kv = next();
88
- const eq = kv.indexOf('=');
89
- if (eq < 0) throw new Error(`--env expects KEY=VALUE, got: ${kv}`);
90
- opts.env[kv.slice(0, eq)] = kv.slice(eq + 1);
92
+ case '--env':
93
+ opts.env.push(next());
91
94
  break;
92
- }
93
95
  case '--timeout': opts.timeout = parseInt(next(), 10); break;
94
- case '--no-stream': opts.stream = false; break;
96
+ case '--no-stream': opts.noStream = true; break;
95
97
  case '--verbose': opts.verbose = true; break;
96
98
  case '--quiet': opts.verbose = false; break;
97
99
  case '--raw': opts.raw = true; break;
98
100
  case '--stdin': opts.stdin = true; break;
99
101
  default:
102
+ if (a === 'hub' && extras.length === 0) {
103
+ opts.hub = true;
104
+ opts.hubArgs = argv.slice(i + 1);
105
+ return { opts, extras };
106
+ }
100
107
  if (a.startsWith('--')) throw new Error(`unknown option: ${a}`);
101
108
  extras.push(a);
102
109
  }
@@ -104,9 +111,288 @@ function parseArgs(argv) {
104
111
  return { opts, extras };
105
112
  }
106
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // Hub subcommand dispatcher
116
+ // ---------------------------------------------------------------------------
117
+
118
+ async function runHubSubcommand(args) {
119
+ const sub = args[0];
120
+ const rest = args.slice(1);
121
+
122
+ if (!sub || sub === '--help' || sub === '-h') {
123
+ showHubHelp();
124
+ return 0;
125
+ }
126
+
127
+ // Any subcommand with --help/-h shows the hub help (covers `hub run --help`,
128
+ // `hub install --help`, etc. — useful for muscle-memory discovery).
129
+ if (rest.includes('--help') || rest.includes('-h')) {
130
+ showHubHelp();
131
+ return 0;
132
+ }
133
+
134
+ // Parse hub args. We can't reuse parseArgs() here because it throws on
135
+ // any unknown `--flag`, and hub supports `--refresh` which the runner
136
+ // parser doesn't know about. Instead, walk the args ourselves, separating
137
+ // hub-level flags (--refresh, --help) from runner-level flags (-p/--prompt,
138
+ // --cwd, --env, ...) and positional args (the profile name).
139
+ const refresh = rest.includes('--refresh');
140
+ const positional = [];
141
+ const runnerArgs = [];
142
+ for (let i = 0; i < rest.length; i++) {
143
+ const a = rest[i];
144
+ if (a === '--refresh' || a === '-h' || a === '--help') continue;
145
+ // Runner flags that take a value (next arg): match both long and short.
146
+ // In the `hub run` context, `-p` means `--prompt` (the profile name is
147
+ // positional, not a path), so we normalize it before handing off.
148
+ const takesValue =
149
+ a === '--prompt' || a === '-p' ||
150
+ a === '--session' || a === '--session-name' || a === '--from' ||
151
+ a === '--cwd' || a === '--env' || a === '--timeout';
152
+ if (takesValue) {
153
+ runnerArgs.push(a === '-p' ? '--prompt' : a);
154
+ if (i + 1 < rest.length) runnerArgs.push(rest[++i]);
155
+ continue;
156
+ }
157
+ // Other known runner flags (boolean).
158
+ if (a === '--no-stream' || a === '--verbose' || a === '--quiet' ||
159
+ a === '--raw' || a === '--stdin') {
160
+ runnerArgs.push(a);
161
+ continue;
162
+ }
163
+ // Anything else starting with - or -- is unknown to us — surface it
164
+ // as an explicit error instead of silently treating it as a positional.
165
+ if (a.startsWith('-')) {
166
+ process.stderr.write(`error: unknown option: ${a}\n\n`);
167
+ showHubHelp();
168
+ return 2;
169
+ }
170
+ positional.push(a);
171
+ }
172
+
173
+ if (sub === 'list') {
174
+ const profiles = await hub.listProfiles({
175
+ onLog: m => process.stderr.write(m + '\n'),
176
+ });
177
+ process.stdout.write('Available profiles in the official hub:\n\n');
178
+ for (const p of profiles) {
179
+ process.stdout.write(
180
+ ` ${p.name.padEnd(15)} ${p.tested.padEnd(12)} ${p.description.slice(0, 60)}\n`
181
+ );
182
+ }
183
+ process.stdout.write(`\nRun \`agentproc hub run <name> -p "hi"\` to use one.\n`);
184
+ return 0;
185
+ }
186
+
187
+ if (sub === 'show') {
188
+ if (!positional[0]) {
189
+ process.stderr.write('error: hub show requires a profile name\n');
190
+ return 2;
191
+ }
192
+ const readme = await hub.showReadme(positional[0], {
193
+ refresh,
194
+ onLog: m => process.stderr.write(m + '\n'),
195
+ });
196
+ process.stdout.write(readme);
197
+ if (!readme.endsWith('\n')) process.stdout.write('\n');
198
+ return 0;
199
+ }
200
+
201
+ if (sub === 'install') {
202
+ if (!positional[0]) {
203
+ process.stderr.write('error: hub install requires a profile name\n');
204
+ return 2;
205
+ }
206
+ const target = process.cwd();
207
+ const dest = await hub.installProfile(positional[0], target, {
208
+ refresh,
209
+ onLog: m => process.stderr.write(m + '\n'),
210
+ });
211
+ // After-the-fact hint: tell the user exactly what they got and how to
212
+ // run it. Without this, "installed to: ./echo-agent/" leaves them
213
+ // guessing what to type next.
214
+ process.stderr.write(`\n`);
215
+ process.stderr.write(`Next: edit ${path.relative(target, path.join(dest, 'profile.yaml'))} if you want, then run:\n`);
216
+ process.stderr.write(` agentproc --profile ${path.relative(target, path.join(dest, 'profile.yaml'))} --prompt "hi" --cwd <your-project>\n`);
217
+ return 0;
218
+ }
219
+
220
+ if (sub === 'run') {
221
+ if (!positional[0]) {
222
+ process.stderr.write('error: hub run requires a profile name\n');
223
+ return 2;
224
+ }
225
+ const profileName = positional[0];
226
+ const cacheDir = await hub.fetchProfile(profileName, {
227
+ refresh,
228
+ onLog: m => process.stderr.write(m + '\n'),
229
+ });
230
+ const profilePath = path.join(cacheDir, 'profile.yaml');
231
+
232
+ // Parse the runner-level flags we separated out above.
233
+ const { opts: runOpts } = parseArgs(runnerArgs);
234
+ if (!runOpts.prompt && !runOpts.stdin) {
235
+ process.stderr.write('error: hub run requires --prompt <text> or --stdin\n');
236
+ return 2;
237
+ }
238
+
239
+ // hub run uses the user's current directory as the agent's cwd when
240
+ // --cwd is not given. This matches the hub docs ("uses your current
241
+ // directory as cwd") and is the right default for AI-CLI profiles
242
+ // where the agent should operate on the user's project.
243
+ if (!runOpts.cwd) {
244
+ runOpts.cwd = process.cwd();
245
+ }
246
+
247
+ return await runAgent(profilePath, runOpts);
248
+ }
249
+
250
+ process.stderr.write(`error: unknown hub subcommand: ${sub}\n\n`);
251
+ showHubHelp();
252
+ return 2;
253
+ }
254
+
255
+ function showHubHelp() {
256
+ process.stdout.write(`agentproc hub — manage profiles from the official Hub
257
+
258
+ Usage:
259
+ agentproc hub list List all profiles in the hub
260
+ agentproc hub show <name> Show a profile's README
261
+ agentproc hub install <name> Copy a profile to the current directory
262
+ agentproc hub run <name> [run-options] Fetch (if needed) and run a profile
263
+
264
+ Hub run options (same as the regular --profile runner):
265
+ -p, --prompt <text> User message (or use --stdin)
266
+ --cwd <path> Override profile.cwd (default: current dir)
267
+ --env KEY=VALUE Extra env var (repeatable)
268
+ --session <id> Previous session id for multi-turn
269
+ --timeout <secs> Override profile.timeout_secs
270
+ --no-stream Disable streaming
271
+ --verbose / --quiet Protocol line visibility (default: verbose)
272
+ --stdin Read prompt from stdin
273
+
274
+ Common options:
275
+ --refresh Force re-fetch from GitHub (ignore cache)
276
+ -h, --help Show this help
277
+
278
+ Examples:
279
+ agentproc hub list
280
+ agentproc hub run echo-agent -p "hello"
281
+ cd ~/projects/my-app && agentproc hub run claude-code -p "explain this" --env ANTHROPIC_API_KEY=$KEY
282
+ agentproc hub show codex
283
+ agentproc hub install agy
284
+
285
+ Profiles are cached at ~/.agentproc/cache/hub/<name>/ (24h TTL).
286
+ `);
287
+ }
288
+
289
+ /**
290
+ * Shared runner logic used by both `agentproc --profile` and `agentproc hub run`.
291
+ * Kept here for the hub subcommand to reuse; the legacy main() path also calls it.
292
+ */
293
+ async function runAgent(profilePath, opts) {
294
+ let profileRaw;
295
+ const profileDir = path.dirname(path.resolve(profilePath));
296
+ try {
297
+ const yamlText = fs.readFileSync(path.resolve(profilePath), 'utf8');
298
+ profileRaw = parseYaml(yamlText);
299
+ } catch (e) {
300
+ process.stderr.write(`error: failed to read profile ${profilePath}: ${e.message}\n`);
301
+ return 2;
302
+ }
303
+
304
+ // Read prompt.
305
+ let prompt = opts.prompt;
306
+ if (opts.stdin) {
307
+ prompt = fs.readFileSync(0, 'utf8').replace(/\n$/, '');
308
+ }
309
+ if (prompt == null) {
310
+ process.stderr.write('error: --prompt (or --stdin) is required\n');
311
+ return 2;
312
+ }
313
+
314
+ // opts.env is an array of "KEY=VALUE" strings (from repeated --env flags)
315
+ const extraEnv = {};
316
+ for (const kv of opts.env || []) {
317
+ const eq = kv.indexOf('=');
318
+ if (eq < 0) {
319
+ process.stderr.write(`error: --env expects KEY=VALUE, got: ${kv}\n`);
320
+ return 2;
321
+ }
322
+ extraEnv[kv.slice(0, eq)] = kv.slice(eq + 1);
323
+ }
324
+
325
+ const streaming = opts.noStream ? false : null;
326
+
327
+ if (opts.raw) {
328
+ const r = await runner.run(profileRaw, {
329
+ message: prompt,
330
+ sessionId: opts.session || '',
331
+ sessionName: opts.sessionName || 'default',
332
+ fromUser: opts.from || '',
333
+ streaming,
334
+ cwd: opts.cwd,
335
+ profileDir,
336
+ extraEnv,
337
+ timeoutSecs: opts.timeout,
338
+ });
339
+ process.stdout.write(r.reply);
340
+ if (r.reply && !r.reply.endsWith('\n')) process.stdout.write('\n');
341
+ return r.exitCode === 0 ? 0 : 1;
342
+ }
343
+
344
+ // verbose: default true, --verbose keeps it true, --quiet sets it false.
345
+ const verbose = opts.verbose !== false;
346
+
347
+ const r = await runner.run(profileRaw, {
348
+ message: prompt,
349
+ sessionId: opts.session || '',
350
+ sessionName: opts.sessionName || 'default',
351
+ fromUser: opts.from || '',
352
+ streaming,
353
+ cwd: opts.cwd,
354
+ profileDir,
355
+ extraEnv,
356
+ timeoutSecs: opts.timeout,
357
+ onPartial: (t) => { if (verbose) process.stderr.write(`AGENT_PARTIAL:${JSON.stringify(t)}\n`); },
358
+ onSession: (id) => { if (verbose) process.stderr.write(`AGENT_SESSION:${id}\n`); },
359
+ onError: (msg) => { if (verbose) process.stderr.write(`AGENT_ERROR:${JSON.stringify(msg)}\n`); },
360
+ onStderr: (line) => { if (verbose) process.stderr.write(`[agent stderr] ${line}\n`); },
361
+ });
362
+
363
+ if (r.reply) {
364
+ process.stdout.write(r.reply);
365
+ if (!r.reply.endsWith('\n')) process.stdout.write('\n');
366
+ }
367
+ if (r.sessionId) process.stderr.write(`agentproc:session:${r.sessionId}\n`);
368
+ if (r.error) process.stderr.write(`agentproc:error:${r.error}\n`);
369
+ return r.exitCode === 0 ? 0 : 1;
370
+ }
371
+
107
372
  function showHelp() {
108
373
  process.stdout.write(`agentproc v${PKG_VERSION} (protocol ${PROTOCOL_VERSION})
109
374
 
375
+ The fastest way in:
376
+ agentproc hub list # see what's available
377
+ agentproc hub run echo-agent -p "hello" # smoke test (no API key)
378
+ cd ~/projects/my-app && agentproc hub run claude-code -p "explain this"
379
+
380
+ The CLI fetches the profile from the GitHub hub on first use, caches it at
381
+ ~/.agentproc/cache/hub/<name>/ (24h TTL), and uses your current directory as
382
+ the agent's cwd. Set GITHUB_TOKEN to raise the rate limit (see \`agentproc hub --help\`).
383
+
384
+ Hub subcommands:
385
+ hub list List all profiles in the hub
386
+ hub show <name> Show a profile's README
387
+ hub run <name> [run-options] Fetch (if needed) and run a profile
388
+ hub install <name> Copy a profile to the current directory
389
+
390
+ Run \`agentproc hub --help\` for the full hub reference.
391
+
392
+ ───────────────────────────────────────────────────────────────────────────────
393
+
394
+ Advanced: run a local profile YAML directly (no hub fetch)
395
+
110
396
  Usage:
111
397
  agentproc --profile <path.yaml> --prompt "hello" [options]
112
398
 
@@ -120,7 +406,8 @@ Session:
120
406
  --from <user> Sender identifier
121
407
 
122
408
  Execution:
123
- --cwd <path> Override profile.cwd
409
+ --cwd <path> Override profile.cwd (relative paths resolve
410
+ against the profile.yaml's directory)
124
411
  --env KEY=VALUE Extra env var (repeatable)
125
412
  --timeout <secs> Override profile.timeout_secs
126
413
  --no-stream Set AGENT_STREAMING=0
@@ -143,9 +430,16 @@ Output semantics:
143
430
  The final session id is printed on stderr as: agentproc:session:<id>
144
431
 
145
432
  Examples:
146
- agentproc --profile hub/echo-agent/profile.yaml --prompt "hi"
147
- agentproc -p hub/claude-code/profile.yaml --prompt "hello" --verbose
148
- cat prompt.txt | agentproc -p prof.yaml --stdin
433
+ # Local profile (relative cwd resolves next to profile.yaml):
434
+ agentproc --profile ./hub/echo-agent/profile.yaml --prompt "hi"
435
+
436
+ # Local claude-code profile, claude runs against your project:
437
+ agentproc --profile ./hub/claude-code/profile.yaml \\
438
+ --prompt "explain this codebase" \\
439
+ --cwd /path/to/your/project
440
+
441
+ # Prompt from stdin:
442
+ cat prompt.txt | agentproc --profile prof.yaml --stdin
149
443
  `);
150
444
  }
151
445
 
@@ -392,6 +686,11 @@ async function main() {
392
686
  if (opts.help) { showHelp(); process.exit(0); }
393
687
  if (opts.version) { showVersion(); process.exit(0); }
394
688
 
689
+ // `agentproc hub <subcommand>` — defer to hub dispatcher.
690
+ if (opts.hub) {
691
+ return await runHubSubcommand(opts.hubArgs);
692
+ }
693
+
395
694
  if (!opts.profile) {
396
695
  process.stderr.write('error: --profile is required\n\n');
397
696
  showHelp();
@@ -408,98 +707,46 @@ async function main() {
408
707
  process.exit(2);
409
708
  }
410
709
 
411
- // Read & parse profile YAML.
412
- let profileRaw;
710
+ // Read & parse profile YAML, then delegate to the shared runner path.
413
711
  try {
414
- const yamlText = fs.readFileSync(path.resolve(opts.profile), 'utf8');
415
- profileRaw = parseYaml(yamlText);
416
- } catch (e) {
417
- process.stderr.write(`error: failed to read profile ${opts.profile}: ${e.message}\n`);
418
- process.exit(2);
419
- }
420
-
421
- // ---- Raw mode: spawn agent, pipe stdout through, exit with agent code ----
422
- if (opts.raw) {
423
- const { spawn } = require('node:child_process');
424
- try {
425
- const r = await runner.run(profileRaw, {
426
- message: prompt,
427
- sessionId: opts.session,
428
- sessionName: opts.sessionName,
429
- fromUser: opts.from,
430
- streaming: opts.stream,
431
- cwd: opts.cwd,
432
- extraEnv: opts.env,
433
- timeoutSecs: opts.timeout,
434
- // No callbacks — we replace stdout forwarding below.
435
- });
436
- // The runner buffers reply; for raw mode we want streaming verbatim,
437
- // so we re-implement with raw pipes. Simpler: just print the reply
438
- // (which equals the agent's stdout minus protocol lines).
439
- // For TRUE raw output (including protocol lines), users should use
440
- // the bridge script directly.
441
- process.stdout.write(r.reply);
442
- if (r.reply && !r.reply.endsWith('\n')) process.stdout.write('\n');
443
- process.exit(r.exitCode === 0 ? 0 : 1);
444
- } catch (e) {
445
- process.stderr.write(`error: ${e.message}\n`);
446
- process.exit(1);
447
- }
448
- }
449
-
450
- // ---- Default mode: classify and pretty-print ----
451
- try {
452
- const r = await runner.run(profileRaw, {
453
- message: prompt,
454
- sessionId: opts.session,
455
- sessionName: opts.sessionName,
456
- fromUser: opts.from,
457
- streaming: opts.stream,
458
- cwd: opts.cwd,
459
- extraEnv: opts.env,
460
- timeoutSecs: opts.timeout,
461
- onPartial: (text) => {
462
- if (opts.verbose) process.stderr.write(`AGENT_PARTIAL:${JSON.stringify(text)}\n`);
463
- },
464
- onSession: (id) => {
465
- if (opts.verbose) process.stderr.write(`AGENT_SESSION:${id}\n`);
466
- },
467
- onError: (msg) => {
468
- if (opts.verbose) process.stderr.write(`AGENT_ERROR:${JSON.stringify(msg)}\n`);
469
- },
470
- onStderr: (line) => {
471
- if (opts.verbose) process.stderr.write(`[agent stderr] ${line}\n`);
472
- },
473
- });
474
-
475
- // Print final reply body to stdout.
476
- if (r.reply) {
477
- process.stdout.write(r.reply);
478
- if (!r.reply.endsWith('\n')) process.stdout.write('\n');
479
- }
480
-
481
- // Final session id on stderr (for shell capture).
482
- if (r.sessionId) {
483
- process.stderr.write(`agentproc:session:${r.sessionId}\n`);
484
- }
485
-
486
- if (r.error) {
487
- process.stderr.write(`agentproc:error:${r.error}\n`);
488
- }
489
-
490
- process.exit(r.exitCode === 0 ? 0 : 1);
712
+ return await runAgent(opts.profile, opts);
491
713
  } catch (e) {
492
714
  process.stderr.write(`error: ${e.message}\n`);
493
- process.exit(1);
715
+ return 1;
494
716
  }
495
717
  }
496
718
 
497
719
  // Run main() only when invoked directly as a script, not when required for tests.
498
720
  if (require.main === module) {
499
- main().catch(e => {
500
- process.stderr.write(`[agentproc] unhandled error: ${e && (e.stack || e)}\n`);
501
- process.exit(1);
502
- });
721
+ main().then(
722
+ (code) => {
723
+ // main() returns an explicit exit code from its various return paths;
724
+ // honor it so shell scripts can distinguish success from failure.
725
+ process.exit(typeof code === 'number' ? code : 0);
726
+ },
727
+ (e) => {
728
+ // Friendly handling for known hub errors: print the message + remediation
729
+ // hint, never a raw Node stack trace.
730
+ if (e && e.name === 'HubError') {
731
+ process.stderr.write(`error: ${e.message}\n`);
732
+ if (e.hint) process.stderr.write(`\n${e.hint}\n`);
733
+ process.exit(1);
734
+ return;
735
+ }
736
+ // For fetch() network errors wrapped by hub.js (also HubError, but be
737
+ // defensive in case some path throws a plain TypeError from fetch).
738
+ if (e && typeof e.message === 'string' && /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET/.test(e.message)) {
739
+ process.stderr.write(`error: network error talking to GitHub: ${e.message}\n`);
740
+ process.stderr.write(`\nThis is usually transient. Re-run the command, or run against a local checkout:\n`);
741
+ process.stderr.write(` agentproc --profile ./hub/<name>/profile.yaml --prompt "hi"\n`);
742
+ process.exit(1);
743
+ return;
744
+ }
745
+ // Everything else: still avoid dumping the stack. Show the message only.
746
+ process.stderr.write(`error: ${e && (e.message || e)}\n`);
747
+ process.exit(1);
748
+ }
749
+ );
503
750
  }
504
751
 
505
752
  module.exports = { parseArgs, parseYaml, showHelp, main };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+ /**
3
+ * Cross-implementation conformance tests.
4
+ *
5
+ * Drives the shared `spec/conformance/cases.json` fixture through the Node
6
+ * runner's `classifyLine` and asserts the result matches the expected
7
+ * {kind, value}. The Python SDK runs the same fixture through its
8
+ * `classify_line` in `sdk/python/tests/test_conformance.py` — together they
9
+ * guarantee the two reference implementations classify stdout identically.
10
+ *
11
+ * When you change the spec's line-recognition rules, add a case to the JSON
12
+ * file first; both SDKs will fail until they agree.
13
+ */
14
+
15
+ const { test } = require('node:test');
16
+ const assert = require('node:assert');
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+
20
+ const { classifyLine } = require('./runner.js');
21
+
22
+ const CASES_PATH = path.resolve(__dirname, '../../../spec/conformance/cases.json');
23
+ const data = JSON.parse(fs.readFileSync(CASES_PATH, 'utf8'));
24
+
25
+ for (const c of data.cases) {
26
+ test(`classifyLine: ${c.line.slice(0, 60)}`, () => {
27
+ assert.deepStrictEqual(classifyLine(c.line), c.expect);
28
+ });
29
+ }