@stigmer/cli 3.18.1-dev.20260919070736 → 3.19.1-dev.20260920100552

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 (141) hide show
  1. package/commands/apply.d.ts.map +1 -1
  2. package/commands/apply.js +45 -101
  3. package/commands/apply.js.map +1 -1
  4. package/commands/install.js +2 -1
  5. package/commands/install.js.map +1 -1
  6. package/commands/marketplace.js +3 -3
  7. package/commands/marketplace.js.map +1 -1
  8. package/commands/push.js +3 -1
  9. package/commands/push.js.map +1 -1
  10. package/commands/validate.js +3 -4
  11. package/commands/validate.js.map +1 -1
  12. package/local/bootstrap.d.ts +1 -2
  13. package/local/bootstrap.d.ts.map +1 -1
  14. package/local/bootstrap.js +13 -41
  15. package/local/bootstrap.js.map +1 -1
  16. package/local/daemon/launch.d.ts +2 -2
  17. package/local/daemon/launch.d.ts.map +1 -1
  18. package/local/daemon/launch.js +2 -2
  19. package/local/daemon/launch.js.map +1 -1
  20. package/local/plugins/defaults.js +2 -2
  21. package/local/plugins/defaults.js.map +1 -1
  22. package/marketplace/config.d.ts.map +1 -1
  23. package/marketplace/config.js +13 -9
  24. package/marketplace/config.js.map +1 -1
  25. package/marketplace/render.d.ts.map +1 -1
  26. package/marketplace/render.js +1 -7
  27. package/marketplace/render.js.map +1 -1
  28. package/package.json +7 -7
  29. package/registry/index.d.ts +2 -2
  30. package/registry/index.d.ts.map +1 -1
  31. package/registry/index.js +2 -2
  32. package/registry/index.js.map +1 -1
  33. package/registry/metadata.d.ts +9 -0
  34. package/registry/metadata.d.ts.map +1 -1
  35. package/registry/metadata.js +14 -5
  36. package/registry/metadata.js.map +1 -1
  37. package/registry/registry.d.ts +9 -0
  38. package/registry/registry.d.ts.map +1 -1
  39. package/registry/registry.js +16 -1
  40. package/registry/registry.js.map +1 -1
  41. package/registry/verb-support.d.ts.map +1 -1
  42. package/registry/verb-support.js +0 -13
  43. package/registry/verb-support.js.map +1 -1
  44. package/resources/apply/apply.d.ts +11 -12
  45. package/resources/apply/apply.d.ts.map +1 -1
  46. package/resources/apply/apply.js +12 -21
  47. package/resources/apply/apply.js.map +1 -1
  48. package/resources/apply/handlers.js +2 -2
  49. package/resources/apply/handlers.js.map +1 -1
  50. package/resources/connect/oauth.d.ts.map +1 -1
  51. package/resources/connect/oauth.js +4 -1
  52. package/resources/connect/oauth.js.map +1 -1
  53. package/resources/delete.d.ts.map +1 -1
  54. package/resources/delete.js +0 -1
  55. package/resources/delete.js.map +1 -1
  56. package/resources/get-bindings.d.ts.map +1 -1
  57. package/resources/get-bindings.js +0 -5
  58. package/resources/get-bindings.js.map +1 -1
  59. package/resources/list.d.ts.map +1 -1
  60. package/resources/list.js +0 -1
  61. package/resources/list.js.map +1 -1
  62. package/resources/plugin.d.ts +27 -0
  63. package/resources/plugin.d.ts.map +1 -1
  64. package/resources/plugin.js +113 -8
  65. package/resources/plugin.js.map +1 -1
  66. package/resources/validate.d.ts.map +1 -1
  67. package/resources/validate.js +0 -2
  68. package/resources/validate.js.map +1 -1
  69. package/src/commands/apply.ts +40 -150
  70. package/src/commands/install.test.ts +20 -21
  71. package/src/commands/install.ts +2 -1
  72. package/src/commands/marketplace.test.ts +10 -19
  73. package/src/commands/marketplace.ts +3 -3
  74. package/src/commands/push.ts +6 -1
  75. package/src/commands/validate.ts +2 -4
  76. package/src/local/bootstrap.test.ts +15 -59
  77. package/src/local/bootstrap.ts +12 -53
  78. package/src/local/daemon/launch.ts +4 -4
  79. package/src/local/plugins/defaults.test.ts +28 -28
  80. package/src/local/plugins/defaults.ts +2 -2
  81. package/src/marketplace/__fixtures__/cursor-marketplace.ts +16 -12
  82. package/src/marketplace/config.test.ts +12 -28
  83. package/src/marketplace/config.ts +13 -9
  84. package/src/marketplace/install.test.ts +19 -19
  85. package/src/marketplace/read.test.ts +6 -6
  86. package/src/marketplace/render.ts +1 -10
  87. package/src/registry/index.ts +7 -1
  88. package/src/registry/metadata.ts +15 -5
  89. package/src/registry/registry.test.ts +1 -7
  90. package/src/registry/registry.ts +17 -1
  91. package/src/registry/verb-support.ts +0 -13
  92. package/src/resources/apply/apply.integration.test.ts +15 -3
  93. package/src/resources/apply/apply.ts +15 -26
  94. package/src/resources/apply/handlers.ts +2 -2
  95. package/src/resources/connect/oauth.test.ts +2 -2
  96. package/src/resources/connect/oauth.ts +4 -1
  97. package/src/resources/delete.ts +0 -1
  98. package/src/resources/get-bindings.ts +0 -5
  99. package/src/resources/list.ts +0 -1
  100. package/src/resources/plugin.test.ts +119 -2
  101. package/src/resources/plugin.ts +140 -8
  102. package/src/resources/validate.test.ts +0 -1
  103. package/src/resources/validate.ts +0 -2
  104. package/local/seedpack-retire.d.ts +0 -63
  105. package/local/seedpack-retire.d.ts.map +0 -1
  106. package/local/seedpack-retire.js +0 -361
  107. package/local/seedpack-retire.js.map +0 -1
  108. package/resources/apply/declarative.d.ts +0 -106
  109. package/resources/apply/declarative.d.ts.map +0 -1
  110. package/resources/apply/declarative.js +0 -397
  111. package/resources/apply/declarative.js.map +0 -1
  112. package/resources/apply/synth/project-track.d.ts +0 -21
  113. package/resources/apply/synth/project-track.d.ts.map +0 -1
  114. package/resources/apply/synth/project-track.js +0 -102
  115. package/resources/apply/synth/project-track.js.map +0 -1
  116. package/resources/apply/synth/reader.d.ts +0 -18
  117. package/resources/apply/synth/reader.d.ts.map +0 -1
  118. package/resources/apply/synth/reader.js +0 -55
  119. package/resources/apply/synth/reader.js.map +0 -1
  120. package/resources/apply/synth/runtime.d.ts +0 -8
  121. package/resources/apply/synth/runtime.d.ts.map +0 -1
  122. package/resources/apply/synth/runtime.js +0 -32
  123. package/resources/apply/synth/runtime.js.map +0 -1
  124. package/resources/apply/synth/synthesize.d.ts +0 -43
  125. package/resources/apply/synth/synthesize.d.ts.map +0 -1
  126. package/resources/apply/synth/synthesize.js +0 -115
  127. package/resources/apply/synth/synthesize.js.map +0 -1
  128. package/src/local/seedpack-retire.test.ts +0 -499
  129. package/src/local/seedpack-retire.ts +0 -474
  130. package/src/resources/apply/declarative.integration.test.ts +0 -204
  131. package/src/resources/apply/declarative.test.ts +0 -132
  132. package/src/resources/apply/declarative.ts +0 -491
  133. package/src/resources/apply/synth/capstone.integration.test.ts +0 -134
  134. package/src/resources/apply/synth/project-track.integration.test.ts +0 -194
  135. package/src/resources/apply/synth/project-track.ts +0 -126
  136. package/src/resources/apply/synth/reader.test.ts +0 -90
  137. package/src/resources/apply/synth/reader.ts +0 -68
  138. package/src/resources/apply/synth/runtime.test.ts +0 -29
  139. package/src/resources/apply/synth/runtime.ts +0 -39
  140. package/src/resources/apply/synth/synthesize.test.ts +0 -109
  141. package/src/resources/apply/synth/synthesize.ts +0 -161
@@ -33,9 +33,9 @@ afterAll(() => {
33
33
  describe("prepareEntry", () => {
34
34
  it("prepares an offered entry exactly as push plugin would prepare its directory", async () => {
35
35
  const tree = readMarketplaceTree(root);
36
- const viaMarketplace = await prepareEntry(tree, "github");
37
- const viaFolder = await preparePluginPush(join(root, "third_party", "github"));
38
- expect(viaMarketplace.plugin.name).toBe("github");
36
+ const viaMarketplace = await prepareEntry(tree, "codeforge");
37
+ const viaFolder = await preparePluginPush(join(root, "third_party", "codeforge"));
38
+ expect(viaMarketplace.plugin.name).toBe("codeforge");
39
39
  expect(viaMarketplace.digest).toBe(viaFolder.digest);
40
40
  expect(
41
41
  Buffer.from(viaMarketplace.archive).equals(
@@ -57,15 +57,15 @@ describe("prepareEntry", () => {
57
57
 
58
58
  describe("assertVersion", () => {
59
59
  it("passes without a pin or with the offered version, refuses another naming the offered one", async () => {
60
- const prepared = await prepareEntry(readMarketplaceTree(root), "thermos");
61
- expect(() => assertVersion(prepared, undefined, "thermos")).not.toThrow();
60
+ const prepared = await prepareEntry(readMarketplaceTree(root), "warmer");
61
+ expect(() => assertVersion(prepared, undefined, "warmer")).not.toThrow();
62
62
  expect(() =>
63
- assertVersion(prepared, "1.0.0", "thermos@1.0.0"),
63
+ assertVersion(prepared, "1.0.0", "warmer@1.0.0"),
64
64
  ).not.toThrow();
65
65
  expect(() =>
66
- assertVersion(prepared, "9.9.9", "acme-plugins/thermos@9.9.9"),
66
+ assertVersion(prepared, "9.9.9", "acme-plugins/warmer@9.9.9"),
67
67
  ).toThrow(
68
- /pins version 9\.9\.9, but the marketplace offers 'thermos' at 1\.0\.0\n\nInstall it as 'acme-plugins\/thermos@1\.0\.0'/,
68
+ /pins version 9\.9\.9, but the marketplace offers 'warmer' at 1\.0\.0\n\nInstall it as 'acme-plugins\/warmer@1\.0\.0'/,
69
69
  );
70
70
  });
71
71
  });
@@ -104,10 +104,10 @@ describe("locateEntry", () => {
104
104
  const remoteFiles = {
105
105
  "marketplace.json": JSON.stringify({
106
106
  name: "remote",
107
- plugins: [{ name: "thermos", source: "./thermos" }],
107
+ plugins: [{ name: "warmer", source: "./warmer" }],
108
108
  }),
109
- "thermos/plugin.json": JSON.stringify({
110
- name: "thermos",
109
+ "warmer/plugin.json": JSON.stringify({
110
+ name: "warmer",
111
111
  version: "3.0.0",
112
112
  }),
113
113
  };
@@ -122,7 +122,7 @@ describe("locateEntry", () => {
122
122
  source: { type: "local", path: root } as const,
123
123
  };
124
124
  const located = await locateEntry(
125
- parseInstallRef("acme-plugins/thermos"),
125
+ parseInstallRef("acme-plugins/warmer"),
126
126
  listing(local),
127
127
  );
128
128
  try {
@@ -132,7 +132,7 @@ describe("locateEntry", () => {
132
132
  located.open.dispose();
133
133
  }
134
134
  await expect(
135
- locateEntry(parseInstallRef("nowhere/thermos"), listing(local)),
135
+ locateEntry(parseInstallRef("nowhere/warmer"), listing(local)),
136
136
  ).rejects.toThrow(/no marketplace named 'nowhere' is configured/);
137
137
  });
138
138
 
@@ -142,7 +142,7 @@ describe("locateEntry", () => {
142
142
  source: { type: "local", path: root } as const,
143
143
  };
144
144
  const located = await locateEntry(
145
- parseInstallRef("github"),
145
+ parseInstallRef("codeforge"),
146
146
  listing(local),
147
147
  );
148
148
  try {
@@ -178,11 +178,11 @@ describe("locateEntry", () => {
178
178
  };
179
179
  const before = countTempTrees();
180
180
  await expect(
181
- locateEntry(parseInstallRef("thermos"), listing(local, remote), {
181
+ locateEntry(parseInstallRef("warmer"), listing(local, remote), {
182
182
  fetchImpl: servingZipOf(remoteFiles),
183
183
  }),
184
184
  ).rejects.toThrow(
185
- /offered by more than one marketplace: acme-plugins, remote\n\nName the one you mean: acme-plugins\/thermos or remote\/thermos/,
185
+ /offered by more than one marketplace: acme-plugins, remote\n\nName the one you mean: acme-plugins\/warmer or remote\/warmer/,
186
186
  );
187
187
  expect(countTempTrees()).toBe(before);
188
188
  });
@@ -197,8 +197,8 @@ describe("locateEntry", () => {
197
197
  source: { type: "github", repo: "a/b" } as const,
198
198
  };
199
199
  const requests: string[] = [];
200
- // `github` is offered by the local fixture alone: the remote is peeked, not downloaded.
201
- const located = await locateEntry(parseInstallRef("github"), listing(local, remote), {
200
+ // `codeforge` is offered by the local fixture alone: the remote is peeked, not downloaded.
201
+ const located = await locateEntry(parseInstallRef("codeforge"), listing(local, remote), {
202
202
  fetchImpl: servingZipOf(remoteFiles, "r-HEAD", requests),
203
203
  });
204
204
  located.open.dispose();
@@ -241,7 +241,7 @@ describe("locateEntry", () => {
241
241
  source: { type: "github", repo: "a/b" } as const,
242
242
  };
243
243
  const located = await locateEntry(
244
- parseInstallRef("remote/thermos"),
244
+ parseInstallRef("remote/warmer"),
245
245
  listing(remote),
246
246
  {
247
247
  fetchImpl: servingZipOf(remoteFiles),
@@ -27,8 +27,8 @@ describe("readMarketplaceTree", () => {
27
27
  expect(tree.marketplace.name).toBe("acme-plugins");
28
28
  expect(tree.marketplace.dialect).toBe("cursor");
29
29
  expect(tree.marketplace.plugins.map((entry) => entry.name)).toEqual([
30
- "thermos",
31
- "github",
30
+ "warmer",
31
+ "codeforge",
32
32
  ]);
33
33
  expect(tree.warnings).toHaveLength(1);
34
34
  expect(tree.warnings[0]?.kind).toBe("entry-directory-missing");
@@ -71,10 +71,10 @@ describe("readMarketplaceTree", () => {
71
71
  describe("findEntry and entryDirectory", () => {
72
72
  it("resolves an offered entry to its absolute directory and an unknown name to undefined", () => {
73
73
  const tree = readMarketplaceTree(root);
74
- const github = findEntry(tree.marketplace, "github");
75
- expect(github?.dir).toBe("third_party/github");
76
- expect(entryDirectory(tree, github!)).toBe(
77
- join(root, "third_party", "github"),
74
+ const codeforge = findEntry(tree.marketplace, "codeforge");
75
+ expect(codeforge?.dir).toBe("third_party/codeforge");
76
+ expect(entryDirectory(tree, codeforge!)).toBe(
77
+ join(root, "third_party", "codeforge"),
78
78
  );
79
79
  expect(findEntry(tree.marketplace, "ghost")).toBeUndefined();
80
80
  });
@@ -5,7 +5,6 @@
5
5
  // same kind of thing: client-side configuration, not a server collection.
6
6
 
7
7
  import { readPluginPackage } from "@stigmer/plugin-package";
8
- import { isBuiltInMarketplaceName } from "@stigmer/plugin-package/client";
9
8
  import { CommandResult } from "../output/index.js";
10
9
  import { count, readPluginDirectory } from "../resources/plugin.js";
11
10
  import {
@@ -27,15 +26,7 @@ export function renderMarketplaceList(
27
26
  );
28
27
  const section = result.addSection("Sources");
29
28
  for (const marketplace of listing.known) {
30
- // A built-in reads "built in" beside where it comes from, so the user
31
- // knows which names `remove` will refuse.
32
- section.field(
33
- marketplace.name,
34
- isBuiltInMarketplaceName(marketplace.name) &&
35
- marketplace.source.type !== "official"
36
- ? `${describeSource(marketplace.source)} (built in)`
37
- : describeSource(marketplace.source),
38
- );
29
+ section.field(marketplace.name, describeSource(marketplace.source));
39
30
  }
40
31
  for (const marketplace of listing.unreadable) {
41
32
  section.field(marketplace.name, `cannot be read: ${marketplace.reason}`);
@@ -7,12 +7,18 @@ export {
7
7
  toKebabCase,
8
8
  toSnakeCase,
9
9
  } from "./aliases.js";
10
- export { CLI_RELEVANT_KINDS, type KindMeta, KIND_META } from "./metadata.js";
10
+ export {
11
+ CLI_RELEVANT_KINDS,
12
+ type KindMeta,
13
+ KIND_META,
14
+ RETIRED_KINDS,
15
+ } from "./metadata.js";
11
16
  export {
12
17
  defaultRegistry,
13
18
  type Registry,
14
19
  supportsVerb,
15
20
  type TypeInfo,
21
+ unknownKindError,
16
22
  } from "./registry.js";
17
23
  export {
18
24
  ALL_VERBS,
@@ -46,10 +46,6 @@ export const KIND_META: ReadonlyMap<ApiResourceKind, KindMeta> = new Map([
46
46
  ApiResourceKind.mcp_server,
47
47
  { name: "McpServer", displayName: "MCP Server", idPrefix: "mcp" },
48
48
  ],
49
- [
50
- ApiResourceKind.project,
51
- { name: "Project", displayName: "Project", idPrefix: "prj" },
52
- ],
53
49
  [
54
50
  ApiResourceKind.api_key,
55
51
  { name: "ApiKey", displayName: "API Key", idPrefix: "key" },
@@ -122,7 +118,6 @@ export const CLI_RELEVANT_KINDS: readonly ApiResourceKind[] = [
122
118
  ApiResourceKind.skill,
123
119
  ApiResourceKind.plugin,
124
120
  ApiResourceKind.mcp_server,
125
- ApiResourceKind.project,
126
121
  ApiResourceKind.api_key,
127
122
  ApiResourceKind.identity_provider,
128
123
  ApiResourceKind.oauth_app,
@@ -135,3 +130,18 @@ export const CLI_RELEVANT_KINDS: readonly ApiResourceKind[] = [
135
130
  ApiResourceKind.workflow_instance,
136
131
  ApiResourceKind.session,
137
132
  ];
133
+
134
+ /**
135
+ * YAML kinds the platform once served and no longer does. A manifest that
136
+ * still carries one is an old file, not a typo, so the refusal it meets
137
+ * says what happened and what to do instead of "unknown kind"; the
138
+ * registry's `unknownKindError` reads this table before falling back to the
139
+ * generic sentence. The key is the YAML `kind` value as the retired proto
140
+ * declared it; the value is the sentence that follows the kind's name.
141
+ */
142
+ export const RETIRED_KINDS: ReadonlyMap<string, string> = new Map([
143
+ [
144
+ "Project",
145
+ "is no longer a Stigmer resource. A folder of resources that belong together is a plugin: run `stigmer push plugin <dir>` to install it as one, or apply each resource file with `stigmer apply -f <file>`.",
146
+ ],
147
+ ]);
@@ -241,13 +241,7 @@ describe("registry — verb/dispatch conformance", () => {
241
241
  label: "apply",
242
242
  verb: Verb.Apply,
243
243
  wired: new Set(APPLY_HANDLERS.keys()),
244
- specialCases: new Map([
245
- [
246
- ApiResourceKind.project,
247
- "the stigmer.yaml declarative/synthesis track (resources/apply/declarative.ts); " +
248
- "file mode refuses with a pointer there (resolveHandlerForKind)",
249
- ],
250
- ]),
244
+ specialCases: new Map(),
251
245
  },
252
246
  {
253
247
  label: "get",
@@ -5,8 +5,9 @@
5
5
  // removed in the TypeScript migration — stigmer/stigmer#203.)
6
6
 
7
7
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
8
+ import { UsageError } from "../errors/index.js";
8
9
  import { generateAliases, normalizeAlias } from "./aliases.js";
9
- import { CLI_RELEVANT_KINDS, KIND_META } from "./metadata.js";
10
+ import { CLI_RELEVANT_KINDS, KIND_META, RETIRED_KINDS } from "./metadata.js";
10
11
  import { Verb } from "./verbs.js";
11
12
  import { verbsForKind } from "./verb-support.js";
12
13
 
@@ -34,6 +35,21 @@ export function supportsVerb(info: TypeInfo, verb: Verb): boolean {
34
35
  return info.supportedVerbs.has(verb);
35
36
  }
36
37
 
38
+ /**
39
+ * The one refusal for a YAML `kind` the registry does not know, shared by
40
+ * every path that reads a kind from a file (`apply -f`, `validate -f`) so
41
+ * the wording lives in one place. A kind the platform retired gets its own
42
+ * sentence (RETIRED_KINDS): the file is old, not wrong, and the user
43
+ * deserves the reason and the way. `where` names the file for the message.
44
+ */
45
+ export function unknownKindError(yamlKind: string, where: string): UsageError {
46
+ const retired = RETIRED_KINDS.get(yamlKind);
47
+ if (retired !== undefined) {
48
+ return new UsageError(`kind '${yamlKind}' in ${where} ${retired}`);
49
+ }
50
+ return new UsageError(`unknown resource kind '${yamlKind}' in ${where}`);
51
+ }
52
+
37
53
  export interface Registry {
38
54
  /** Look up a type by its proto kind. */
39
55
  getByKind(kind: ApiResourceKind): TypeInfo | undefined;
@@ -66,19 +66,6 @@ export const VERB_SUPPORT: ReadonlyMap<
66
66
  Verb.Delete,
67
67
  ]),
68
68
  ],
69
- // Project apply is the stigmer.yaml declarative/synthesis track, not
70
- // `apply -f project.yaml` — resolveHandlerForKind carries the teaching
71
- // refusal, and the conformance test documents it as a special case.
72
- [
73
- ApiResourceKind.project,
74
- new Set<Verb>([
75
- Verb.Apply,
76
- Verb.Validate,
77
- Verb.Get,
78
- Verb.List,
79
- Verb.Delete,
80
- ]),
81
- ],
82
69
  [ApiResourceKind.api_key, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete])],
83
70
  // agent_execution is special — uses dedicated AgentExecutionQueryController
84
71
  // RPCs, not the unified SearchService. delete maps to cancel.
@@ -268,12 +268,24 @@ describe("file-mode apply — channel app (#353)", () => {
268
268
  });
269
269
  });
270
270
 
271
- describe("file-mode apply — project special case", () => {
272
- it("refuses a Project manifest with a pointer to the stigmer.yaml flow", () => {
271
+ describe("file-mode apply — kinds the registry does not know", () => {
272
+ it("refuses a retired kind with what happened and what to run instead", () => {
273
273
  const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
274
274
  try {
275
275
  writeYaml(dir, "project.yaml", ["kind: Project", "metadata:", " name: Demo", " slug: demo", ""].join("\n"));
276
- expect(() => resolveApplyItems(dir)).toThrow(/stigmer\.yaml/);
276
+ expect(() => resolveApplyItems(dir)).toThrow(
277
+ /kind 'Project' in .*project\.yaml is no longer a Stigmer resource.*stigmer push plugin <dir>/,
278
+ );
279
+ } finally {
280
+ rmSync(dir, { recursive: true, force: true });
281
+ }
282
+ });
283
+
284
+ it("refuses a kind that never existed with the plain unknown-kind sentence", () => {
285
+ const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
286
+ try {
287
+ writeYaml(dir, "thing.yaml", ["kind: Widget", "metadata:", " name: Demo", ""].join("\n"));
288
+ expect(() => resolveApplyItems(dir)).toThrow(/^unknown resource kind 'Widget' in .*thing\.yaml$/);
277
289
  } finally {
278
290
  rmSync(dir, { recursive: true, force: true });
279
291
  }
@@ -18,7 +18,7 @@ import {
18
18
  } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
19
19
  import { UsageError } from "../../errors/index.js";
20
20
  import { CommandResult } from "../../output/index.js";
21
- import { defaultRegistry, Verb } from "../../registry/index.js";
21
+ import { defaultRegistry, unknownKindError, Verb } from "../../registry/index.js";
22
22
  import { loadDocuments, resolveYamlFiles } from "../documents.js";
23
23
  import { decodeWorkflowTaskConfigs } from "../task-configs.js";
24
24
  import { type ApplyHandler, APPLY_HANDLERS, type ControllerFn } from "./handlers.js";
@@ -36,9 +36,9 @@ export interface ApplyOutcome {
36
36
  /** Optional org-mismatch warning to surface on stderr. */
37
37
  readonly warning?: string;
38
38
  /**
39
- * The applied resource message (absent in dry-run). Carries the server's
40
- * authoritative metadata (id/slug) — the reconciler reads this to register
41
- * project membership, so membership matches what the backend stored.
39
+ * The applied resource message (absent in dry-run), as the server stored
40
+ * it: authoritative metadata (id, slug, visibility) for anything that
41
+ * runs after the apply.
42
42
  */
43
43
  readonly applied?: Message;
44
44
  }
@@ -60,22 +60,14 @@ export function resolveApplyItems(path: string): ApplyItem[] {
60
60
 
61
61
  /**
62
62
  * Resolve a YAML `kind` to its apply handler, enforcing the verb-support gate.
63
- * Shared by file mode and declarative mode so the lookup + error wording lives
64
- * in exactly one place. `where` is woven into errors for a precise location.
63
+ * The unknown-kind wording is the registry's (`unknownKindError`), shared
64
+ * with `validate -f`, so a retired kind is refused with the same sentence on
65
+ * both paths. `where` is woven into errors for a precise location.
65
66
  */
66
67
  export function resolveHandlerForKind(kind: string, where: string): ApplyHandler {
67
68
  const info = defaultRegistry().getByYamlKind(kind);
68
- if (info === undefined) throw new UsageError(`unknown resource kind '${kind}' in ${where}`);
69
+ if (info === undefined) throw unknownKindError(kind, where);
69
70
  if (!info.supportedVerbs.has(Verb.Apply)) throw new UsageError(`${info.displayName} does not support 'apply'`);
70
- if (info.kind === ApiResourceKind.project) {
71
- // Project's Apply verb is the stigmer.yaml track, deliberately not a
72
- // manifest handler: a file-mode apply would create a second, member-less
73
- // way to apply a project, bypassing the membership reconciler.
74
- throw new UsageError(
75
- `a Project is not applied from a resource manifest (${where}): ` +
76
- "run `stigmer apply` in the directory containing stigmer.yaml and the project is reconciled from its members",
77
- );
78
- }
79
71
  const handler = APPLY_HANDLERS.get(info.kind);
80
72
  if (handler === undefined) throw new UsageError(`apply not implemented for ${info.displayName}`);
81
73
  return handler;
@@ -109,16 +101,15 @@ export async function applyItem(
109
101
 
110
102
  /**
111
103
  * Strict YAML→proto marshal for one item, with a precise location in the error.
112
- * Split out from {@link applyItem} so the declarative reconciler can marshal
113
- * without immediately applying (it batches messages, then reconciles).
104
+ * Split out from {@link applyItem} so marshalling can be exercised and reused
105
+ * apart from the apply it feeds.
114
106
  */
115
107
  export function marshalItem(item: ApplyItem): Message {
116
108
  try {
117
109
  const message = fromJson(item.handler.schema, item.document, { ignoreUnknownFields: false });
118
110
  // Workflow task_config blocks live inside an open Struct the top-level
119
- // decode cannot see into; decode them per kind so a dry-run (and the
120
- // reconciler's marshal) rejects exactly what a real apply rejects
121
- // (stigmer/stigmer#778).
111
+ // decode cannot see into; decode them per kind so a dry-run rejects
112
+ // exactly what a real apply rejects (stigmer/stigmer#778).
122
113
  if (item.handler.kind === ApiResourceKind.workflow) {
123
114
  decodeWorkflowTaskConfigs(message as Workflow);
124
115
  }
@@ -130,11 +121,9 @@ export function marshalItem(item: ApplyItem): Message {
130
121
 
131
122
  /**
132
123
  * Apply a fully-marshalled resource message: inject org, then dry-run preview or
133
- * drive the controller's `apply` RPC. This is the single apply core shared by
134
- * both tracks — the declarative/file track feeds messages marshalled from YAML
135
- * (`marshalItem`), the synthesis track feeds messages decoded from `.pb`
136
- * (`fromBinary`). One core means one place where org injection, create/update
137
- * detection, and result shaping live (DD-009 §6).
124
+ * drive the controller's `apply` RPC. The single apply core behind file mode,
125
+ * taking the message rather than the YAML so org injection, create/update
126
+ * detection and result shaping live in one place whatever produced the message.
138
127
  */
139
128
  export async function applyMessage(
140
129
  controller: ControllerFn,
@@ -1,6 +1,6 @@
1
- // Apply-handler registry for `apply -f` (file mode) and the synthesis track.
1
+ // Apply-handler registry for `apply -f` (file mode).
2
2
  //
3
- // The declarative manifest kinds come from the SDK's manifest registry
3
+ // The manifest kinds come from the SDK's manifest registry
4
4
  // (`manifestKinds()` in @stigmer/sdk) — the one table binding a kind to its
5
5
  // proto schema, its raw command-controller `apply` RPC, and its dependency
6
6
  // apply order. The CLI consumes that table instead of keeping a copy: the
@@ -112,7 +112,7 @@ describe("runOAuthFlow", () => {
112
112
  log: () => {},
113
113
  });
114
114
  expect(openBrowser).toHaveBeenCalledWith(
115
- "https://app.stigmer.ai/acme/mcp-servers/github",
115
+ "https://app.stigmer.ai/library/mcp-servers/acme/github",
116
116
  );
117
117
  });
118
118
 
@@ -152,7 +152,7 @@ describe("runOAuthFlow", () => {
152
152
  log: () => {},
153
153
  });
154
154
  expect(openBrowser).toHaveBeenCalledWith(
155
- "http://localhost:7234/acme/mcp-servers/github",
155
+ "http://localhost:7234/library/mcp-servers/acme/github",
156
156
  );
157
157
  });
158
158
  });
@@ -60,7 +60,10 @@ export async function runOAuthFlow(deps: OAuthFlowDeps): Promise<void> {
60
60
 
61
61
  const slug = deps.server.metadata?.slug ?? "";
62
62
  const name = deps.server.metadata?.name ?? slug;
63
- const pageURL = `${consoleURL}/${deps.org}/mcp-servers/${slug}`;
63
+ // The route both consoles serve (`/library/mcp-servers/<org>/<slug>`);
64
+ // the Go-era `/<org>/mcp-servers/<slug>` answered 404 in the TypeScript
65
+ // console and sent the user to a dead page.
66
+ const pageURL = `${consoleURL}/library/mcp-servers/${deps.org}/${slug}`;
64
67
 
65
68
  log(`OAuth authentication required for '${name}'.`);
66
69
  log("Opening web console to complete authentication...");
@@ -82,7 +82,6 @@ export const DELETE_HANDLERS: ReadonlyMap<ApiResourceKind, DeleteFn> = new Map<
82
82
  ApiResourceKind.mcp_server,
83
83
  (c, id, force) => c.mcpServer.delete({ resourceId: id, force }),
84
84
  ],
85
- [ApiResourceKind.project, (c, id) => c.project.delete(id)],
86
85
  [
87
86
  ApiResourceKind.environment,
88
87
  (c, id, force) => c.environment.delete({ resourceId: id, force }),
@@ -17,7 +17,6 @@ import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/a
17
17
  import { WorkflowInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
18
18
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
19
19
  import { ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
20
- import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
21
20
  import type { Stigmer } from "@stigmer/sdk";
22
21
  import { UsageError } from "../errors/index.js";
23
22
  import type { ParsedReference } from "./reference.js";
@@ -64,10 +63,6 @@ export const GET_BINDINGS: ReadonlyMap<ApiResourceKind, Getter> = new Map([
64
63
  ApiResourceKind.mcp_server,
65
64
  refGetter(ApiResourceKind.mcp_server, McpServerSchema, (c) => c.mcpServer),
66
65
  ],
67
- [
68
- ApiResourceKind.project,
69
- refGetter(ApiResourceKind.project, ProjectSchema, (c) => c.project),
70
- ],
71
66
  [
72
67
  ApiResourceKind.environment,
73
68
  refGetter(
@@ -55,7 +55,6 @@ export const SEARCH_KINDS: ReadonlySet<ApiResourceKind> =
55
55
  ApiResourceKind.agent,
56
56
  ApiResourceKind.workflow,
57
57
  ApiResourceKind.mcp_server,
58
- ApiResourceKind.project,
59
58
  ApiResourceKind.skill,
60
59
  ApiResourceKind.plugin,
61
60
  ApiResourceKind.environment,
@@ -2,8 +2,13 @@
2
2
  // sorted sized entries, the ignore matcher applied (defaults, .gitignore,
3
3
  // .stigmerignore), symlinks never followed, and the JSON projection that
4
4
  // reduces overlay documents to their paths; then the prepared push (the
5
- // deterministic archive and its SHA-256, the server's own digest) and the
6
- // install summary renderer.
5
+ // deterministic archive and its SHA-256, the server's own digest), the
6
+ // install summary renderer (only the kinds installed are counted; the Next
7
+ // section says what each server still needs), and `readNextSteps` against an
8
+ // in-memory backend: a sign-in for an OAuth server without a grant, "signed
9
+ // in" with one, the variables an API-key server wants and where they are
10
+ // asked, the agent an MCP-only plugin still needs, and no line for a server
11
+ // the CLI cannot read.
7
12
 
8
13
  import {
9
14
  mkdirSync,
@@ -260,3 +265,115 @@ describe("renderPushOutcome", () => {
260
265
  expect(text).toContain("runs on the session's model");
261
266
  });
262
267
  });
268
+
269
+ describe("readNextSteps", () => {
270
+ it("reads what each server still needs, best-effort, and names the agent an MCP-only plugin lacks", async () => {
271
+ const { readNextSteps, renderPushOutcome } = await import("./plugin.js");
272
+ const { create } = await import("@bufbuild/protobuf");
273
+ const { Code, ConnectError, createRouterTransport } =
274
+ await import("@connectrpc/connect");
275
+ const { Stigmer } = await import("@stigmer/sdk");
276
+ const { McpServerQueryController } =
277
+ await import("@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb");
278
+ const { McpServerSchema } =
279
+ await import("@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb");
280
+ const { GetOAuthGrantStatusOutputSchema, OAuthConnectionHealth } =
281
+ await import("@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb");
282
+ const { McpServerAuthSchema, McpServerSpecSchema } =
283
+ await import("@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb");
284
+ const { EnvVarDeclarationSchema } =
285
+ await import("@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb");
286
+ const { PluginSchema } =
287
+ await import("@stigmer/protos/ai/stigmer/agentic/plugin/v1/api_pb");
288
+ const { PluginMemberSchema } =
289
+ await import("@stigmer/protos/ai/stigmer/agentic/plugin/v1/io_pb");
290
+ const { ApiResourceKind } =
291
+ await import("@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb");
292
+ const { renderResult } = await import("../output/command-result.js");
293
+
294
+ const shapes: Record<string, "oauth" | "oauth-granted" | "api-key" | "open" | "missing"> = {
295
+ linear: "oauth",
296
+ notion: "oauth-granted",
297
+ warmth: "api-key",
298
+ weather: "open",
299
+ ghost: "missing",
300
+ };
301
+ const transport = createRouterTransport(({ service }) => {
302
+ service(McpServerQueryController, {
303
+ getByReference: (ref) => {
304
+ const shape = shapes[ref.slug];
305
+ if (shape === undefined || shape === "missing")
306
+ throw new ConnectError("no server", Code.NotFound);
307
+ const spec = create(McpServerSpecSchema);
308
+ if (shape === "oauth" || shape === "oauth-granted") {
309
+ spec.auth = create(McpServerAuthSchema, { targetEnvVar: "TOKEN", oauthOnly: true });
310
+ spec.env = { TOKEN: create(EnvVarDeclarationSchema, { isSecret: true }) };
311
+ }
312
+ if (shape === "api-key") spec.env = { API_TOKEN: create(EnvVarDeclarationSchema, { isSecret: true }) };
313
+ return create(McpServerSchema, { metadata: { id: `mcp_${ref.slug}`, org: "acme", slug: ref.slug }, spec });
314
+ },
315
+ getOAuthGrantStatus: (input) => {
316
+ const granted = input.resourceId === "mcp_notion";
317
+ return create(GetOAuthGrantStatusOutputSchema, {
318
+ connected: granted,
319
+ connectionHealth: granted
320
+ ? OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_HEALTHY
321
+ : OAuthConnectionHealth.OAUTH_CONNECTION_HEALTH_NO_GRANT,
322
+ });
323
+ },
324
+ });
325
+ });
326
+ const client = new Stigmer({ baseUrl: "/", getAccessToken: () => "t", customTransport: transport });
327
+ const member = (slug: string) =>
328
+ create(PluginMemberSchema, { kind: ApiResourceKind.mcp_server, id: `mcp_${slug}`, slug, name: slug });
329
+
330
+ const next = await readNextSteps(client, "acme", Object.keys(shapes).map(member));
331
+ expect(next).toEqual([
332
+ { kind: "sign-in", server: "linear", command: "stigmer connect mcp-server linear" },
333
+ { kind: "signed-in", server: "notion" },
334
+ {
335
+ kind: "api-key",
336
+ server: "warmth",
337
+ variables: ["API_TOKEN"],
338
+ askedAt: "connect",
339
+ command: "stigmer connect mcp-server warmth --env API_TOKEN=...",
340
+ },
341
+ { kind: "add-to-agent", servers: ["linear", "notion", "warmth", "weather", "ghost"] },
342
+ ]);
343
+
344
+ // With an agent, the variables are the agent's to ask and no agent is missing.
345
+ const withAgent = await readNextSteps(client, "acme", [
346
+ member("warmth"),
347
+ create(PluginMemberSchema, { kind: ApiResourceKind.agent, id: "agt_1", slug: "thermos", name: "thermos" }),
348
+ ]);
349
+ expect(withAgent).toEqual([
350
+ { kind: "api-key", server: "warmth", variables: ["API_TOKEN"], askedAt: "agent" },
351
+ ]);
352
+
353
+ // The renderer names only the kinds installed and prints the Next section.
354
+ const plugin = create(PluginSchema, {
355
+ metadata: { id: "plg_1", slug: "linear" },
356
+ status: { materialized: { skills: 0, mcpServers: 1, agents: 0, workflows: 0 } },
357
+ });
358
+ const result = renderPushOutcome(
359
+ { plugin, members: [member("linear")], archiveBytes: 10 },
360
+ { next: next.slice(0, 1) },
361
+ );
362
+ const lines: string[] = [];
363
+ const original = process.stderr.write.bind(process.stderr);
364
+ process.stderr.write = ((chunk: string | Uint8Array) => {
365
+ lines.push(String(chunk));
366
+ return true;
367
+ }) as typeof process.stderr.write;
368
+ try {
369
+ renderResult(result, "human");
370
+ } finally {
371
+ process.stderr.write = original;
372
+ }
373
+ const text = lines.join("");
374
+ expect(text).toContain("Installed plugin 'linear' (1 MCP server)");
375
+ expect(text).not.toContain("0 skills");
376
+ expect(text).toContain("Next");
377
+ expect(text).toContain("Sign in to linear: stigmer connect mcp-server linear");
378
+ });
379
+ });