@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.
Files changed (65) hide show
  1. package/README.md +3 -4
  2. package/commands/apply.d.ts.map +1 -1
  3. package/commands/apply.js +56 -4
  4. package/commands/apply.js.map +1 -1
  5. package/commands/push.d.ts.map +1 -1
  6. package/commands/push.js +122 -16
  7. package/commands/push.js.map +1 -1
  8. package/commands/validate.d.ts.map +1 -1
  9. package/commands/validate.js +39 -7
  10. package/commands/validate.js.map +1 -1
  11. package/output/command-result.d.ts +8 -0
  12. package/output/command-result.d.ts.map +1 -1
  13. package/output/command-result.js +13 -0
  14. package/output/command-result.js.map +1 -1
  15. package/package.json +6 -5
  16. package/registry/aliases.d.ts.map +1 -1
  17. package/registry/aliases.js.map +1 -1
  18. package/registry/index.d.ts +3 -3
  19. package/registry/index.d.ts.map +1 -1
  20. package/registry/index.js +3 -3
  21. package/registry/index.js.map +1 -1
  22. package/registry/metadata.d.ts.map +1 -1
  23. package/registry/metadata.js +85 -18
  24. package/registry/metadata.js.map +1 -1
  25. package/registry/registry.d.ts.map +1 -1
  26. package/registry/registry.js.map +1 -1
  27. package/registry/verb-support.d.ts.map +1 -1
  28. package/registry/verb-support.js +81 -13
  29. package/registry/verb-support.js.map +1 -1
  30. package/resources/delete.d.ts.map +1 -1
  31. package/resources/delete.js +35 -9
  32. package/resources/delete.js.map +1 -1
  33. package/resources/get-bindings.d.ts.map +1 -1
  34. package/resources/get-bindings.js +56 -13
  35. package/resources/get-bindings.js.map +1 -1
  36. package/resources/list.d.ts.map +1 -1
  37. package/resources/list.js +62 -14
  38. package/resources/list.js.map +1 -1
  39. package/resources/plugin.d.ts +80 -0
  40. package/resources/plugin.d.ts.map +1 -0
  41. package/resources/plugin.js +305 -0
  42. package/resources/plugin.js.map +1 -0
  43. package/resources/skill.d.ts +1 -0
  44. package/resources/skill.d.ts.map +1 -1
  45. package/resources/skill.js +1 -1
  46. package/resources/skill.js.map +1 -1
  47. package/src/commands/apply.ts +133 -21
  48. package/src/commands/push.ts +270 -41
  49. package/src/commands/validate.test.ts +111 -22
  50. package/src/commands/validate.ts +68 -12
  51. package/src/output/command-result.test.ts +9 -0
  52. package/src/output/command-result.ts +14 -0
  53. package/src/registry/aliases.test.ts +24 -5
  54. package/src/registry/aliases.ts +6 -1
  55. package/src/registry/index.ts +19 -3
  56. package/src/registry/metadata.ts +85 -18
  57. package/src/registry/registry.test.ts +86 -37
  58. package/src/registry/registry.ts +8 -2
  59. package/src/registry/verb-support.ts +85 -14
  60. package/src/resources/delete.ts +106 -26
  61. package/src/resources/get-bindings.ts +95 -17
  62. package/src/resources/list.ts +102 -28
  63. package/src/resources/plugin.test.ts +233 -0
  64. package/src/resources/plugin.ts +432 -0
  65. package/src/resources/skill.ts +1 -1
@@ -1,10 +1,13 @@
1
- // Command-level exit-code contract for `validate`. Drives the real commander
2
- // program against temp YAML files and asserts the classified exit code. The
3
- // contract: 0 = valid, 2 = invalid input (bad path, bad YAML, unknown kind,
4
- // schema failure), 1 = unexpected. This is a deliberate, documented refinement
5
- // over the Go CLI, which returns a generic exit 1 for all validate failures.
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
- rmSync(dir, { recursive: true, force: true });
62
+ for (const path of [dir, plugin, brokenPlugin]) rmSync(path, { recursive: true, force: true });
38
63
  });
39
64
 
40
- // Runs `validate <args>` with output suppressed, returning the classified exit
41
- // code (0 on success).
42
- async function runValidate(...args: string[]): Promise<number> {
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
- const outSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true);
46
- const errSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true);
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("exits 0 when validating a directory of valid resources", async () => {
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
  });
@@ -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
- // Auto-detects each document's kind, checks it supports validation, and parses
4
- // it into its proto schema (structural validation, no server). Supports single
5
- // files, directories, and multi-document YAML.
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 { CommandResult, type OutputFlags, renderResult } from "../output/index.js";
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("-f, --file <path>", "path to a YAML file or directory")
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 } = await import("../resources/validate.js");
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(`${info.displayName} does not support validation (in ${file})`);
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(`validation is not implemented for ${info.displayName}`);
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(`${file}: invalid ${info.displayName}: ${(err as Error).message}`);
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(`Validation complete: ${validated.length} resource(s) valid`);
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 { generateAliases, normalizeAlias, pluralize, toKebabCase, toSnakeCase } from "./aliases.js";
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(generateAliases("McpServer", "MCP Server", "mcp", "mcp_server"));
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(generateAliases("Workflow", "Workflow", "wfl", "workflow"));
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(generateAliases("OAuthApp", "OAuth App", "oapp", "oauth_app"));
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(generateAliases("AgentInstance", "Agent Instance", "ain", "agent_instance"));
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");
@@ -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(name: string, displayName: string, idPrefix: string, protoName: string): string[] {
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
 
@@ -1,7 +1,23 @@
1
1
  // Public surface of the type-registry module.
2
2
 
3
- export { generateAliases, normalizeAlias, pluralize, toKebabCase, toSnakeCase } from "./aliases.js";
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 { defaultRegistry, type Registry, supportsVerb, type TypeInfo } from "./registry.js";
6
- export { ALL_VERBS, isFileBasedVerb, isReferenceBasedVerb, Verb } from "./verbs.js";
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";
@@ -22,24 +22,90 @@ export interface KindMeta {
22
22
  }
23
23
 
24
24
  export const KIND_META: ReadonlyMap<ApiResourceKind, KindMeta> = new Map([
25
- [ApiResourceKind.organization, { name: "Organization", displayName: "Organization", idPrefix: "org" }],
26
- [ApiResourceKind.agent, { name: "Agent", displayName: "Agent", idPrefix: "agt" }],
27
- [ApiResourceKind.workflow, { name: "Workflow", displayName: "Workflow", idPrefix: "wfl" }],
28
- [ApiResourceKind.skill, { name: "Skill", displayName: "Skill", idPrefix: "skl" }],
29
- [ApiResourceKind.mcp_server, { name: "McpServer", displayName: "MCP Server", idPrefix: "mcp" }],
30
- [ApiResourceKind.project, { name: "Project", displayName: "Project", idPrefix: "prj" }],
31
- [ApiResourceKind.api_key, { name: "ApiKey", displayName: "API Key", idPrefix: "key" }],
32
- [ApiResourceKind.identity_provider, { name: "IdentityProvider", displayName: "Identity Provider", idPrefix: "idp" }],
33
- [ApiResourceKind.oauth_app, { name: "OAuthApp", displayName: "OAuth App", idPrefix: "oapp" }],
34
- [ApiResourceKind.environment, { name: "Environment", displayName: "Environment", idPrefix: "env" }],
35
- [ApiResourceKind.agent_share, { name: "AgentShare", displayName: "Agent Share", idPrefix: "ash" }],
36
- [ApiResourceKind.agent_channel, { name: "AgentChannel", displayName: "Agent Channel", idPrefix: "ach" }],
37
- [ApiResourceKind.channel_app, { name: "ChannelApp", displayName: "Channel App", idPrefix: "chapp" }],
38
- [ApiResourceKind.schedule, { name: "Schedule", displayName: "Schedule", idPrefix: "sch" }],
39
- [ApiResourceKind.agent_instance, { name: "AgentInstance", displayName: "Agent Instance", idPrefix: "ain" }],
40
- [ApiResourceKind.workflow_instance, { name: "WorkflowInstance", displayName: "Workflow Instance", idPrefix: "win" }],
41
- [ApiResourceKind.session, { name: "Session", displayName: "Session", idPrefix: "ses" }],
42
- [ApiResourceKind.agent_execution, { name: "AgentExecution", displayName: "Agent Execution", idPrefix: "aex" }],
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,