@stigmer/cli 3.15.3-dev.20260914121148 → 3.15.3-dev.20260916211208
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/README.md +3 -4
- package/commands/apply.d.ts.map +1 -1
- package/commands/apply.js +56 -4
- package/commands/apply.js.map +1 -1
- package/commands/push.d.ts.map +1 -1
- package/commands/push.js +122 -16
- package/commands/push.js.map +1 -1
- package/commands/validate.d.ts.map +1 -1
- package/commands/validate.js +39 -7
- package/commands/validate.js.map +1 -1
- package/output/command-result.d.ts +8 -0
- package/output/command-result.d.ts.map +1 -1
- package/output/command-result.js +13 -0
- package/output/command-result.js.map +1 -1
- package/package.json +6 -5
- package/registry/aliases.d.ts.map +1 -1
- package/registry/aliases.js.map +1 -1
- package/registry/index.d.ts +3 -3
- package/registry/index.d.ts.map +1 -1
- package/registry/index.js +3 -3
- package/registry/index.js.map +1 -1
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +85 -18
- package/registry/metadata.js.map +1 -1
- package/registry/registry.d.ts.map +1 -1
- package/registry/registry.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +81 -13
- package/registry/verb-support.js.map +1 -1
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +35 -9
- package/resources/delete.js.map +1 -1
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +56 -13
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +62 -14
- package/resources/list.js.map +1 -1
- package/resources/plugin.d.ts +80 -0
- package/resources/plugin.d.ts.map +1 -0
- package/resources/plugin.js +305 -0
- package/resources/plugin.js.map +1 -0
- package/resources/skill.d.ts +1 -0
- package/resources/skill.d.ts.map +1 -1
- package/resources/skill.js +1 -1
- package/resources/skill.js.map +1 -1
- package/src/commands/apply.ts +133 -21
- package/src/commands/push.ts +270 -41
- package/src/commands/validate.test.ts +111 -22
- package/src/commands/validate.ts +68 -12
- package/src/output/command-result.test.ts +9 -0
- package/src/output/command-result.ts +14 -0
- package/src/registry/aliases.test.ts +24 -5
- package/src/registry/aliases.ts +6 -1
- package/src/registry/index.ts +19 -3
- package/src/registry/metadata.ts +85 -18
- package/src/registry/registry.test.ts +86 -37
- package/src/registry/registry.ts +8 -2
- package/src/registry/verb-support.ts +85 -14
- package/src/resources/delete.ts +106 -26
- package/src/resources/get-bindings.ts +95 -17
- package/src/resources/list.ts +102 -28
- package/src/resources/plugin.test.ts +233 -0
- package/src/resources/plugin.ts +432 -0
- package/src/resources/skill.ts +1 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
// Command-level
|
|
2
|
-
//
|
|
3
|
-
// contract: 0 = valid
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Command-level contract for `validate`. Drives the real commander program
|
|
2
|
+
// against temp files and asserts the classified exit code and, for the plugin
|
|
3
|
+
// route, the rendered output. The contract: 0 = valid (a plugin with warnings
|
|
4
|
+
// is still 0), 2 = invalid input (bad path, bad YAML, unknown kind, schema
|
|
5
|
+
// failure, a plugin the reader refuses), 1 = unexpected. This is a
|
|
6
|
+
// deliberate, documented refinement over the Go CLI, which returned a
|
|
7
|
+
// generic exit 1 for all validate failures. The YAML cases are unchanged by
|
|
8
|
+
// the plugin route and pin that it stays out of their way.
|
|
6
9
|
|
|
7
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
11
|
import { tmpdir } from "node:os";
|
|
9
12
|
import { join } from "node:path";
|
|
10
13
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
@@ -12,6 +15,14 @@ import { classify, ExitCode } from "../errors/index.js";
|
|
|
12
15
|
import { buildProgram } from "../program.js";
|
|
13
16
|
|
|
14
17
|
let dir: string;
|
|
18
|
+
let plugin: string;
|
|
19
|
+
let brokenPlugin: string;
|
|
20
|
+
|
|
21
|
+
function write(root: string, rel: string, content: string): void {
|
|
22
|
+
const full = join(root, rel);
|
|
23
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
24
|
+
writeFileSync(full, content);
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
beforeAll(() => {
|
|
17
28
|
dir = mkdtempSync(join(tmpdir(), "stigmer-validate-"));
|
|
@@ -31,49 +42,127 @@ beforeAll(() => {
|
|
|
31
42
|
);
|
|
32
43
|
writeFileSync(join(dir, "bad-schema.yaml"), ["kind: Agent", "metadata: not-an-object", ""].join("\n"));
|
|
33
44
|
writeFileSync(join(dir, "unknown.yaml"), ["kind: Banana", "metadata:", " name: x", ""].join("\n"));
|
|
45
|
+
|
|
46
|
+
// A Cursor plugin with a header token the manifest does not declare: valid,
|
|
47
|
+
// with one warning, and an ai.stigmer/ document that must not be validated
|
|
48
|
+
// as a loose resource.
|
|
49
|
+
plugin = mkdtempSync(join(tmpdir(), "stigmer-validate-plugin-"));
|
|
50
|
+
write(plugin, ".cursor-plugin/plugin.json", JSON.stringify({ name: "github", version: "1.0.0", mcpServers: "./mcp.json", logo: "assets/logo.svg" }));
|
|
51
|
+
write(plugin, "mcp.json", JSON.stringify({ mcpServers: { github: { type: "http", url: "https://api.example.com/mcp/", headers: { Authorization: "Bearer ${GITHUB_TOKEN}" } } } }));
|
|
52
|
+
write(plugin, "ai.stigmer/agent.yaml", "not: a valid resource document\n");
|
|
53
|
+
write(plugin, "assets/logo.svg", "<svg/>");
|
|
54
|
+
|
|
55
|
+
// The same plugin with the URL made a variable: the one refusal.
|
|
56
|
+
brokenPlugin = mkdtempSync(join(tmpdir(), "stigmer-validate-broken-"));
|
|
57
|
+
write(brokenPlugin, ".cursor-plugin/plugin.json", JSON.stringify({ name: "Broken Name", mcpServers: "./mcp.json" }));
|
|
58
|
+
write(brokenPlugin, "mcp.json", JSON.stringify({ mcpServers: { crm: { type: "http", url: "${CRM_URL}", auth: { scopes: ["a"] } } } }));
|
|
34
59
|
});
|
|
35
60
|
|
|
36
61
|
afterAll(() => {
|
|
37
|
-
|
|
62
|
+
for (const path of [dir, plugin, brokenPlugin]) rmSync(path, { recursive: true, force: true });
|
|
38
63
|
});
|
|
39
64
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
interface Run {
|
|
66
|
+
readonly exitCode: number;
|
|
67
|
+
readonly stdout: string;
|
|
68
|
+
readonly stderr: string;
|
|
69
|
+
readonly error: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Runs `validate <args>` with output captured, returning the classified exit
|
|
73
|
+
// code (0 on success) and what reached each stream.
|
|
74
|
+
async function runValidate(...args: string[]): Promise<Run> {
|
|
43
75
|
const program = buildProgram();
|
|
44
76
|
program.exitOverride();
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let stderr = "";
|
|
79
|
+
const outSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
|
80
|
+
stdout += String(chunk);
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
const errSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
84
|
+
stderr += String(chunk);
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
47
87
|
try {
|
|
48
88
|
await program.parseAsync(["node", "stigmer", "validate", ...args]);
|
|
49
|
-
return ExitCode.Success;
|
|
89
|
+
return { exitCode: ExitCode.Success, stdout, stderr, error: undefined };
|
|
50
90
|
} catch (err) {
|
|
51
|
-
return classify(err)?.exitCode ?? -1;
|
|
91
|
+
return { exitCode: classify(err)?.exitCode ?? -1, stdout, stderr, error: err };
|
|
52
92
|
} finally {
|
|
53
93
|
outSpy.mockRestore();
|
|
54
94
|
errSpy.mockRestore();
|
|
55
95
|
}
|
|
56
96
|
}
|
|
57
97
|
|
|
58
|
-
describe("validate exit codes", () => {
|
|
98
|
+
describe("validate exit codes (YAML)", () => {
|
|
59
99
|
it("exits 0 for a valid resource", async () => {
|
|
60
|
-
expect(await runValidate("-f", join(dir, "valid.yaml"))).toBe(ExitCode.Success);
|
|
100
|
+
expect((await runValidate("-f", join(dir, "valid.yaml"))).exitCode).toBe(ExitCode.Success);
|
|
61
101
|
});
|
|
62
102
|
|
|
63
|
-
it("
|
|
64
|
-
expect(await runValidate("-f", dir)).not.toBe(ExitCode.Success);
|
|
65
|
-
// The dir also contains invalid files; a single bad file fails the batch.
|
|
103
|
+
it("fails the batch when a directory holds an invalid file", async () => {
|
|
104
|
+
expect((await runValidate("-f", dir)).exitCode).not.toBe(ExitCode.Success);
|
|
66
105
|
});
|
|
67
106
|
|
|
68
107
|
it("exits 2 for a schema-invalid resource", async () => {
|
|
69
|
-
expect(await runValidate("-f", join(dir, "bad-schema.yaml"))).toBe(ExitCode.Usage);
|
|
108
|
+
expect((await runValidate("-f", join(dir, "bad-schema.yaml"))).exitCode).toBe(ExitCode.Usage);
|
|
70
109
|
});
|
|
71
110
|
|
|
72
111
|
it("exits 2 for an unknown resource kind", async () => {
|
|
73
|
-
expect(await runValidate("-f", join(dir, "unknown.yaml"))).toBe(ExitCode.Usage);
|
|
112
|
+
expect((await runValidate("-f", join(dir, "unknown.yaml"))).exitCode).toBe(ExitCode.Usage);
|
|
74
113
|
});
|
|
75
114
|
|
|
76
115
|
it("exits 2 for a path that does not exist", async () => {
|
|
77
|
-
expect(await runValidate("-f", join(dir, "missing.yaml"))).toBe(ExitCode.Usage);
|
|
116
|
+
expect((await runValidate("-f", join(dir, "missing.yaml"))).exitCode).toBe(ExitCode.Usage);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("validate on a plugin directory", () => {
|
|
121
|
+
it("accepts the plugin with a warning status, exit 0, everything on stderr", async () => {
|
|
122
|
+
const run = await runValidate("-f", plugin);
|
|
123
|
+
expect(run.exitCode).toBe(ExitCode.Success);
|
|
124
|
+
expect(run.stdout).toBe("");
|
|
125
|
+
expect(run.stderr).toContain("Plugin 'github' is valid with 1 warning");
|
|
126
|
+
expect(run.stderr).toContain("Cursor plugin");
|
|
127
|
+
// Section keys are padded to the widest key, so match key and value loosely.
|
|
128
|
+
expect(run.stderr).toMatch(/MCP servers\s+1: github \(http\)/);
|
|
129
|
+
expect(run.stderr).toMatch(/Variables\s+1: GITHUB_TOKEN \(inferred\)/);
|
|
130
|
+
expect(run.stderr).toMatch(/Files\s+4 read, 0 excluded by ignore rules/);
|
|
131
|
+
expect(run.stderr).toMatch(/Stigmer overlay\s+agent/);
|
|
132
|
+
expect(run.stderr).toContain("variable 'GITHUB_TOKEN' is referenced by MCP server 'github' but not declared");
|
|
133
|
+
expect(run.stderr).toContain("- logo (.cursor-plugin/plugin.json#logo)");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("emits the normalised package as JSON data on stdout with --json", async () => {
|
|
137
|
+
const run = await runValidate("-f", plugin, "--json");
|
|
138
|
+
expect(run.exitCode).toBe(ExitCode.Success);
|
|
139
|
+
expect(run.stderr).toBe("");
|
|
140
|
+
const parsed = JSON.parse(run.stdout);
|
|
141
|
+
expect(parsed.status).toBe("warning");
|
|
142
|
+
expect(parsed.data.plugin).toMatchObject({
|
|
143
|
+
name: "github",
|
|
144
|
+
dialect: "cursor",
|
|
145
|
+
mcpServers: [{ name: "github", transport: "http", env: ["GITHUB_TOKEN"] }],
|
|
146
|
+
variables: [{ name: "GITHUB_TOKEN", isSecret: true, optional: false, declaredBy: "inferred" }],
|
|
147
|
+
overlay: { agent: "ai.stigmer/agent.yaml", workflows: [], mcpServers: [] },
|
|
148
|
+
});
|
|
149
|
+
expect(parsed.data.warnings.map((w: { kind: string }) => w.kind)).toEqual(["variable-inferred"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("prints only the status line with --quiet", async () => {
|
|
153
|
+
const run = await runValidate("-f", plugin, "--quiet");
|
|
154
|
+
expect(run.stderr.trim()).toBe("⚠ Plugin 'github' is valid with 1 warning");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("refuses with exit 2 and every problem's sentence", async () => {
|
|
158
|
+
const run = await runValidate("-f", brokenPlugin);
|
|
159
|
+
expect(run.exitCode).toBe(ExitCode.Usage);
|
|
160
|
+
const message = (run.error as Error).message;
|
|
161
|
+
expect(message).toContain("plugin cannot be installed, 2 problems found:");
|
|
162
|
+
expect(message).toContain("plugin name 'Broken Name' in '.cursor-plugin/plugin.json' is invalid");
|
|
163
|
+
expect(message).toContain("MCP server 'crm' in 'mcp.json' has a variable in its 'url'");
|
|
164
|
+
// Warnings ride along under the refusal so one run says everything.
|
|
165
|
+
expect(message).toContain("and 1 warning:");
|
|
166
|
+
expect(message).toContain("MCP server 'crm' in 'mcp.json' has an 'auth' block Stigmer does not read");
|
|
78
167
|
});
|
|
79
168
|
});
|
package/src/commands/validate.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
// `stigmer validate -f <file|dir>` — offline validation of resource YAML
|
|
1
|
+
// `stigmer validate -f <file|dir>` — offline validation of resource YAML, or
|
|
2
|
+
// of a plugin directory.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Two inputs, one verb. A YAML file or a directory of YAML files is parsed
|
|
5
|
+
// document by document into its proto schema (structural validation, no
|
|
6
|
+
// server). A directory holding a plugin manifest (`plugin.json`,
|
|
7
|
+
// `.cursor-plugin/plugin.json`, `.claude-plugin/plugin.json` or
|
|
8
|
+
// `.codex-plugin/plugin.json`) is read with `@stigmer/plugin-package`, the
|
|
9
|
+
// same reader the server installs with, so what validates here installs
|
|
10
|
+
// there and what is refused here is refused there with the same sentence.
|
|
11
|
+
// The plugin check runs BEFORE the YAML walk: a plugin's `ai.stigmer/*.yaml`
|
|
12
|
+
// documents would otherwise be validated as loose resources.
|
|
13
|
+
//
|
|
14
|
+
// A plugin refusal is a `UsageError` (exit 2) listing every problem at once;
|
|
15
|
+
// an accepted plugin renders as `success`, or `warning` when the reader had
|
|
16
|
+
// something to say, both exit 0. `--json` carries the normalised package as
|
|
17
|
+
// the result's `data`.
|
|
6
18
|
|
|
7
19
|
import type { Command } from "commander";
|
|
8
20
|
import { UsageError } from "../errors/index.js";
|
|
9
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
CommandResult,
|
|
23
|
+
type OutputFlags,
|
|
24
|
+
renderResult,
|
|
25
|
+
} from "../output/index.js";
|
|
10
26
|
import { defaultRegistry, Verb } from "../registry/index.js";
|
|
11
27
|
import { loadDocuments, resolveYamlFiles } from "../resources/documents.js";
|
|
12
28
|
import { addResultFlags, resultFormat } from "./shared.js";
|
|
@@ -18,8 +34,11 @@ interface ValidateFlags extends OutputFlags {
|
|
|
18
34
|
export function registerValidate(program: Command): void {
|
|
19
35
|
const validate = program
|
|
20
36
|
.command("validate")
|
|
21
|
-
.description("validate resource YAML files offline")
|
|
22
|
-
.requiredOption(
|
|
37
|
+
.description("validate resource YAML files or a plugin directory offline")
|
|
38
|
+
.requiredOption(
|
|
39
|
+
"-f, --file <path>",
|
|
40
|
+
"path to a YAML file, a directory of YAML files, or a plugin directory",
|
|
41
|
+
)
|
|
23
42
|
.action(async (options: ValidateFlags) => {
|
|
24
43
|
renderResult(await runValidate(options), resultFormat(options));
|
|
25
44
|
});
|
|
@@ -32,12 +51,18 @@ async function runValidate(options: ValidateFlags): Promise<CommandResult> {
|
|
|
32
51
|
throw new UsageError("file path is required: use -f <file>");
|
|
33
52
|
}
|
|
34
53
|
|
|
54
|
+
const { isPluginDirectory } = await import("../resources/plugin.js");
|
|
55
|
+
if (isPluginDirectory(path)) {
|
|
56
|
+
return runValidatePlugin(path);
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
const files = resolveYamlFiles(path);
|
|
36
60
|
if (files.length === 0) {
|
|
37
61
|
throw new UsageError(`no YAML files found in ${path}`);
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
const { schemaForValidate, validateDocument } =
|
|
64
|
+
const { schemaForValidate, validateDocument } =
|
|
65
|
+
await import("../resources/validate.js");
|
|
41
66
|
const registry = defaultRegistry();
|
|
42
67
|
const validated: string[] = [];
|
|
43
68
|
|
|
@@ -49,25 +74,56 @@ async function runValidate(options: ValidateFlags): Promise<CommandResult> {
|
|
|
49
74
|
throw new UsageError(`unknown resource kind '${kind}' in ${file}`);
|
|
50
75
|
}
|
|
51
76
|
if (!info.supportedVerbs.has(Verb.Validate)) {
|
|
52
|
-
throw new UsageError(
|
|
77
|
+
throw new UsageError(
|
|
78
|
+
`${info.displayName} does not support validation (in ${file})`,
|
|
79
|
+
);
|
|
53
80
|
}
|
|
54
81
|
const schema = schemaForValidate(info.kind);
|
|
55
82
|
if (schema === undefined) {
|
|
56
83
|
// The kind declares validate support but the CLI lacks a schema binding
|
|
57
84
|
// — a CLI gap, not bad user input, so this is a general (exit 1) error.
|
|
58
|
-
throw new Error(
|
|
85
|
+
throw new Error(
|
|
86
|
+
`validation is not implemented for ${info.displayName}`,
|
|
87
|
+
);
|
|
59
88
|
}
|
|
60
89
|
try {
|
|
61
90
|
validateDocument(schema, document);
|
|
62
91
|
} catch (err) {
|
|
63
|
-
throw new UsageError(
|
|
92
|
+
throw new UsageError(
|
|
93
|
+
`${file}: invalid ${info.displayName}: ${(err as Error).message}`,
|
|
94
|
+
);
|
|
64
95
|
}
|
|
65
96
|
validated.push(`${file}: ${info.displayName} is valid`);
|
|
66
97
|
}
|
|
67
98
|
}
|
|
68
99
|
|
|
69
|
-
const result = CommandResult.success(
|
|
100
|
+
const result = CommandResult.success(
|
|
101
|
+
`Validation complete: ${validated.length} resource(s) valid`,
|
|
102
|
+
);
|
|
70
103
|
const section = result.addSection("");
|
|
71
104
|
for (const item of validated) section.item(item);
|
|
72
105
|
return result;
|
|
73
106
|
}
|
|
107
|
+
|
|
108
|
+
async function runValidatePlugin(dir: string): Promise<CommandResult> {
|
|
109
|
+
const { readPluginPackage } = await import("@stigmer/plugin-package");
|
|
110
|
+
const { count, describePackageOn, pluginRefusal, readPluginDirectory } =
|
|
111
|
+
await import("../resources/plugin.js");
|
|
112
|
+
|
|
113
|
+
const directory = readPluginDirectory(dir);
|
|
114
|
+
const outcome = readPluginPackage(directory.files);
|
|
115
|
+
if (!outcome.ok) {
|
|
116
|
+
// Warnings ride along under the refusal so the author fixes everything
|
|
117
|
+
// in one pass, not the errors now and the warnings on the next run.
|
|
118
|
+
throw pluginRefusal(dir, outcome.errors, outcome.warnings);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { plugin, warnings } = outcome;
|
|
122
|
+
const result =
|
|
123
|
+
warnings.length === 0
|
|
124
|
+
? CommandResult.success(`Plugin '${plugin.name}' is valid`)
|
|
125
|
+
: CommandResult.warning(
|
|
126
|
+
`Plugin '${plugin.name}' is valid with ${count(warnings.length, "warning")}`,
|
|
127
|
+
);
|
|
128
|
+
return describePackageOn(result, plugin, warnings, directory.stats);
|
|
129
|
+
}
|
|
@@ -32,6 +32,15 @@ describe("resultToJson", () => {
|
|
|
32
32
|
expect(parsed.sections[1]).toEqual({ title: "Items", items: ["first", "second"] });
|
|
33
33
|
expect(parsed.hints).toEqual(["try this", "and that"]);
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
it("emits the data payload in JSON only, and omits it when unset", () => {
|
|
37
|
+
const result = CommandResult.warning("Plugin is valid").withData({ plugin: { name: "x" }, warnings: [] });
|
|
38
|
+
const parsed = JSON.parse(resultToJson(result));
|
|
39
|
+
expect(parsed.data).toEqual({ plugin: { name: "x" }, warnings: [] });
|
|
40
|
+
expect(JSON.parse(resultToJson(CommandResult.success("ok")))).not.toHaveProperty("data");
|
|
41
|
+
expect(resultToHuman(result, false)).not.toContain("plugin");
|
|
42
|
+
expect(resultToQuiet(result, false)).toBe("⚠ Plugin is valid\n");
|
|
43
|
+
});
|
|
35
44
|
});
|
|
36
45
|
|
|
37
46
|
describe("resultToHuman (plain, no color)", () => {
|
|
@@ -43,6 +43,13 @@ export class CommandResult {
|
|
|
43
43
|
readonly message: string;
|
|
44
44
|
readonly sections: Section[] = [];
|
|
45
45
|
readonly hints: string[] = [];
|
|
46
|
+
/**
|
|
47
|
+
* A machine-readable payload for the JSON form only: the thing the command
|
|
48
|
+
* computed, in full, where the sections carry its human summary (a
|
|
49
|
+
* validated plugin's normalised description, for example). Human and quiet
|
|
50
|
+
* output never render it; `undefined` omits it from the JSON.
|
|
51
|
+
*/
|
|
52
|
+
data: unknown = undefined;
|
|
46
53
|
|
|
47
54
|
private constructor(status: ResultStatus, message: string) {
|
|
48
55
|
this.status = status;
|
|
@@ -71,6 +78,11 @@ export class CommandResult {
|
|
|
71
78
|
this.hints.push(text);
|
|
72
79
|
return this;
|
|
73
80
|
}
|
|
81
|
+
|
|
82
|
+
withData(data: unknown): this {
|
|
83
|
+
this.data = data;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
const ICONS: Record<ResultStatus, string> = {
|
|
@@ -84,6 +96,7 @@ interface JsonResult {
|
|
|
84
96
|
message: string;
|
|
85
97
|
sections?: JsonSection[];
|
|
86
98
|
hints?: string[];
|
|
99
|
+
data?: unknown;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
interface JsonSection {
|
|
@@ -109,6 +122,7 @@ export function resultToJson(result: CommandResult): string {
|
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
if (result.hints.length > 0) out.hints = [...result.hints];
|
|
125
|
+
if (result.data !== undefined) out.data = result.data;
|
|
112
126
|
|
|
113
127
|
return JSON.stringify(out, null, 2) + "\n";
|
|
114
128
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
generateAliases,
|
|
4
|
+
normalizeAlias,
|
|
5
|
+
pluralize,
|
|
6
|
+
toKebabCase,
|
|
7
|
+
toSnakeCase,
|
|
8
|
+
} from "./aliases.js";
|
|
3
9
|
|
|
4
10
|
function normalized(aliases: string[]): Set<string> {
|
|
5
11
|
return new Set(aliases.map(normalizeAlias));
|
|
@@ -7,7 +13,9 @@ function normalized(aliases: string[]): Set<string> {
|
|
|
7
13
|
|
|
8
14
|
describe("generateAliases", () => {
|
|
9
15
|
it("derives the full McpServer alias set", () => {
|
|
10
|
-
const found = normalized(
|
|
16
|
+
const found = normalized(
|
|
17
|
+
generateAliases("McpServer", "MCP Server", "mcp", "mcp_server"),
|
|
18
|
+
);
|
|
11
19
|
for (const exp of [
|
|
12
20
|
"mcpserver",
|
|
13
21
|
"mcp-server",
|
|
@@ -30,7 +38,9 @@ describe("generateAliases", () => {
|
|
|
30
38
|
});
|
|
31
39
|
|
|
32
40
|
it("derives the Workflow alias set", () => {
|
|
33
|
-
const found = normalized(
|
|
41
|
+
const found = normalized(
|
|
42
|
+
generateAliases("Workflow", "Workflow", "wfl", "workflow"),
|
|
43
|
+
);
|
|
34
44
|
for (const exp of ["workflow", "wfl", "workflows", "wfls"]) {
|
|
35
45
|
expect(found).toContain(exp);
|
|
36
46
|
}
|
|
@@ -42,7 +52,9 @@ describe("generateAliases", () => {
|
|
|
42
52
|
// enum name (oauth_app). Both families must be accepted: the canonical
|
|
43
53
|
// spellings via protoName (stigmer/stigmer#470), and the historical
|
|
44
54
|
// split-derived spellings for backward compatibility.
|
|
45
|
-
const found = normalized(
|
|
55
|
+
const found = normalized(
|
|
56
|
+
generateAliases("OAuthApp", "OAuth App", "oapp", "oauth_app"),
|
|
57
|
+
);
|
|
46
58
|
for (const exp of [
|
|
47
59
|
"oauth_app",
|
|
48
60
|
"oauth-app",
|
|
@@ -61,7 +73,14 @@ describe("generateAliases", () => {
|
|
|
61
73
|
|
|
62
74
|
it("does not let a multi-word display name steal the parent's name", () => {
|
|
63
75
|
// "Agent Instance" must NOT register "agent" (that belongs to Agent).
|
|
64
|
-
const found = normalized(
|
|
76
|
+
const found = normalized(
|
|
77
|
+
generateAliases(
|
|
78
|
+
"AgentInstance",
|
|
79
|
+
"Agent Instance",
|
|
80
|
+
"ain",
|
|
81
|
+
"agent_instance",
|
|
82
|
+
),
|
|
83
|
+
);
|
|
65
84
|
expect(found).not.toContain("agent");
|
|
66
85
|
expect(found).toContain("agentinstance");
|
|
67
86
|
expect(found).toContain("agent-instance");
|
package/src/registry/aliases.ts
CHANGED
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
* "OAuthApp" it contributes "oauth_app"/"oauth-app"
|
|
16
16
|
* Plus the plural of every form above.
|
|
17
17
|
*/
|
|
18
|
-
export function generateAliases(
|
|
18
|
+
export function generateAliases(
|
|
19
|
+
name: string,
|
|
20
|
+
displayName: string,
|
|
21
|
+
idPrefix: string,
|
|
22
|
+
protoName: string,
|
|
23
|
+
): string[] {
|
|
19
24
|
const seen = new Set<string>();
|
|
20
25
|
const aliases: string[] = [];
|
|
21
26
|
|
package/src/registry/index.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
// Public surface of the type-registry module.
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
generateAliases,
|
|
5
|
+
normalizeAlias,
|
|
6
|
+
pluralize,
|
|
7
|
+
toKebabCase,
|
|
8
|
+
toSnakeCase,
|
|
9
|
+
} from "./aliases.js";
|
|
4
10
|
export { CLI_RELEVANT_KINDS, type KindMeta, KIND_META } from "./metadata.js";
|
|
5
|
-
export {
|
|
6
|
-
|
|
11
|
+
export {
|
|
12
|
+
defaultRegistry,
|
|
13
|
+
type Registry,
|
|
14
|
+
supportsVerb,
|
|
15
|
+
type TypeInfo,
|
|
16
|
+
} from "./registry.js";
|
|
17
|
+
export {
|
|
18
|
+
ALL_VERBS,
|
|
19
|
+
isFileBasedVerb,
|
|
20
|
+
isReferenceBasedVerb,
|
|
21
|
+
Verb,
|
|
22
|
+
} from "./verbs.js";
|
|
7
23
|
export { VERB_SUPPORT, verbsForKind } from "./verb-support.js";
|
package/src/registry/metadata.ts
CHANGED
|
@@ -22,24 +22,90 @@ export interface KindMeta {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const KIND_META: ReadonlyMap<ApiResourceKind, KindMeta> = new Map([
|
|
25
|
-
[
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
[
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[
|
|
42
|
-
|
|
25
|
+
[
|
|
26
|
+
ApiResourceKind.organization,
|
|
27
|
+
{ name: "Organization", displayName: "Organization", idPrefix: "org" },
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
ApiResourceKind.agent,
|
|
31
|
+
{ name: "Agent", displayName: "Agent", idPrefix: "agt" },
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
ApiResourceKind.workflow,
|
|
35
|
+
{ name: "Workflow", displayName: "Workflow", idPrefix: "wfl" },
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
ApiResourceKind.skill,
|
|
39
|
+
{ name: "Skill", displayName: "Skill", idPrefix: "skl" },
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
ApiResourceKind.plugin,
|
|
43
|
+
{ name: "Plugin", displayName: "Plugin", idPrefix: "plg" },
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
ApiResourceKind.mcp_server,
|
|
47
|
+
{ name: "McpServer", displayName: "MCP Server", idPrefix: "mcp" },
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
ApiResourceKind.project,
|
|
51
|
+
{ name: "Project", displayName: "Project", idPrefix: "prj" },
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
ApiResourceKind.api_key,
|
|
55
|
+
{ name: "ApiKey", displayName: "API Key", idPrefix: "key" },
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
ApiResourceKind.identity_provider,
|
|
59
|
+
{
|
|
60
|
+
name: "IdentityProvider",
|
|
61
|
+
displayName: "Identity Provider",
|
|
62
|
+
idPrefix: "idp",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
ApiResourceKind.oauth_app,
|
|
67
|
+
{ name: "OAuthApp", displayName: "OAuth App", idPrefix: "oapp" },
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
ApiResourceKind.environment,
|
|
71
|
+
{ name: "Environment", displayName: "Environment", idPrefix: "env" },
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
ApiResourceKind.agent_share,
|
|
75
|
+
{ name: "AgentShare", displayName: "Agent Share", idPrefix: "ash" },
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
ApiResourceKind.agent_channel,
|
|
79
|
+
{ name: "AgentChannel", displayName: "Agent Channel", idPrefix: "ach" },
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
ApiResourceKind.channel_app,
|
|
83
|
+
{ name: "ChannelApp", displayName: "Channel App", idPrefix: "chapp" },
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
ApiResourceKind.schedule,
|
|
87
|
+
{ name: "Schedule", displayName: "Schedule", idPrefix: "sch" },
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
ApiResourceKind.agent_instance,
|
|
91
|
+
{ name: "AgentInstance", displayName: "Agent Instance", idPrefix: "ain" },
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
ApiResourceKind.workflow_instance,
|
|
95
|
+
{
|
|
96
|
+
name: "WorkflowInstance",
|
|
97
|
+
displayName: "Workflow Instance",
|
|
98
|
+
idPrefix: "win",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
ApiResourceKind.session,
|
|
103
|
+
{ name: "Session", displayName: "Session", idPrefix: "ses" },
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
ApiResourceKind.agent_execution,
|
|
107
|
+
{ name: "AgentExecution", displayName: "Agent Execution", idPrefix: "aex" },
|
|
108
|
+
],
|
|
43
109
|
]);
|
|
44
110
|
|
|
45
111
|
// Kinds that are user-facing in the CLI and therefore registered as addressable
|
|
@@ -54,6 +120,7 @@ export const CLI_RELEVANT_KINDS: readonly ApiResourceKind[] = [
|
|
|
54
120
|
ApiResourceKind.agent,
|
|
55
121
|
ApiResourceKind.workflow,
|
|
56
122
|
ApiResourceKind.skill,
|
|
123
|
+
ApiResourceKind.plugin,
|
|
57
124
|
ApiResourceKind.mcp_server,
|
|
58
125
|
ApiResourceKind.project,
|
|
59
126
|
ApiResourceKind.api_key,
|