@crouton-kit/crouter 0.2.6 → 0.3.2
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/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
- package/dist/cli.js +42 -37
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +214 -0
- package/dist/commands/__tests__/skill.test.d.ts +1 -0
- package/dist/commands/__tests__/skill.test.js +294 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +179 -0
- package/dist/commands/flow.d.ts +2 -0
- package/dist/commands/flow.js +24 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +480 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +669 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +1021 -0
- package/dist/commands/plan.d.ts +4 -2
- package/dist/commands/plan.js +306 -22
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.js +613 -456
- package/dist/commands/spec.d.ts +3 -2
- package/dist/commands/spec.js +283 -10
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +712 -0
- package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
- package/dist/core/__tests__/argv-parser.test.js +199 -0
- package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
- package/dist/core/__tests__/flow-leaves.test.js +248 -0
- package/dist/core/__tests__/job.test.d.ts +1 -0
- package/dist/core/__tests__/job.test.js +346 -0
- package/dist/core/__tests__/pkg.test.d.ts +1 -0
- package/dist/core/__tests__/pkg.test.js +218 -0
- package/dist/core/__tests__/sys.test.d.ts +1 -0
- package/dist/core/__tests__/sys.test.js +208 -0
- package/dist/core/artifact.d.ts +29 -18
- package/dist/core/artifact.js +78 -221
- package/dist/core/auto-update.js +11 -4
- package/dist/core/command.d.ts +36 -0
- package/dist/core/command.js +287 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +4 -0
- package/dist/core/help.d.ts +98 -0
- package/dist/core/help.js +163 -0
- package/dist/core/io.d.ts +29 -0
- package/dist/core/io.js +83 -0
- package/dist/core/jobs.d.ts +87 -0
- package/dist/core/jobs.js +353 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/core/self-update.js +105 -0
- package/dist/core/spawn.d.ts +47 -65
- package/dist/core/spawn.js +78 -228
- package/dist/prompts/agent.d.ts +10 -5
- package/dist/prompts/agent.js +51 -74
- package/dist/prompts/debug.d.ts +8 -0
- package/dist/prompts/debug.js +37 -0
- package/dist/prompts/review.js +4 -11
- package/dist/prompts/skill.d.ts +0 -1
- package/dist/prompts/skill.js +95 -149
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -2
- package/dist/commands/agent.js +0 -265
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -146
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -268
- package/dist/commands/marketplace.d.ts +0 -2
- package/dist/commands/marketplace.js +0 -365
- package/dist/commands/plugin.d.ts +0 -2
- package/dist/commands/plugin.js +0 -367
- package/dist/commands/update.d.ts +0 -4
- package/dist/commands/update.js +0 -140
- package/dist/prompts/plan.d.ts +0 -1
- package/dist/prompts/plan.js +0 -175
- package/dist/prompts/spec.d.ts +0 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
106
|
-
crtr plugin show my-plugin
|
|
107
|
-
crtr skill list --plugin my-plugin
|
|
108
|
-
crtr doctor
|
|
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
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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 {};
|