@stigmer/cli 3.16.0 → 3.17.0

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 (171) hide show
  1. package/commands/agent-exec-flags.d.ts +2 -5
  2. package/commands/agent-exec-flags.d.ts.map +1 -1
  3. package/commands/agent-exec-flags.js +6 -13
  4. package/commands/agent-exec-flags.js.map +1 -1
  5. package/commands/connect.js +1 -1
  6. package/commands/connect.js.map +1 -1
  7. package/commands/install.d.ts +3 -0
  8. package/commands/install.d.ts.map +1 -0
  9. package/commands/install.js +68 -0
  10. package/commands/install.js.map +1 -0
  11. package/commands/marketplace.d.ts +3 -0
  12. package/commands/marketplace.d.ts.map +1 -0
  13. package/commands/marketplace.js +94 -0
  14. package/commands/marketplace.js.map +1 -0
  15. package/commands/push.js +2 -7
  16. package/commands/push.js.map +1 -1
  17. package/commands/run.js +0 -3
  18. package/commands/run.js.map +1 -1
  19. package/config/config.d.ts +19 -0
  20. package/config/config.d.ts.map +1 -1
  21. package/config/config.js +15 -0
  22. package/config/config.js.map +1 -1
  23. package/config/index.d.ts +1 -1
  24. package/config/index.d.ts.map +1 -1
  25. package/config/index.js.map +1 -1
  26. package/local/bootstrap.d.ts +21 -0
  27. package/local/bootstrap.d.ts.map +1 -0
  28. package/local/bootstrap.js +88 -0
  29. package/local/bootstrap.js.map +1 -0
  30. package/local/daemon/launch.d.ts +2 -2
  31. package/local/daemon/launch.d.ts.map +1 -1
  32. package/local/daemon/launch.js +4 -34
  33. package/local/daemon/launch.js.map +1 -1
  34. package/local/daemon/process.d.ts +1 -1
  35. package/local/plugins/defaults.d.ts +35 -0
  36. package/local/plugins/defaults.d.ts.map +1 -0
  37. package/local/plugins/defaults.js +83 -0
  38. package/local/plugins/defaults.js.map +1 -0
  39. package/local/seedpack/content.d.ts +2 -1
  40. package/local/seedpack/content.d.ts.map +1 -1
  41. package/local/seedpack/content.js +5 -34
  42. package/local/seedpack/content.js.map +1 -1
  43. package/marketplace/config.d.ts +59 -0
  44. package/marketplace/config.d.ts.map +1 -0
  45. package/marketplace/config.js +162 -0
  46. package/marketplace/config.js.map +1 -0
  47. package/marketplace/github.d.ts +31 -0
  48. package/marketplace/github.d.ts.map +1 -0
  49. package/marketplace/github.js +203 -0
  50. package/marketplace/github.js.map +1 -0
  51. package/marketplace/index.d.ts +8 -0
  52. package/marketplace/index.d.ts.map +1 -0
  53. package/marketplace/index.js +12 -0
  54. package/marketplace/index.js.map +1 -0
  55. package/marketplace/install.d.ts +28 -0
  56. package/marketplace/install.d.ts.map +1 -0
  57. package/marketplace/install.js +109 -0
  58. package/marketplace/install.js.map +1 -0
  59. package/marketplace/materialize.d.ts +19 -0
  60. package/marketplace/materialize.d.ts.map +1 -0
  61. package/marketplace/materialize.js +53 -0
  62. package/marketplace/materialize.js.map +1 -0
  63. package/marketplace/official.d.ts +28 -0
  64. package/marketplace/official.d.ts.map +1 -0
  65. package/marketplace/official.js +86 -0
  66. package/marketplace/official.js.map +1 -0
  67. package/marketplace/read.d.ts +17 -0
  68. package/marketplace/read.d.ts.map +1 -0
  69. package/marketplace/read.js +44 -0
  70. package/marketplace/read.js.map +1 -0
  71. package/marketplace/ref.d.ts +13 -0
  72. package/marketplace/ref.d.ts.map +1 -0
  73. package/marketplace/ref.js +54 -0
  74. package/marketplace/ref.js.map +1 -0
  75. package/marketplace/render.d.ts +22 -0
  76. package/marketplace/render.d.ts.map +1 -0
  77. package/marketplace/render.js +118 -0
  78. package/marketplace/render.js.map +1 -0
  79. package/output/ndjson.js +1 -1
  80. package/output/ndjson.js.map +1 -1
  81. package/package.json +7 -6
  82. package/program.d.ts.map +1 -1
  83. package/program.js +4 -2
  84. package/program.js.map +1 -1
  85. package/resources/ignore/index.d.ts +2 -4
  86. package/resources/ignore/index.d.ts.map +1 -1
  87. package/resources/ignore/index.js +5 -5
  88. package/resources/ignore/index.js.map +1 -1
  89. package/resources/ignore/matcher.d.ts +2 -35
  90. package/resources/ignore/matcher.d.ts.map +1 -1
  91. package/resources/ignore/matcher.js +25 -109
  92. package/resources/ignore/matcher.js.map +1 -1
  93. package/resources/plugin.d.ts +50 -18
  94. package/resources/plugin.d.ts.map +1 -1
  95. package/resources/plugin.js +73 -74
  96. package/resources/plugin.js.map +1 -1
  97. package/resources/run/agent-exec.js +1 -1
  98. package/resources/run/agent-exec.js.map +1 -1
  99. package/resources/run/prepare.d.ts +8 -19
  100. package/resources/run/prepare.d.ts.map +1 -1
  101. package/resources/run/prepare.js +4 -4
  102. package/resources/run/prepare.js.map +1 -1
  103. package/resources/skill.d.ts +2 -2
  104. package/resources/skill.d.ts.map +1 -1
  105. package/resources/skill.js +6 -10
  106. package/resources/skill.js.map +1 -1
  107. package/src/commands/agent-exec-flags.ts +6 -12
  108. package/src/commands/connect.ts +1 -1
  109. package/src/commands/install.test.ts +289 -0
  110. package/src/commands/install.ts +108 -0
  111. package/src/commands/marketplace.test.ts +200 -0
  112. package/src/commands/marketplace.ts +131 -0
  113. package/src/commands/push.ts +5 -10
  114. package/src/commands/run.ts +0 -3
  115. package/src/config/config.ts +37 -0
  116. package/src/config/index.ts +1 -0
  117. package/src/local/bootstrap.test.ts +71 -0
  118. package/src/local/bootstrap.ts +133 -0
  119. package/src/local/daemon/launch.ts +5 -40
  120. package/src/local/daemon/process.ts +1 -1
  121. package/src/local/plugins/defaults.test.ts +238 -0
  122. package/src/local/plugins/defaults.ts +135 -0
  123. package/src/local/seedpack/content.ts +11 -42
  124. package/src/marketplace/__fixtures__/cursor-marketplace.ts +62 -0
  125. package/src/marketplace/config.test.ts +245 -0
  126. package/src/marketplace/config.ts +236 -0
  127. package/src/marketplace/github.test.ts +318 -0
  128. package/src/marketplace/github.ts +296 -0
  129. package/src/marketplace/index.ts +54 -0
  130. package/src/marketplace/install.test.ts +194 -0
  131. package/src/marketplace/install.ts +159 -0
  132. package/src/marketplace/materialize.ts +80 -0
  133. package/src/marketplace/official.test.ts +107 -0
  134. package/src/marketplace/official.ts +125 -0
  135. package/src/marketplace/read.test.ts +94 -0
  136. package/src/marketplace/read.ts +76 -0
  137. package/src/marketplace/ref.test.ts +179 -0
  138. package/src/marketplace/ref.ts +74 -0
  139. package/src/marketplace/render.ts +154 -0
  140. package/src/output/ndjson.ts +1 -1
  141. package/src/program.test.ts +4 -13
  142. package/src/program.ts +4 -2
  143. package/src/resources/ignore/ignore.test.ts +23 -100
  144. package/src/resources/ignore/index.ts +11 -8
  145. package/src/resources/ignore/matcher.ts +33 -136
  146. package/src/resources/plugin.test.ts +35 -6
  147. package/src/resources/plugin.ts +132 -85
  148. package/src/resources/run/agent-exec.ts +1 -1
  149. package/src/resources/run/prepare.test.ts +4 -18
  150. package/src/resources/run/prepare.ts +12 -24
  151. package/src/resources/skill.ts +6 -10
  152. package/commands/draft.d.ts +0 -3
  153. package/commands/draft.d.ts.map +0 -1
  154. package/commands/draft.js +0 -115
  155. package/commands/draft.js.map +0 -1
  156. package/resources/ignore/defaults.d.ts +0 -2
  157. package/resources/ignore/defaults.d.ts.map +0 -1
  158. package/resources/ignore/defaults.js +0 -140
  159. package/resources/ignore/defaults.js.map +0 -1
  160. package/resources/ignore/match.d.ts +0 -3
  161. package/resources/ignore/match.d.ts.map +0 -1
  162. package/resources/ignore/match.js +0 -166
  163. package/resources/ignore/match.js.map +0 -1
  164. package/resources/ignore/pattern.d.ts +0 -13
  165. package/resources/ignore/pattern.d.ts.map +0 -1
  166. package/resources/ignore/pattern.js +0 -118
  167. package/resources/ignore/pattern.js.map +0 -1
  168. package/src/commands/draft.ts +0 -189
  169. package/src/resources/ignore/defaults.ts +0 -155
  170. package/src/resources/ignore/match.ts +0 -158
  171. package/src/resources/ignore/pattern.ts +0 -131
@@ -0,0 +1,94 @@
1
+ // Pins the CLI's edge for the marketplace reader: a tree read through the one
2
+ // walker, the refusal shape, entry lookup and directory resolution, and the
3
+ // official tree in this repository reading clean with `assistant` offered.
4
+
5
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
9
+ import { writeCursorMarketplace } from "./__fixtures__/cursor-marketplace.js";
10
+ import { resolveOfficialMarketplace } from "./official.js";
11
+ import { entryDirectory, findEntry, readMarketplaceTree } from "./read.js";
12
+
13
+ let root: string;
14
+
15
+ beforeAll(() => {
16
+ root = mkdtempSync(join(tmpdir(), "stigmer-marketplace-read-"));
17
+ writeCursorMarketplace(root);
18
+ });
19
+
20
+ afterAll(() => {
21
+ rmSync(root, { recursive: true, force: true });
22
+ });
23
+
24
+ describe("readMarketplaceTree", () => {
25
+ it("reads a Cursor-dialect tree into the one shape, dropping the missing entry with its sentence", () => {
26
+ const tree = readMarketplaceTree(root);
27
+ expect(tree.marketplace.name).toBe("cursor-plugins");
28
+ expect(tree.marketplace.dialect).toBe("cursor");
29
+ expect(tree.marketplace.plugins.map((entry) => entry.name)).toEqual([
30
+ "thermos",
31
+ "github",
32
+ ]);
33
+ expect(tree.warnings).toHaveLength(1);
34
+ expect(tree.warnings[0]?.kind).toBe("entry-directory-missing");
35
+ expect(tree.warnings[0]?.subject).toBe("ghost");
36
+ });
37
+
38
+ it("refuses a directory with no marketplace file, naming the four locations", () => {
39
+ const empty = mkdtempSync(join(tmpdir(), "stigmer-marketplace-empty-"));
40
+ try {
41
+ writeFileSync(join(empty, "README.md"), "nothing here");
42
+ expect(() => readMarketplaceTree(empty, "github.com/a/b")).toThrow(
43
+ /github\.com\/a\/b: not a marketplace this CLI can read, 1 problem found:\n - .*marketplace\.json/,
44
+ );
45
+ } finally {
46
+ rmSync(empty, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ it("refuses a broken file with every problem and every warning", () => {
51
+ const broken = mkdtempSync(join(tmpdir(), "stigmer-marketplace-broken-"));
52
+ try {
53
+ writeFileSync(
54
+ join(broken, "marketplace.json"),
55
+ JSON.stringify({
56
+ plugins: [
57
+ { name: "a", source: "../out" },
58
+ { name: "b", source: "https://x" },
59
+ ],
60
+ }),
61
+ );
62
+ expect(() => readMarketplaceTree(broken)).toThrow(
63
+ /2 problems found:[\s\S]*and 1 warning:/,
64
+ );
65
+ } finally {
66
+ rmSync(broken, { recursive: true, force: true });
67
+ }
68
+ });
69
+ });
70
+
71
+ describe("findEntry and entryDirectory", () => {
72
+ it("resolves an offered entry to its absolute directory and an unknown name to undefined", () => {
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"),
78
+ );
79
+ expect(findEntry(tree.marketplace, "ghost")).toBeUndefined();
80
+ });
81
+ });
82
+
83
+ describe("the official tree in this repository", () => {
84
+ it("reads clean through the CLI walker and offers assistant as its one default", () => {
85
+ const official = resolveOfficialMarketplace();
86
+ expect(official.source).toBe("repo");
87
+ const tree = readMarketplaceTree(official.dir);
88
+ expect(tree.marketplace.name).toBe("stigmer");
89
+ expect(tree.marketplace.dialect).toBe("stigmer");
90
+ expect(tree.warnings).toEqual([]);
91
+ expect(tree.marketplace.defaults).toEqual(["assistant"]);
92
+ expect(findEntry(tree.marketplace, "assistant")?.dir).toBe("assistant");
93
+ });
94
+ });
@@ -0,0 +1,76 @@
1
+ // A marketplace tree on disk, read into the library's one shape.
2
+ //
3
+ // `@stigmer/plugin-package` is pure over a `PluginFiles`; the CLI feeds it
4
+ // the same gitignore-aware walk `push plugin` uses (`readPluginDirectory`),
5
+ // rooted at the marketplace tree, so what `show` lists is what the tree
6
+ // holds after the ignore rules that would apply to a push from it. The
7
+ // library's refusal (a broken marketplace FILE) becomes one `UsageError`
8
+ // with every sentence, the shape `pluginRefusal` prints for a plugin; its
9
+ // warnings (entries the tree cannot offer) travel with the marketplace so
10
+ // `show` can say why an entry is missing.
11
+
12
+ import { join } from "node:path";
13
+ import {
14
+ type Marketplace,
15
+ type MarketplaceEntry,
16
+ type MarketplaceFinding,
17
+ readMarketplace,
18
+ } from "@stigmer/plugin-package";
19
+ import { UsageError } from "../errors/index.js";
20
+ import { count, readPluginDirectory } from "../resources/plugin.js";
21
+
22
+ export interface ReadMarketplaceTree {
23
+ readonly root: string;
24
+ readonly marketplace: Marketplace;
25
+ /** The library's warnings: entries dropped, each with its own sentence. */
26
+ readonly warnings: readonly MarketplaceFinding[];
27
+ }
28
+
29
+ /** Read the tree at `root`; a marketplace the library refuses throws with every problem named. */
30
+ export function readMarketplaceTree(
31
+ root: string,
32
+ describe: string = root,
33
+ ): ReadMarketplaceTree {
34
+ const directory = readPluginDirectory(root);
35
+ const outcome = readMarketplace(directory.files);
36
+ if (!outcome.ok)
37
+ throw marketplaceRefusal(describe, outcome.errors, outcome.warnings);
38
+ return { root, marketplace: outcome.marketplace, warnings: outcome.warnings };
39
+ }
40
+
41
+ /** The library's refusal as the CLI's one UsageError: every problem, then every warning. */
42
+ export function marketplaceRefusal(
43
+ describe: string,
44
+ errors: readonly MarketplaceFinding[],
45
+ warnings: readonly MarketplaceFinding[],
46
+ ): UsageError {
47
+ const lines = [
48
+ `${describe}: not a marketplace this CLI can read, ${count(errors.length, "problem")} found:`,
49
+ ...errors.map((finding) => ` - ${finding.message}`),
50
+ ];
51
+ if (warnings.length > 0) {
52
+ lines.push(
53
+ `and ${count(warnings.length, "warning")}:`,
54
+ ...warnings.map((finding) => ` - ${finding.message}`),
55
+ );
56
+ }
57
+ return new UsageError(lines.join("\n"));
58
+ }
59
+
60
+ /** The offered entry called `name`, or `undefined`. */
61
+ export function findEntry(
62
+ marketplace: Marketplace,
63
+ name: string,
64
+ ): MarketplaceEntry | undefined {
65
+ return marketplace.plugins.find((entry) => entry.name === name);
66
+ }
67
+
68
+ /** The absolute directory of an offered entry: what the plugin walker reads next. */
69
+ export function entryDirectory(
70
+ tree: ReadMarketplaceTree,
71
+ entry: MarketplaceEntry,
72
+ ): string {
73
+ return entry.dir === ""
74
+ ? tree.root
75
+ : join(tree.root, ...entry.dir.split("/"));
76
+ }
@@ -0,0 +1,179 @@
1
+ // Pins the two grammars: every accepted form of `[marketplace/]name[@version]`
2
+ // and every refusal with its sentence; every accepted form of an `add`
3
+ // source (a directory wins, then owner/repo[@ref], then the GitHub URL) and
4
+ // the refusals.
5
+
6
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
10
+ import { formatInstallRef, parseAddSource, parseInstallRef } from "./ref.js";
11
+
12
+ describe("parseInstallRef", () => {
13
+ it("reads a bare name, a marketplace prefix, a version, and all three", () => {
14
+ expect(parseInstallRef("thermos")).toEqual({ name: "thermos" });
15
+ expect(parseInstallRef("cursor-plugins/thermos")).toEqual({
16
+ marketplace: "cursor-plugins",
17
+ name: "thermos",
18
+ });
19
+ expect(parseInstallRef("thermos@1.0.0")).toEqual({
20
+ name: "thermos",
21
+ version: "1.0.0",
22
+ });
23
+ expect(parseInstallRef("stigmer/assistant@1.0.0")).toEqual({
24
+ marketplace: "stigmer",
25
+ name: "assistant",
26
+ version: "1.0.0",
27
+ });
28
+ expect(parseInstallRef("third.party-x@v2-rc.1")).toEqual({
29
+ name: "third.party-x",
30
+ version: "v2-rc.1",
31
+ });
32
+ });
33
+
34
+ it("round-trips through formatInstallRef", () => {
35
+ for (const text of [
36
+ "thermos",
37
+ "cursor-plugins/thermos",
38
+ "thermos@1.0.0",
39
+ "a/b@c",
40
+ ]) {
41
+ expect(formatInstallRef(parseInstallRef(text))).toBe(text);
42
+ }
43
+ });
44
+
45
+ it("refuses a path toward push plugin", () => {
46
+ for (const path of [
47
+ "./thermos",
48
+ "../x",
49
+ "/tmp/plugin",
50
+ ".",
51
+ "..",
52
+ "~/plugins",
53
+ "C:\\plugins",
54
+ ]) {
55
+ expect(() => parseInstallRef(path)).toThrow(
56
+ /is a path.*stigmer push plugin/s,
57
+ );
58
+ }
59
+ });
60
+
61
+ it("refuses names and versions outside the grammar, naming the part", () => {
62
+ expect(() => parseInstallRef("Thermos")).toThrow(
63
+ /'Thermos' is not a plugin name/,
64
+ );
65
+ expect(() => parseInstallRef("a/b/c")).toThrow(
66
+ /'b\/c' is not a plugin name/,
67
+ );
68
+ expect(() => parseInstallRef("Bad/thermos")).toThrow(
69
+ /'Bad' is not a marketplace name/,
70
+ );
71
+ expect(() => parseInstallRef("thermos@")).toThrow(/'' is not a version/);
72
+ expect(() => parseInstallRef("thermos@1.0+build")).toThrow(
73
+ /'1.0\+build' is not a version/,
74
+ );
75
+ expect(() => parseInstallRef("")).toThrow(/'' is not a plugin name/);
76
+ expect(() => parseInstallRef("a@b@c")).toThrow(/'b@c' is not a version/);
77
+ });
78
+ });
79
+
80
+ describe("parseAddSource", () => {
81
+ let cwd: string;
82
+
83
+ beforeAll(() => {
84
+ cwd = mkdtempSync(join(tmpdir(), "stigmer-add-source-"));
85
+ mkdirSync(join(cwd, "plugins"));
86
+ mkdirSync(join(cwd, "cursor", "plugins"), { recursive: true });
87
+ });
88
+
89
+ afterAll(() => {
90
+ rmSync(cwd, { recursive: true, force: true });
91
+ });
92
+
93
+ it("reads an existing directory as local, stored absolute, before any other reading", () => {
94
+ expect(parseAddSource("plugins", cwd)).toEqual({
95
+ type: "local",
96
+ path: join(cwd, "plugins"),
97
+ });
98
+ expect(parseAddSource("./plugins/", cwd)).toEqual({
99
+ type: "local",
100
+ path: join(cwd, "plugins"),
101
+ });
102
+ expect(parseAddSource(join(cwd, "plugins"), cwd)).toEqual({
103
+ type: "local",
104
+ path: join(cwd, "plugins"),
105
+ });
106
+ // A directory named like an owner/repo is the directory.
107
+ expect(parseAddSource("cursor/plugins", cwd)).toEqual({
108
+ type: "local",
109
+ path: join(cwd, "cursor", "plugins"),
110
+ });
111
+ });
112
+
113
+ it("reads owner/repo and owner/repo@ref as github when no such directory exists", () => {
114
+ expect(parseAddSource("anthropics/claude-code", cwd)).toEqual({
115
+ type: "github",
116
+ repo: "anthropics/claude-code",
117
+ });
118
+ expect(parseAddSource("anthropics/claude-code@v1.0", cwd)).toEqual({
119
+ type: "github",
120
+ repo: "anthropics/claude-code",
121
+ ref: "v1.0",
122
+ });
123
+ expect(parseAddSource("openai/codex@refs/heads/main", cwd)).toEqual({
124
+ type: "github",
125
+ repo: "openai/codex",
126
+ ref: "refs/heads/main",
127
+ });
128
+ });
129
+
130
+ it("unwraps the GitHub URL a browser shows, with or without a tree ref", () => {
131
+ expect(parseAddSource("https://github.com/cursor/plugins", cwd)).toEqual({
132
+ type: "github",
133
+ repo: "cursor/plugins",
134
+ });
135
+ expect(parseAddSource("https://github.com/cursor/plugins/", cwd)).toEqual({
136
+ type: "github",
137
+ repo: "cursor/plugins",
138
+ });
139
+ expect(
140
+ parseAddSource("https://github.com/cursor/plugins.git", cwd),
141
+ ).toEqual({ type: "github", repo: "cursor/plugins" });
142
+ expect(
143
+ parseAddSource("https://www.github.com/cursor/plugins/tree/main", cwd),
144
+ ).toEqual({
145
+ type: "github",
146
+ repo: "cursor/plugins",
147
+ ref: "main",
148
+ });
149
+ expect(
150
+ parseAddSource("http://github.com/openai/codex/tree/release/1.2", cwd),
151
+ ).toEqual({
152
+ type: "github",
153
+ repo: "openai/codex",
154
+ ref: "release/1.2",
155
+ });
156
+ });
157
+
158
+ it("refuses what is neither, naming the three forms", () => {
159
+ expect(() => parseAddSource("", cwd)).toThrow(/source is required/);
160
+ expect(() => parseAddSource("missing-dir", cwd)).toThrow(
161
+ /not a marketplace source/,
162
+ );
163
+ expect(() => parseAddSource("./missing-dir", cwd)).toThrow(
164
+ /not a directory on this machine and not a GitHub/,
165
+ );
166
+ expect(() => parseAddSource("a/b/c", cwd)).toThrow(
167
+ /not a directory on this machine and not a GitHub/,
168
+ );
169
+ expect(() => parseAddSource("cursor/plugins@", cwd)).toThrow(
170
+ /not a marketplace source|not a directory/,
171
+ );
172
+ expect(() => parseAddSource("https://gitlab.com/a/b", cwd)).toThrow(
173
+ /not a directory on this machine and not a GitHub/,
174
+ );
175
+ expect(() => parseAddSource("https://github.com/cursor", cwd)).toThrow(
176
+ /not a directory on this machine and not a GitHub/,
177
+ );
178
+ });
179
+ });
@@ -0,0 +1,74 @@
1
+ // The two grammars a user types at the marketplace surface, as the CLI's
2
+ // errors.
3
+ //
4
+ // The grammars themselves (`[marketplace/]name[@version]` and the GitHub
5
+ // `owner/repo[@ref]` or URL forms) live in `@stigmer/plugin-package/client`,
6
+ // where the console parses the same text and gets the same sentence. This
7
+ // module adds what only the CLI has: a `UsageError` carrying the sentence
8
+ // plus the paragraph naming the CLI command to run instead, and the reading
9
+ // of `marketplace add <source>` as a directory on this machine, which wins
10
+ // over every other reading (`add plugins` in a checkout means the folder,
11
+ // not a GitHub owner).
12
+
13
+ import { statSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import {
16
+ GITHUB_SOURCE_SHAPE,
17
+ INSTALL_REF_SHAPE,
18
+ type InstallRef,
19
+ formatInstallRef,
20
+ looksLikePath,
21
+ parseGitHubSource,
22
+ parseInstallRef as parseInstallRefText,
23
+ } from "@stigmer/plugin-package/client";
24
+ import { UsageError } from "../errors/index.js";
25
+ import { type MarketplaceSource } from "./config.js";
26
+
27
+ export { type InstallRef, formatInstallRef };
28
+
29
+ export function parseInstallRef(text: string): InstallRef {
30
+ const outcome = parseInstallRefText(text);
31
+ if (outcome.ok) return outcome.ref;
32
+ if (outcome.kind === "path") {
33
+ throw new UsageError(
34
+ `${outcome.message}\n\nTo install a plugin from a folder, run: stigmer push plugin ${text}`,
35
+ );
36
+ }
37
+ throw new UsageError(`${outcome.message}\n\nA ref is ${INSTALL_REF_SHAPE}.`);
38
+ }
39
+
40
+ /**
41
+ * What `marketplace add <source>` was given, as a source. An existing
42
+ * directory wins over every other reading; otherwise the shared GitHub
43
+ * grammar decides, and what it refuses is refused here with the shape.
44
+ */
45
+ export function parseAddSource(
46
+ text: string,
47
+ cwd: string = process.cwd(),
48
+ ): Exclude<MarketplaceSource, { type: "official" }> {
49
+ if (text === "")
50
+ throw new UsageError(`a marketplace source is required\n\n${ADD_SHAPE}`);
51
+
52
+ const asDirectory = resolve(cwd, text);
53
+ if (isDirectory(asDirectory)) return { type: "local", path: asDirectory };
54
+
55
+ const github = parseGitHubSource(text);
56
+ if (github.ok) return github.source;
57
+
58
+ if (looksLikePath(text) || text.includes("/")) {
59
+ throw new UsageError(
60
+ `'${text}' is not a directory on this machine and not a GitHub 'owner/repo'\n\n${ADD_SHAPE}`,
61
+ );
62
+ }
63
+ throw new UsageError(`'${text}' is not a marketplace source\n\n${ADD_SHAPE}`);
64
+ }
65
+
66
+ const ADD_SHAPE = `A source is a directory holding a marketplace file, or ${GITHUB_SOURCE_SHAPE}.`;
67
+
68
+ function isDirectory(path: string): boolean {
69
+ try {
70
+ return statSync(path).isDirectory();
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
@@ -0,0 +1,154 @@
1
+ // What `stigmer marketplace list` and `show` print, as `CommandResult`s: the
2
+ // human form on stderr, the same facts as `data` on stdout under `--json`.
3
+ // The marketplace group renders the way `config backend list` does (a
4
+ // mutating-class result rather than a read-verb table) because it is the
5
+ // same kind of thing: client-side configuration, not a server collection.
6
+
7
+ import { readPluginPackage } from "@stigmer/plugin-package";
8
+ import { CommandResult } from "../output/index.js";
9
+ import { count, readPluginDirectory } from "../resources/plugin.js";
10
+ import {
11
+ type KnownMarketplace,
12
+ type MarketplaceListing,
13
+ describeSource,
14
+ } from "./config.js";
15
+ import { type ReadMarketplaceTree, entryDirectory } from "./read.js";
16
+
17
+ export function renderMarketplaceList(
18
+ listing: MarketplaceListing,
19
+ ): CommandResult {
20
+ const total = listing.known.length + listing.unreadable.length;
21
+ const result =
22
+ listing.unreadable.length === 0
23
+ ? CommandResult.success(`${count(total, "marketplace")} configured`)
24
+ : CommandResult.warning(
25
+ `${count(total, "marketplace")} configured, ${count(listing.unreadable.length, "entry")} this CLI cannot read`,
26
+ );
27
+ const section = result.addSection("Marketplaces");
28
+ for (const marketplace of listing.known) {
29
+ section.field(marketplace.name, describeSource(marketplace.source));
30
+ }
31
+ for (const marketplace of listing.unreadable) {
32
+ section.field(marketplace.name, `cannot be read: ${marketplace.reason}`);
33
+ }
34
+ result.hint("Show what one offers with: stigmer marketplace show <name>");
35
+ result.hint("Install from one with: stigmer install [<name>/]<plugin>");
36
+ return result.withData({
37
+ marketplaces: [
38
+ ...listing.known.map((marketplace) => ({
39
+ name: marketplace.name,
40
+ source: marketplace.source,
41
+ })),
42
+ ...listing.unreadable.map((marketplace) => ({
43
+ name: marketplace.name,
44
+ unreadable: marketplace.reason,
45
+ })),
46
+ ],
47
+ });
48
+ }
49
+
50
+ /** One offered entry as `show` reports it, after its own manifest has been read. */
51
+ export interface ShownEntry {
52
+ readonly name: string;
53
+ readonly dir: string;
54
+ readonly version?: string;
55
+ readonly description?: string;
56
+ /** Set when the entry's own package refuses to read; `show` still lists it so the gap is visible. */
57
+ readonly problems?: number;
58
+ }
59
+
60
+ /**
61
+ * Read each offered entry's own package for its version and description.
62
+ * The marketplace file's description is a fallback (Stigmer's own catalogue
63
+ * carries none, so nothing is written twice); the plugin's manifest is the
64
+ * truth a user installs.
65
+ */
66
+ export function inspectEntries(tree: ReadMarketplaceTree): ShownEntry[] {
67
+ return tree.marketplace.plugins.map((entry) => {
68
+ const outcome = readPluginPackage(
69
+ readPluginDirectory(entryDirectory(tree, entry)).files,
70
+ );
71
+ if (!outcome.ok) {
72
+ return {
73
+ name: entry.name,
74
+ dir: entry.dir,
75
+ ...(entry.description !== undefined && {
76
+ description: entry.description,
77
+ }),
78
+ problems: outcome.errors.length,
79
+ };
80
+ }
81
+ const description = outcome.plugin.description ?? entry.description;
82
+ return {
83
+ name: entry.name,
84
+ dir: entry.dir,
85
+ ...(outcome.plugin.version !== undefined && {
86
+ version: outcome.plugin.version,
87
+ }),
88
+ ...(description !== undefined && { description }),
89
+ };
90
+ });
91
+ }
92
+
93
+ export function renderMarketplaceShow(
94
+ marketplace: KnownMarketplace,
95
+ tree: ReadMarketplaceTree,
96
+ entries: readonly ShownEntry[],
97
+ ): CommandResult {
98
+ const installable = entries.filter(
99
+ (entry) => entry.problems === undefined,
100
+ ).length;
101
+ const headline = `Marketplace '${marketplace.name}' offers ${count(installable, "plugin")}`;
102
+ const result =
103
+ tree.warnings.length === 0 && installable === entries.length
104
+ ? CommandResult.success(headline)
105
+ : CommandResult.warning(
106
+ `${headline}, with ${count(tree.warnings.length + (entries.length - installable), "warning")}`,
107
+ );
108
+
109
+ const about = result.addSection("Marketplace");
110
+ about.field("Name", tree.marketplace.name);
111
+ about.field("Source", describeSource(marketplace.source));
112
+ if (tree.marketplace.description !== undefined)
113
+ about.field("Description", tree.marketplace.description);
114
+ if (tree.marketplace.owner?.name !== undefined)
115
+ about.field("Owner", tree.marketplace.owner.name);
116
+ about.field("Format", tree.marketplace.path);
117
+ if (tree.marketplace.defaults.length > 0)
118
+ about.field("Defaults", tree.marketplace.defaults.join(", "));
119
+
120
+ const plugins = result.addSection("Plugins");
121
+ for (const entry of entries) {
122
+ const version = entry.version === undefined ? "" : `@${entry.version}`;
123
+ const tail =
124
+ entry.problems !== undefined
125
+ ? ` (not installable: ${count(entry.problems, "problem")}; run 'stigmer validate -f' on it)`
126
+ : entry.description === undefined
127
+ ? ""
128
+ : ` ${entry.description}`;
129
+ plugins.item(`${entry.name}${version}${tail}`);
130
+ }
131
+
132
+ if (tree.warnings.length > 0) {
133
+ const section = result.addSection("Not offered");
134
+ for (const warning of tree.warnings) section.item(warning.message);
135
+ }
136
+ result.hint(`Install one with: stigmer install ${marketplace.name}/<plugin>`);
137
+ return result.withData({
138
+ marketplace: {
139
+ name: tree.marketplace.name,
140
+ source: marketplace.source,
141
+ dialect: tree.marketplace.dialect,
142
+ path: tree.marketplace.path,
143
+ ...(tree.marketplace.description !== undefined && {
144
+ description: tree.marketplace.description,
145
+ }),
146
+ ...(tree.marketplace.owner !== undefined && {
147
+ owner: tree.marketplace.owner,
148
+ }),
149
+ defaults: tree.marketplace.defaults,
150
+ },
151
+ plugins: entries,
152
+ warnings: tree.warnings,
153
+ });
154
+ }
@@ -1,4 +1,4 @@
1
- // NDJSON envelope + writer for streaming commands (run/resume/draft).
1
+ // NDJSON envelope + writer for streaming commands (run/resume).
2
2
  //
3
3
  // The wire shape mirrors the Go CLI's run --json output (run_stream_json.go):
4
4
  // every line is a `{type, ts, payload}` object, and the payload is cleaned of
@@ -25,22 +25,13 @@ describe("buildProgram", () => {
25
25
  it("registers the Wave 3c streaming commands", () => {
26
26
  const program = buildProgram();
27
27
  const names = program.commands.map((command) => command.name());
28
- expect(names).toEqual(expect.arrayContaining(["run", "resume", "draft"]));
28
+ expect(names).toEqual(expect.arrayContaining(["run", "resume"]));
29
29
  });
30
30
 
31
- it("exposes the draft subcommands", () => {
31
+ it("has no draft command: a plugin is authored outside Stigmer and installed, never drafted by a system agent", () => {
32
32
  const program = buildProgram();
33
- const draft = program.commands.find((command) => command.name() === "draft");
34
- const subs = draft?.commands.map((command) => command.name()).sort();
35
- expect(subs).toEqual(["agent", "mcp-server", "skill"]);
36
- });
37
-
38
- it("marks draft subcommands' --message as required", () => {
39
- const program = buildProgram();
40
- const draft = program.commands.find((command) => command.name() === "draft");
41
- const skill = draft?.commands.find((command) => command.name() === "skill");
42
- const message = skill?.options.find((option) => option.long === "--message");
43
- expect(message?.required).toBe(true);
33
+ const names = program.commands.map((command) => command.name());
34
+ expect(names).not.toContain("draft");
44
35
  });
45
36
 
46
37
  it("exposes the share subcommands", () => {
package/src/program.ts CHANGED
@@ -17,12 +17,13 @@ import { registerDelete } from "./commands/delete.js";
17
17
  import { registerDiff } from "./commands/diff.js";
18
18
  import { registerDown } from "./commands/down.js";
19
19
  import { registerDownload } from "./commands/download.js";
20
- import { registerDraft } from "./commands/draft.js";
21
20
  import { registerExecution } from "./commands/execution/index.js";
22
21
  import { registerGet } from "./commands/get.js";
22
+ import { registerInstall } from "./commands/install.js";
23
23
  import { registerInternalDaemon } from "./commands/internal-daemon.js";
24
24
  import { registerList } from "./commands/list.js";
25
25
  import { registerLogs } from "./commands/logs.js";
26
+ import { registerMarketplace } from "./commands/marketplace.js";
26
27
  import { registerMcpServer } from "./commands/mcp-server.js";
27
28
  import { registerPush } from "./commands/push.js";
28
29
  import { registerReset } from "./commands/reset.js";
@@ -83,11 +84,12 @@ export function buildProgram(): Command {
83
84
  registerDiff(program);
84
85
  registerUsage(program);
85
86
  registerPush(program);
87
+ registerInstall(program);
88
+ registerMarketplace(program);
86
89
  registerDownload(program);
87
90
  registerApply(program);
88
91
  registerRun(program);
89
92
  registerResume(program);
90
- registerDraft(program);
91
93
  registerExecution(program);
92
94
  registerConnect(program);
93
95
  registerMcpServer(program);