@crouton-kit/crouter 0.2.6 → 0.3.1

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.
Files changed (79) hide show
  1. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
  2. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
  3. package/dist/cli.js +42 -37
  4. package/dist/commands/__tests__/human.test.d.ts +1 -0
  5. package/dist/commands/__tests__/human.test.js +214 -0
  6. package/dist/commands/__tests__/skill.test.d.ts +1 -0
  7. package/dist/commands/__tests__/skill.test.js +287 -0
  8. package/dist/commands/debug.d.ts +3 -0
  9. package/dist/commands/debug.js +179 -0
  10. package/dist/commands/flow.d.ts +2 -0
  11. package/dist/commands/flow.js +24 -0
  12. package/dist/commands/human.d.ts +2 -0
  13. package/dist/commands/human.js +480 -0
  14. package/dist/commands/job.d.ts +2 -0
  15. package/dist/commands/job.js +669 -0
  16. package/dist/commands/pkg.d.ts +2 -0
  17. package/dist/commands/pkg.js +1021 -0
  18. package/dist/commands/plan.d.ts +4 -2
  19. package/dist/commands/plan.js +306 -22
  20. package/dist/commands/skill.d.ts +2 -2
  21. package/dist/commands/skill.js +607 -456
  22. package/dist/commands/spec.d.ts +3 -2
  23. package/dist/commands/spec.js +283 -10
  24. package/dist/commands/sys.d.ts +2 -0
  25. package/dist/commands/sys.js +712 -0
  26. package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
  27. package/dist/core/__tests__/argv-parser.test.js +199 -0
  28. package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
  29. package/dist/core/__tests__/flow-leaves.test.js +248 -0
  30. package/dist/core/__tests__/job.test.d.ts +1 -0
  31. package/dist/core/__tests__/job.test.js +346 -0
  32. package/dist/core/__tests__/pkg.test.d.ts +1 -0
  33. package/dist/core/__tests__/pkg.test.js +218 -0
  34. package/dist/core/__tests__/sys.test.d.ts +1 -0
  35. package/dist/core/__tests__/sys.test.js +208 -0
  36. package/dist/core/artifact.d.ts +29 -18
  37. package/dist/core/artifact.js +78 -221
  38. package/dist/core/auto-update.js +11 -3
  39. package/dist/core/command.d.ts +36 -0
  40. package/dist/core/command.js +287 -0
  41. package/dist/core/errors.d.ts +3 -0
  42. package/dist/core/errors.js +5 -0
  43. package/dist/core/fs-utils.d.ts +1 -0
  44. package/dist/core/fs-utils.js +4 -0
  45. package/dist/core/help.d.ts +98 -0
  46. package/dist/core/help.js +163 -0
  47. package/dist/core/io.d.ts +29 -0
  48. package/dist/core/io.js +83 -0
  49. package/dist/core/jobs.d.ts +87 -0
  50. package/dist/core/jobs.js +353 -0
  51. package/dist/core/pagination.d.ts +33 -0
  52. package/dist/core/pagination.js +89 -0
  53. package/dist/core/self-update.d.ts +21 -0
  54. package/dist/{commands/update.js → core/self-update.js} +28 -63
  55. package/dist/core/spawn.d.ts +47 -65
  56. package/dist/core/spawn.js +78 -228
  57. package/dist/prompts/agent.d.ts +10 -5
  58. package/dist/prompts/agent.js +51 -74
  59. package/dist/prompts/debug.d.ts +8 -0
  60. package/dist/prompts/debug.js +37 -0
  61. package/dist/prompts/review.js +4 -11
  62. package/dist/prompts/skill.d.ts +0 -1
  63. package/dist/prompts/skill.js +95 -149
  64. package/package.json +4 -2
  65. package/dist/commands/agent.d.ts +0 -2
  66. package/dist/commands/agent.js +0 -265
  67. package/dist/commands/config.d.ts +0 -2
  68. package/dist/commands/config.js +0 -146
  69. package/dist/commands/doctor.d.ts +0 -2
  70. package/dist/commands/doctor.js +0 -268
  71. package/dist/commands/marketplace.d.ts +0 -2
  72. package/dist/commands/marketplace.js +0 -365
  73. package/dist/commands/plugin.d.ts +0 -2
  74. package/dist/commands/plugin.js +0 -367
  75. package/dist/commands/update.d.ts +0 -4
  76. package/dist/prompts/plan.d.ts +0 -1
  77. package/dist/prompts/plan.js +0 -175
  78. package/dist/prompts/spec.d.ts +0 -1
  79. package/dist/prompts/spec.js +0 -153
@@ -7,13 +7,13 @@ keywords: [marketplace, marketplace.json, plugin, index, publishing]
7
7
 
8
8
  # Authoring crtr marketplaces
9
9
 
10
- A **marketplace** is a git repo that indexes multiple plugins. Users register it once, then `crtr marketplace install <mkt>:<plugin>` symlinks any plugin from it. One `crtr marketplace update` pulls updates for every installed plugin at once.
10
+ A **marketplace** is a git repo that indexes multiple plugins. Users register it once, then `crtr pkg market manage install --marketplace <mkt> --plugin <plugin>` symlinks any plugin from it. `crtr pkg market manage update --marketplace <mkt>` pulls updates for every installed plugin at once.
11
11
 
12
12
  Audience: LLM agents creating a marketplace or adding plugins to an existing one.
13
13
 
14
14
  ## When you need a marketplace (vs a single-plugin repo)
15
15
 
16
- A solo plugin ships from any git URL via `crtr plugin install <url>` — no marketplace needed.
16
+ A solo plugin ships from any git URL via `crtr pkg plugin manage install <url> --scope user` — no marketplace needed.
17
17
 
18
18
  Reach for a marketplace when:
19
19
  - You want to publish ≥3 plugins under one brand or theme.
@@ -70,12 +70,12 @@ The `plugins[]` array IS the index — what's installable. A plugin on disk but
70
70
 
71
71
  ## Install mechanics — symlinks, not clones
72
72
 
73
- When a user runs `crtr marketplace install my-marketplace:plugin-a`:
73
+ When a user runs `crtr pkg market manage install --marketplace my-marketplace --plugin plugin-a`:
74
74
  1. Crouter looks up `plugin-a` in the marketplace's manifest.
75
75
  2. **Symlinks** `<marketplace-clone>/plugins/plugin-a/` → `<user-scope>/plugins/plugin-a/`.
76
76
  3. Records the install in the scope config with `source_marketplace: my-marketplace`.
77
77
 
78
- Therefore one `crtr marketplace update my-marketplace` (which does `git pull` in the marketplace clone) updates every installed plugin from it — no per-plugin re-install, no second clone.
78
+ Therefore `crtr pkg market manage update --marketplace my-marketplace` (which does `git pull` in the marketplace clone) updates every installed plugin from it — no per-plugin re-install, no second clone.
79
79
 
80
80
  ## Version-bump automation (recommended)
81
81
 
@@ -107,14 +107,14 @@ mkdir -p plugins/my-new-plugin/.crouter-plugin plugins/my-new-plugin/skills
107
107
  $EDITOR plugins/my-new-plugin/.crouter-plugin/plugin.json
108
108
 
109
109
  # Add at least one skill
110
- crtr skill new my-new-plugin:first-skill --type playbook --description "Use when …"
110
+ crtr skill author scaffold my-new-plugin:first-skill --type playbook --description "Use when …"
111
111
 
112
112
  # Add the plugin to the marketplace index
113
113
  $EDITOR .crouter-marketplace/marketplace.json
114
114
  # (append to plugins[] with name, initial version, source, description)
115
115
 
116
116
  # Validate
117
- crtr doctor
117
+ crtr sys doctor
118
118
 
119
119
  # Commit — CI bumps versions if you've wired up auto-bump
120
120
  git add -A
@@ -122,7 +122,7 @@ git commit -m "feat: add my-new-plugin"
122
122
  git push
123
123
  ```
124
124
 
125
- Existing users pick it up on their next `crtr marketplace update <marketplace-name>` (or on the next auto-update tick if they've set `auto_update.content: "apply"`).
125
+ Existing users pick it up on their next `crtr pkg market manage update --marketplace <marketplace-name>` (or on the next auto-update tick if they've set `auto_update.content: "apply"`).
126
126
 
127
127
  ## Updating an existing plugin
128
128
 
@@ -141,11 +141,11 @@ A marketplace itself registers per-scope:
141
141
  | user | `~/.crouter/marketplaces/<name>/` | Private to your machine — your personal subscriptions |
142
142
  | project | `<project>/.crouter/marketplaces/<name>/` | Checked into the repo — anyone cloning gets the same marketplace pinned |
143
143
 
144
- `crtr marketplace add <git-url> --scope user` is the default. Use `--scope project` when the marketplace is integral to how the repo expects to be developed.
144
+ `crtr pkg market manage add --url <git-url> --scope user` is the default. Use `--scope project` when the marketplace is integral to how the repo expects to be developed.
145
145
 
146
146
  ## Validation
147
147
 
148
- `crtr doctor` checks marketplaces:
148
+ `crtr sys doctor` checks marketplaces:
149
149
  - `marketplace.json` is valid JSON.
150
150
  - Every entry in `plugins[]` corresponds to a real directory under `plugins/`.
151
151
  - Each plugin under `plugins/` passes plugin-level validation.
@@ -17,7 +17,7 @@ Scope-owned skills live at `~/.crouter/skills/` (user) or `<project>/.crouter/sk
17
17
 
18
18
  Reach for a **plugin** when:
19
19
  - You want to share skills across multiple projects or with other people.
20
- - You want versioning + update mechanics (`crtr plugin update`).
20
+ - You want versioning + update mechanics (`crtr pkg plugin manage update --name <name>`).
21
21
  - You want a marketplace to index the work — see [[crouter-development/marketplaces]].
22
22
 
23
23
  If it's a one-off note for yourself, scope-owned skills are simpler. Promote to a plugin later.
@@ -43,7 +43,7 @@ The `<plugin-name>` directory IS the plugin. The manifest's `name` field must ma
43
43
  {
44
44
  "name": "my-plugin",
45
45
  "version": "0.1.0",
46
- "description": "One sentence — shown in `crtr plugin list`.",
46
+ "description": "One sentence — shown in `crtr pkg plugin inspect list`.",
47
47
  "source": "https://github.com/<owner>/<repo>",
48
48
  "owner": {
49
49
  "name": "Your Name",
@@ -57,7 +57,7 @@ The `<plugin-name>` directory IS the plugin. The manifest's `name` field must ma
57
57
  | `name` | yes | Must match the directory name. Lowercase kebab. |
58
58
  | `version` | yes | Semver. Marketplace CI may bump automatically — see marketplaces skill. |
59
59
  | `description` | yes | One sentence. |
60
- | `source` | recommended | Git URL where the plugin lives. Used by `crtr plugin update`. |
60
+ | `source` | recommended | Git URL where the plugin lives. Used by `crtr pkg plugin manage update --name <name>`. |
61
61
  | `owner` | optional | Author info. |
62
62
 
63
63
  ## Scopes
@@ -75,19 +75,19 @@ Project-scope plugins outrank user-scope on resolution. Both outrank marketplace
75
75
 
76
76
  Three ways a plugin lands in a scope:
77
77
 
78
- 1. **From a git URL** (`crtr plugin install <url> --scope user`):
78
+ 1. **From a git URL** (`crtr pkg plugin manage install <url> --scope user`):
79
79
  - Clones into `<scope>/plugins/<name>/` using the manifest's name.
80
- - `crtr plugin update <name>` does `git pull`.
80
+ - `crtr pkg plugin manage update --name <name>` does `git pull`.
81
81
  - Independent of any marketplace.
82
82
 
83
- 2. **From a marketplace** (`crtr marketplace install <mkt>:<name> --scope user`):
83
+ 2. **From a marketplace** (`crtr pkg market manage install --marketplace <mkt> --plugin <name>`):
84
84
  - **Symlinks** the marketplace's `plugins/<name>/` into `<scope>/plugins/<name>/`.
85
- - One `crtr marketplace update <mkt>` pulls updates for every installed plugin from that marketplace.
85
+ - `crtr pkg market manage update --marketplace <mkt>` pulls updates for every installed plugin from that marketplace.
86
86
  - See [[crouter-development/marketplaces]].
87
87
 
88
88
  3. **Authored in place** (you're writing the plugin in a working repo):
89
89
  - Symlink for tight dev loop: `ln -s $(pwd) ~/.crouter/plugins/<name>`.
90
- - Or `crtr plugin install file://$(pwd) --scope project` to clone-install.
90
+ - Or `crtr pkg plugin manage install file://$(pwd) --scope project` to clone-install.
91
91
 
92
92
  ## Local development loop
93
93
 
@@ -96,19 +96,19 @@ Three ways a plugin lands in a scope:
96
96
  mkdir -p my-plugin/.crouter-plugin my-plugin/skills
97
97
  $EDITOR my-plugin/.crouter-plugin/plugin.json # write the manifest
98
98
  cd my-plugin
99
- crtr skill new my-plugin:my-first-skill --type playbook --description "Use when …"
99
+ crtr skill author scaffold my-plugin:my-first-skill --type playbook --description "Use when …"
100
100
 
101
101
  # Symlink for fast iteration — no clone, edits land immediately
102
102
  ln -s $(pwd) ~/.crouter/plugins/my-plugin
103
103
 
104
104
  # Verify
105
- crtr plugin list # my-plugin appears
106
- crtr plugin show my-plugin # lists its skills
107
- crtr skill list --plugin my-plugin
108
- crtr doctor # validates manifest + every skill
105
+ crtr pkg plugin inspect list # my-plugin appears
106
+ crtr pkg plugin inspect show my-plugin # lists its skills
107
+ crtr skill find list --plugin my-plugin # just my-plugin's skills
108
+ crtr sys doctor # validates manifest + every skill
109
109
  ```
110
110
 
111
- When ready to share: push to a git remote; anyone can `crtr plugin install <url>`.
111
+ When ready to share: push to a git remote; anyone can `crtr pkg plugin manage install <url> --scope user`.
112
112
 
113
113
  ## Versioning
114
114
 
@@ -120,13 +120,13 @@ Standard semver:
120
120
  | New skill, new section, new example | minor (0.1.0 → 0.2.0) |
121
121
  | Removed skill, renamed skill, changed manifest schema | major (0.1.0 → 1.0.0) |
122
122
 
123
- `crtr plugin update <name>` reads the new version after pulling and updates the local config. Plugins published through a marketplace may have their `version` field bumped automatically by CI — see [[crouter-development/marketplaces]].
123
+ `crtr pkg plugin manage update --name <name>` reads the new version after pulling and updates the local config. Plugins published through a marketplace may have their `version` field bumped automatically by CI — see [[crouter-development/marketplaces]].
124
124
 
125
125
  ## Enable/disable
126
126
 
127
- `crtr plugin disable <name>` flips the per-scope config without removing files. Disabled plugins are hidden from `crtr skill list` and don't resolve via `crtr skill show <name>`. Re-enable with `crtr plugin enable <name>`.
127
+ `crtr pkg plugin manage disable <name>` flips the per-scope config without removing files. Disabled plugins are hidden from `crtr skill find list` and don't resolve via `crtr skill read show <name>`. Re-enable with `crtr pkg plugin manage enable <name>`.
128
128
 
129
- Individual skills inside an enabled plugin can also be disabled: `crtr skill disable <plugin>:<skill>`.
129
+ Individual skills inside an enabled plugin can also be disabled: `crtr skill state disable <plugin>:<skill>`.
130
130
 
131
131
  ## What goes in a plugin
132
132
 
@@ -145,10 +145,10 @@ If your skill conceptually depends on another plugin's skill, link via `## Relat
145
145
 
146
146
  ## Validation
147
147
 
148
- `crtr doctor` checks every plugin:
148
+ `crtr sys doctor` checks every plugin:
149
149
  - Manifest exists and is valid JSON.
150
150
  - Manifest `name` matches the directory name.
151
- - Every skill under `skills/` passes the skill-validation contract (frontmatter parses, `name` matches dir path, `type` in enum). Run `crtr skill` (no args) for the full format reference.
151
+ - Every skill under `skills/` passes the skill-validation contract (frontmatter parses, `name` matches dir path, `type` in enum). Run `crtr skill author guide` for the authoring workflow + SKILL.md format reference.
152
152
  - Sibling artifact dirs (`commands/`, `hooks/`, etc.) — validated by their respective specs as those land.
153
153
 
154
154
  ## Cross-publishing with Claude Code
package/dist/cli.js CHANGED
@@ -1,46 +1,51 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { readFileSync } from 'node:fs';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname, join } from 'node:path';
6
- import { registerSkillCommands } from './commands/skill.js';
7
- import { registerPluginCommands } from './commands/plugin.js';
8
- import { registerMarketplaceCommands } from './commands/marketplace.js';
9
- import { registerConfigCommands } from './commands/config.js';
10
- import { registerUpdateCommand } from './commands/update.js';
11
- import { registerDoctorCommand } from './commands/doctor.js';
12
- import { registerPlanCommand } from './commands/plan.js';
13
- import { registerSpecCommand } from './commands/spec.js';
14
- import { registerAgentCommand } from './commands/agent.js';
2
+ import { defineRoot, runCli } from './core/command.js';
3
+ import { registerFlow } from './commands/flow.js';
4
+ import { registerSkill } from './commands/skill.js';
5
+ import { registerPkg } from './commands/pkg.js';
6
+ import { registerJob } from './commands/job.js';
7
+ import { registerHuman } from './commands/human.js';
8
+ import { registerSys } from './commands/sys.js';
15
9
  import { maybeAutoUpdate } from './core/auto-update.js';
16
10
  import { ensureBootSkill, ensureOfficialMarketplace, ensureProjectScope } from './core/bootstrap.js';
17
- function readPackageVersion() {
18
- const here = dirname(fileURLToPath(import.meta.url));
19
- const pkgPath = join(here, '..', 'package.json');
20
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
21
- if (typeof pkg.version === 'string')
22
- return pkg.version;
23
- return '0.0.0';
24
- }
25
- const program = new Command();
26
- program
27
- .name('crtr')
28
- .description('crtr — fast access to skills, plugins, and marketplaces')
29
- .version(readPackageVersion(), '-v, --version');
30
- registerSkillCommands(program);
31
- registerPluginCommands(program);
32
- registerMarketplaceCommands(program);
33
- registerConfigCommands(program);
34
- registerUpdateCommand(program);
35
- registerDoctorCommand(program);
36
- registerPlanCommand(program);
37
- registerSpecCommand(program);
38
- registerAgentCommand(program);
11
+ const root = defineRoot({
12
+ help: {
13
+ tagline: 'crtr: agentic planning runtime.',
14
+ concepts: [
15
+ { name: 'flow', desc: 'spec → plan → debug: the development process' },
16
+ { name: 'skill', desc: 'loadable SKILL.md document an agent reads to adopt a workflow' },
17
+ { name: 'pkg', desc: 'plugins and marketplaces that supply skills' },
18
+ { name: 'job', desc: 'a running agent worker and its logs and result' },
19
+ { name: 'human', desc: 'human-in-the-loop decisions, document review, and live display' },
20
+ { name: 'sys', desc: 'crtr configuration, diagnostics, and self-management' },
21
+ ],
22
+ subtrees: [
23
+ { name: 'flow', desc: 'spec, plan, and debug workflows', useWhen: 'capturing requirements, planning work, or root-causing a bug' },
24
+ { name: 'skill', desc: 'discover, read, author, and manage skills', useWhen: 'working with SKILL.md documents' },
25
+ { name: 'pkg', desc: 'manage plugins and marketplaces', useWhen: 'installing or browsing skill collections' },
26
+ { name: 'job', desc: 'spawn, monitor, and collect from agent workers', useWhen: 'running or watching agent jobs' },
27
+ { name: 'human', desc: 'ask, approve, review, notify, show, inbox, list', useWhen: 'putting a decision or document in front of a person' },
28
+ { name: 'sys', desc: 'config, doctor, update, version', useWhen: 'managing the crtr installation' },
29
+ ],
30
+ globals: [
31
+ { name: '-h', desc: 'print help for any node — append to any subcommand path' },
32
+ ],
33
+ },
34
+ subtrees: [
35
+ registerFlow(),
36
+ registerSkill(),
37
+ registerPkg(),
38
+ registerJob(),
39
+ registerHuman(),
40
+ registerSys(),
41
+ ],
42
+ });
39
43
  ensureOfficialMarketplace(process.argv);
40
44
  ensureBootSkill(process.argv);
41
45
  ensureProjectScope(process.argv);
42
46
  maybeAutoUpdate(process.argv);
43
- program.parseAsync().catch((err) => {
44
- process.stderr.write(`crtr: ${err instanceof Error ? err.message : String(err)}\n`);
47
+ runCli(root, process.argv).catch((err) => {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ process.stderr.write(`crtr: ${msg}\n`);
45
50
  process.exit(1);
46
51
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,214 @@
1
+ // Tests for the human subtree argv-model migration.
2
+ // Run with: node --import tsx/esm --test 'src/commands/__tests__/human.test.ts'
3
+ //
4
+ // These tests exercise the leaf param schemas via parseArgv (framework) and
5
+ // spot-check the leaf definitions directly — no subprocess spawning, no tmux.
6
+ import { test, describe } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { writeFileSync, mkdirSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { randomBytes } from 'node:crypto';
12
+ import { parseArgv } from '../../core/command.js';
13
+ // ---------------------------------------------------------------------------
14
+ // Helper: write a temp JSON file and return its path.
15
+ // ---------------------------------------------------------------------------
16
+ function tmpJson(obj) {
17
+ const dir = join(tmpdir(), `crtr-human-test-${randomBytes(4).toString('hex')}`);
18
+ mkdirSync(dir, { recursive: true });
19
+ const p = join(dir, 'deck.json');
20
+ writeFileSync(p, JSON.stringify(obj));
21
+ return p;
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // human approve — positional title + optional flags
25
+ // ---------------------------------------------------------------------------
26
+ describe('human approve: params', () => {
27
+ const params = [
28
+ { kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The question.' },
29
+ { kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'One-line context.' },
30
+ { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Markdown body.' },
31
+ ];
32
+ test('parses positional title', async () => {
33
+ const result = await parseArgv(params, ['Deploy to prod?']);
34
+ assert.equal(result['title'], 'Deploy to prod?');
35
+ });
36
+ test('parses title with optional flags', async () => {
37
+ const result = await parseArgv(params, ['Deploy?', '--subtitle', 'v1.2.3', '--body', 'LGTM']);
38
+ assert.equal(result['title'], 'Deploy?');
39
+ assert.equal(result['subtitle'], 'v1.2.3');
40
+ assert.equal(result['body'], 'LGTM');
41
+ });
42
+ test('flags absent → undefined', async () => {
43
+ const result = await parseArgv(params, ['Deploy?']);
44
+ assert.equal(result['subtitle'], undefined);
45
+ assert.equal(result['body'], undefined);
46
+ });
47
+ test('missing title throws missing_parameter', async () => {
48
+ await assert.rejects(() => parseArgv(params, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
49
+ });
50
+ });
51
+ // ---------------------------------------------------------------------------
52
+ // human review — positional file (path) + optional --output
53
+ // ---------------------------------------------------------------------------
54
+ describe('human review: params', () => {
55
+ const params = [
56
+ { kind: 'positional', name: 'file', type: 'path', required: true, constraint: 'Existing .md file.' },
57
+ { kind: 'flag', name: 'output', type: 'path', required: false, constraint: 'Where to write feedback.' },
58
+ ];
59
+ test('parses positional file path', async () => {
60
+ const result = await parseArgv(params, ['/tmp/plan.md']);
61
+ assert.equal(result['file'], '/tmp/plan.md');
62
+ });
63
+ test('parses file + --output', async () => {
64
+ const result = await parseArgv(params, ['/tmp/plan.md', '--output', '/tmp/fb.json']);
65
+ assert.equal(result['file'], '/tmp/plan.md');
66
+ assert.equal(result['output'], '/tmp/fb.json');
67
+ });
68
+ test('missing file throws missing_parameter', async () => {
69
+ await assert.rejects(() => parseArgv(params, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
70
+ });
71
+ });
72
+ // ---------------------------------------------------------------------------
73
+ // human notify — positional title + optional --body
74
+ // ---------------------------------------------------------------------------
75
+ describe('human notify: params', () => {
76
+ const params = [
77
+ { kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'Headline.' },
78
+ { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Markdown body.' },
79
+ ];
80
+ test('parses positional title', async () => {
81
+ const result = await parseArgv(params, ['Build done']);
82
+ assert.equal(result['title'], 'Build done');
83
+ });
84
+ test('parses title + --body', async () => {
85
+ const result = await parseArgv(params, ['Done', '--body', 'All tests pass.']);
86
+ assert.equal(result['title'], 'Done');
87
+ assert.equal(result['body'], 'All tests pass.');
88
+ });
89
+ });
90
+ // ---------------------------------------------------------------------------
91
+ // human show — positional path + --watch bool + --window enum
92
+ // ---------------------------------------------------------------------------
93
+ describe('human show: params', () => {
94
+ const params = [
95
+ { kind: 'positional', name: 'path', type: 'path', required: true, constraint: 'File to render.' },
96
+ { kind: 'flag', name: 'watch', type: 'bool', required: false, constraint: 'Presence = true.' },
97
+ { kind: 'flag', name: 'window', type: 'enum', choices: ['auto', 'split', 'new'], required: false, default: 'auto', constraint: 'Placement.' },
98
+ ];
99
+ test('parses positional path', async () => {
100
+ const result = await parseArgv(params, ['/tmp/doc.md']);
101
+ assert.equal(result['path'], '/tmp/doc.md');
102
+ });
103
+ test('--watch absent → false (presence-only bool default)', async () => {
104
+ const result = await parseArgv(params, ['/tmp/doc.md']);
105
+ assert.equal(result['watch'], false);
106
+ });
107
+ test('--watch present → true', async () => {
108
+ const result = await parseArgv(params, ['/tmp/doc.md', '--watch']);
109
+ assert.equal(result['watch'], true);
110
+ });
111
+ test('--watch=true is rejected (bool takes no value)', async () => {
112
+ await assert.rejects(() => parseArgv(params, ['/tmp/doc.md', '--watch=true']), (err) => { assert.match(err.message, /takes no value/); return true; });
113
+ });
114
+ test('--window default is auto', async () => {
115
+ const result = await parseArgv(params, ['/tmp/doc.md']);
116
+ assert.equal(result['window'], 'auto');
117
+ });
118
+ test('--window accepts valid enum value', async () => {
119
+ const result = await parseArgv(params, ['/tmp/doc.md', '--window', 'split']);
120
+ assert.equal(result['window'], 'split');
121
+ });
122
+ test('--window rejects invalid value', async () => {
123
+ await assert.rejects(() => parseArgv(params, ['/tmp/doc.md', '--window', 'float']), (err) => { assert.match(err.message, /must be one of/); return true; });
124
+ });
125
+ });
126
+ // ---------------------------------------------------------------------------
127
+ // human ask — --context-file (context-file kind, key=deckFile) + --wait bool
128
+ //
129
+ // NOTE: The argv parser hardcodes the CLI token as `--context-file` regardless
130
+ // of the param's `name` field. The `name` field ("deckFile") becomes the key
131
+ // in the input record. So the correct invocation is:
132
+ // crtr human ask --context-file deck.json
133
+ // not --deck-file. Tests use --context-file accordingly.
134
+ // ---------------------------------------------------------------------------
135
+ describe('human ask: params', () => {
136
+ const params = [
137
+ { kind: 'context-file', name: 'deckFile', required: true, constraint: 'Humanloop deck JSON.' },
138
+ { kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'Presence-only.' },
139
+ ];
140
+ test('--context-file parses JSON file and yields input.deckFile', async () => {
141
+ const deck = { interactions: [{ id: 'q', title: 'Go?', options: [{ id: 'yes', label: 'Yes' }] }] };
142
+ const path = tmpJson(deck);
143
+ const result = await parseArgv(params, ['--context-file', path]);
144
+ assert.deepEqual(result['deckFile'], deck);
145
+ });
146
+ test('--context-file missing → missing_parameter', async () => {
147
+ await assert.rejects(() => parseArgv(params, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
148
+ });
149
+ test('--context-file with nonexistent file → invalid_type', async () => {
150
+ await assert.rejects(() => parseArgv(params, ['--context-file', '/no/such/deck.json']), (err) => { assert.match(err.message, /cannot read file/); return true; });
151
+ });
152
+ test('--context-file with non-JSON file → invalid_type', async () => {
153
+ const dir = join(tmpdir(), `crtr-human-test-${randomBytes(4).toString('hex')}`);
154
+ mkdirSync(dir, { recursive: true });
155
+ const p = join(dir, 'bad.json');
156
+ writeFileSync(p, 'not json at all');
157
+ await assert.rejects(() => parseArgv(params, ['--context-file', p]), (err) => { assert.match(err.message, /not valid JSON/); return true; });
158
+ });
159
+ test('--wait absent → false', async () => {
160
+ const deck = { interactions: [] };
161
+ const path = tmpJson(deck);
162
+ const result = await parseArgv(params, ['--context-file', path]);
163
+ assert.equal(result['wait'], false);
164
+ });
165
+ test('--wait present → true', async () => {
166
+ const deck = { interactions: [] };
167
+ const path = tmpJson(deck);
168
+ const result = await parseArgv(params, ['--context-file', path, '--wait']);
169
+ assert.equal(result['wait'], true);
170
+ });
171
+ });
172
+ // ---------------------------------------------------------------------------
173
+ // human list — --limit int + --cursor string
174
+ // ---------------------------------------------------------------------------
175
+ describe('human list: params', () => {
176
+ const params = [
177
+ { kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: 'Default 20.' },
178
+ { kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Pagination token.' },
179
+ ];
180
+ test('defaults: limit=20, cursor=undefined', async () => {
181
+ const result = await parseArgv(params, []);
182
+ assert.equal(result['limit'], 20);
183
+ assert.equal(result['cursor'], undefined);
184
+ });
185
+ test('--limit parses integer', async () => {
186
+ const result = await parseArgv(params, ['--limit', '5']);
187
+ assert.equal(result['limit'], 5);
188
+ });
189
+ test('--limit rejects non-integer', async () => {
190
+ await assert.rejects(() => parseArgv(params, ['--limit', 'abc']), (err) => { assert.match(err.message, /must be an integer/); return true; });
191
+ });
192
+ test('--cursor passes through as string', async () => {
193
+ const result = await parseArgv(params, ['--cursor', 'tok_abc123']);
194
+ assert.equal(result['cursor'], 'tok_abc123');
195
+ });
196
+ });
197
+ // ---------------------------------------------------------------------------
198
+ // human _run — no params (reads CRTR_HUMAN_DIR from env)
199
+ // ---------------------------------------------------------------------------
200
+ describe('human _run: no params', () => {
201
+ const params = [];
202
+ test('empty argv yields empty result (no params declared)', async () => {
203
+ const result = await parseArgv(params, []);
204
+ assert.deepEqual(result, {});
205
+ });
206
+ test('positional token throws bad_invocation (no positionals declared)', async () => {
207
+ await assert.rejects(() => parseArgv(params, ['some-value']), (err) => { assert.match(err.message, /takes no positional/); return true; });
208
+ });
209
+ test('CRTR_HUMAN_DIR env var contract: _run reads it from env, not argv', () => {
210
+ // Structural check: the params array is empty, confirming no argv surface.
211
+ // The actual env-var read is in the run handler and verified by inspection.
212
+ assert.equal(params.length, 0);
213
+ });
214
+ });
@@ -0,0 +1 @@
1
+ export {};