magus-cli 7.12.2 → 7.14.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 (111) hide show
  1. package/README.md +21 -0
  2. package/package.json +4 -4
  3. package/src/__tests__/market-bases.test.ts +112 -0
  4. package/src/__tests__/market-config.test.ts +84 -0
  5. package/src/__tests__/market-conflicts.test.ts +175 -0
  6. package/src/__tests__/market-consent.test.ts +185 -0
  7. package/src/__tests__/market-content.test.ts +183 -0
  8. package/src/__tests__/market-delivery.test.ts +308 -0
  9. package/src/__tests__/market-fixture.ts +1167 -0
  10. package/src/__tests__/market-git.test.ts +130 -0
  11. package/src/__tests__/market-identity.test.ts +127 -0
  12. package/src/__tests__/market-install.test.ts +143 -0
  13. package/src/__tests__/market-layout.test.ts +206 -0
  14. package/src/__tests__/market-publish.test.ts +263 -0
  15. package/src/__tests__/market-r2-adapters.test.ts +190 -0
  16. package/src/__tests__/market-r2-company-cli.test.ts +350 -0
  17. package/src/__tests__/market-r2-company-unit.test.ts +224 -0
  18. package/src/__tests__/market-r2-local-items.test.ts +197 -0
  19. package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
  20. package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
  21. package/src/__tests__/market-r2-routing.test.ts +127 -0
  22. package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
  23. package/src/__tests__/market-r2b-display-path.test.ts +180 -0
  24. package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
  25. package/src/__tests__/market-r2b-local-items.test.ts +253 -0
  26. package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
  27. package/src/__tests__/market-readonly.test.ts +162 -0
  28. package/src/__tests__/market-repositories.test.ts +263 -0
  29. package/src/__tests__/market-results.test.ts +198 -0
  30. package/src/__tests__/market-router.test.ts +65 -0
  31. package/src/__tests__/market-safety.test.ts +357 -0
  32. package/src/__tests__/market-teammates.test.ts +304 -0
  33. package/src/cli/install.ts +64 -5
  34. package/src/cli/market-view.ts +329 -0
  35. package/src/cli/market.ts +664 -0
  36. package/src/cli/router.ts +69 -3
  37. package/src/cli/update-view.ts +56 -0
  38. package/src/cli/update.ts +111 -9
  39. package/src/data/marketplaces.ts +8 -2
  40. package/src/main.tsx +20 -2
  41. package/src/market-tui.tsx +21 -0
  42. package/src/services/active-closure.ts +71 -9
  43. package/src/services/claude-cli.ts +140 -10
  44. package/src/services/claude-settings.ts +19 -25
  45. package/src/services/doctor.ts +5 -5
  46. package/src/services/git-write.ts +615 -0
  47. package/src/services/github-cli.ts +309 -0
  48. package/src/services/local-marketplace.ts +112 -50
  49. package/src/services/market-bases.ts +191 -0
  50. package/src/services/market-change.ts +242 -0
  51. package/src/services/market-clone.ts +572 -0
  52. package/src/services/market-fs.ts +138 -0
  53. package/src/services/market-install.ts +172 -0
  54. package/src/services/market-layout.ts +311 -0
  55. package/src/services/market-local.ts +93 -0
  56. package/src/services/market-marker.ts +49 -0
  57. package/src/services/market-ops-content.ts +277 -0
  58. package/src/services/market-ops-plugin.ts +655 -0
  59. package/src/services/market-ops-publish.ts +1227 -0
  60. package/src/services/market-ops-repo.ts +916 -0
  61. package/src/services/market-ops-skill.ts +788 -0
  62. package/src/services/market-ops.ts +686 -0
  63. package/src/services/market-publish.ts +468 -0
  64. package/src/services/market-registry.ts +382 -0
  65. package/src/services/market-types.ts +268 -0
  66. package/src/services/marketplace-conflicts.ts +127 -0
  67. package/src/services/marketplace-refresh.ts +33 -3
  68. package/src/services/marketplace-sync.ts +253 -22
  69. package/src/services/plugin-dependencies.ts +17 -0
  70. package/src/services/plugin-manager.ts +1 -1
  71. package/src/services/plugin-mcp-config.ts +6 -9
  72. package/src/services/plugin-requires.ts +20 -8
  73. package/src/services/profile-absorb.ts +88 -3
  74. package/src/services/profile-adopt.ts +10 -1
  75. package/src/services/profiles.ts +64 -1
  76. package/src/services/resolver.ts +17 -5
  77. package/src/services/settings-manager.ts +2 -2
  78. package/src/services/skills-manager.ts +2 -2
  79. package/src/services/update-plan.ts +67 -8
  80. package/src/tui-runtime.tsx +142 -0
  81. package/src/tui.tsx +7 -124
  82. package/src/types/index.ts +7 -0
  83. package/src/ui/adapters/marketAdapter.ts +366 -0
  84. package/src/ui/adapters/pluginsAdapter.ts +45 -3
  85. package/src/ui/components/layout/ScreenLayout.tsx +43 -17
  86. package/src/ui/components/modals/ConfirmModal.tsx +145 -8
  87. package/src/ui/components/modals/HelpOverlay.tsx +10 -3
  88. package/src/ui/components/modals/ModalContainer.tsx +47 -5
  89. package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
  90. package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
  91. package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
  92. package/src/ui/components/primitives/SelectableRow.tsx +14 -1
  93. package/src/ui/components/primitives/ToneLine.tsx +25 -0
  94. package/src/ui/components/primitives/index.ts +5 -0
  95. package/src/ui/keymap.ts +21 -5
  96. package/src/ui/market/MarketApp.tsx +59 -0
  97. package/src/ui/market/MarketplaceScreen.tsx +280 -0
  98. package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
  99. package/src/ui/market/MarketplacesScreen.tsx +264 -0
  100. package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
  101. package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
  102. package/src/ui/market/marketKeymap.ts +243 -0
  103. package/src/ui/market/useMarketState.ts +694 -0
  104. package/src/ui/renderers/pluginRenderers.tsx +31 -3
  105. package/src/ui/renderers/pluginStatusText.ts +6 -0
  106. package/src/ui/screens/PluginsScreen.tsx +49 -5
  107. package/src/ui/screens/SkillsScreen.tsx +2 -2
  108. package/src/ui/state/AppContext.tsx +14 -2
  109. package/src/ui/state/types.ts +9 -2
  110. package/src/utils/config-dir.ts +29 -9
  111. package/src/utils/display-path.ts +33 -0
@@ -0,0 +1,183 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ ID,
6
+ MANIFEST,
7
+ MarketFixture,
8
+ PLUGIN_MANIFEST,
9
+ SKILL,
10
+ SKILL_REL,
11
+ readJson,
12
+ sourceModule,
13
+ write,
14
+ writeJson,
15
+ } from "./market-fixture.js";
16
+
17
+ let f: MarketFixture | undefined;
18
+ afterEach(() => {
19
+ f?.close();
20
+ f = undefined;
21
+ });
22
+
23
+ test("TEST-21 REQ-8 plugin creation commits the specified layout; deletion commits only that plugin", () => {
24
+ f = new MarketFixture();
25
+ const repo = f.seed();
26
+ f.register(repo);
27
+ const before = f.head(repo);
28
+
29
+ const created = f.market([
30
+ "plugin",
31
+ "create",
32
+ "new-kit@acme",
33
+ "--description",
34
+ "New tools",
35
+ "--yes",
36
+ ]);
37
+
38
+ expect(created.code).toBe(0);
39
+ const plugin = f.remoteJson(
40
+ repo,
41
+ "plugins/new-kit/.claude-plugin/plugin.json",
42
+ );
43
+ expect(plugin).toEqual({
44
+ name: "new-kit",
45
+ version: "0.1.0",
46
+ description: "New tools",
47
+ author: { name: "acme" },
48
+ });
49
+ expect(f.remoteJson(repo, MANIFEST).plugins).toContainEqual({
50
+ name: "new-kit",
51
+ source: "./plugins/new-kit",
52
+ description: "New tools",
53
+ version: "0.1.0",
54
+ });
55
+ expect(f.head(repo)).not.toBe(before);
56
+ const afterCreate = f.head(repo);
57
+
58
+ const deleted = f.market(["plugin", "delete", "new-kit@acme", "--yes"]);
59
+
60
+ expect(deleted.code).toBe(0);
61
+ expect(f.head(repo)).not.toBe(afterCreate);
62
+ expect(f.remotePaths(repo)).not.toContain(
63
+ "plugins/new-kit/.claude-plugin/plugin.json",
64
+ );
65
+ expect(f.remoteFile(repo, SKILL_REL)).toBe(SKILL.trimEnd());
66
+ f.assertOwned();
67
+ }, 60_000);
68
+
69
+ test("TEST-21 REQ-8 external plugin sources are not writable targets", () => {
70
+ f = new MarketFixture();
71
+ const repo = f.seed();
72
+ const manifest = readJson(join(repo.author, MANIFEST));
73
+ manifest.plugins[0].source = { source: "github", repo: "other/repo" };
74
+ writeJson(join(repo.author, MANIFEST), manifest);
75
+ f.push(repo);
76
+ f.register(repo);
77
+ write(join(f.folder("project", "new-skill"), "SKILL.md"), "New skill\n");
78
+ const head = f.head(repo);
79
+
80
+ const r = f.market(["skill", "add", "new-skill", "--plugin", ID, "--yes"]);
81
+
82
+ expect(r.code).toBe(1);
83
+ expect(r.result.error?.code).toBe("external-plugin");
84
+ expect(f.head(repo)).toBe(head);
85
+ f.assertOwned();
86
+ }, 30_000);
87
+
88
+ test.each(["project", "global"] as const)(
89
+ "TEST-22 REQ-9 adds a %s skill, then commits skill removal without deleting the last local copy",
90
+ (from) => {
91
+ f = new MarketFixture();
92
+ const repo = f.seed();
93
+ f.register(repo);
94
+ f.install();
95
+ const folder = f.folder(from, "new-skill");
96
+ write(
97
+ join(folder, "SKILL.md"),
98
+ "---\nname: new-skill\n---\nA new skill.\n",
99
+ );
100
+ const before = f.head(repo);
101
+
102
+ const added = f.market([
103
+ "skill",
104
+ "add",
105
+ "new-skill",
106
+ "--plugin",
107
+ ID,
108
+ "--from",
109
+ from,
110
+ "--keep-local",
111
+ "--yes",
112
+ ]);
113
+
114
+ expect(added.code).toBe(0);
115
+ expect(added.result.version).toEqual({ from: "0.3.1", to: "0.3.2" });
116
+ expect(f.head(repo)).not.toBe(before);
117
+ expect(
118
+ f.remoteFile(repo, "plugins/review-kit/skills/new-skill/SKILL.md"),
119
+ ).toContain("A new skill.");
120
+ expect(f.remoteJson(repo, MANIFEST).plugins[0].version).toBe("0.3.2");
121
+ expect(f.remoteJson(repo, PLUGIN_MANIFEST).version).toBe("0.3.2");
122
+
123
+ const removed = f.market([
124
+ "skill",
125
+ "remove",
126
+ "new-skill",
127
+ "--plugin",
128
+ ID,
129
+ "--yes",
130
+ ]);
131
+
132
+ expect(removed.code).toBe(0);
133
+ expect(f.remotePaths(repo)).not.toContain(
134
+ "plugins/review-kit/skills/new-skill/SKILL.md",
135
+ );
136
+ expect(f.remoteJson(repo, MANIFEST).plugins[0].version).toBe("0.3.3");
137
+ expect(f.remoteJson(repo, PLUGIN_MANIFEST).version).toBe("0.3.3");
138
+ expect(readFileSync(join(folder, "SKILL.md"), "utf8")).toContain(
139
+ "A new skill.",
140
+ );
141
+ expect(removed.result.localCopy?.action).toBe("kept");
142
+ const base = f.value(
143
+ `const b=await import(${JSON.stringify(sourceModule("services/market-bases.js"))}); console.log(JSON.stringify(b.readBase(${JSON.stringify(folder)})));`,
144
+ );
145
+ expect(base).toBeNull();
146
+ f.assertOwned();
147
+ },
148
+ 60_000,
149
+ );
150
+
151
+ test("TEST-22 REQ-23 adding an existing skill rejects the collision without a commit", () => {
152
+ f = new MarketFixture();
153
+ const repo = f.seed();
154
+ f.register(repo);
155
+ write(join(f.folder(), "SKILL.md"), SKILL);
156
+ const head = f.head(repo);
157
+
158
+ const r = f.market(["skill", "add", "review", "--plugin", ID, "--yes"]);
159
+
160
+ expect(r.code).toBe(1);
161
+ expect(r.result.error?.code).toBe("name-taken");
162
+ expect(JSON.stringify(r.result)).toContain("publish");
163
+ expect(f.head(repo)).toBe(head);
164
+ expect(existsSync(f.folder())).toBe(true);
165
+ f.assertOwned();
166
+ }, 30_000);
167
+
168
+ test("TEST-23 REQ-10 skill edit refuses an existing local target", () => {
169
+ f = new MarketFixture();
170
+ const repo = f.seed();
171
+ f.register(repo);
172
+ write(join(f.folder(), "SKILL.md"), "Do not overwrite me\n");
173
+ const head = f.head(repo);
174
+
175
+ const r = f.market(["skill", "edit", "review", "--plugin", ID, "--yes"]);
176
+
177
+ expect(r.code).toBe(1);
178
+ expect(r.result.error?.code).toBe("name-taken");
179
+ expect(readFileSync(join(f.folder(), "SKILL.md"), "utf8")).toBe(
180
+ "Do not overwrite me\n",
181
+ );
182
+ expect(f.head(repo)).toBe(head);
183
+ }, 30_000);
@@ -0,0 +1,308 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { chmodSync, existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ EDITED,
6
+ ID,
7
+ MarketFixture,
8
+ SKILL_REL,
9
+ expectPublished,
10
+ write,
11
+ } from "./market-fixture.js";
12
+
13
+ let f: MarketFixture | undefined;
14
+ afterEach(() => {
15
+ f?.close();
16
+ f = undefined;
17
+ });
18
+
19
+ test("TEST-46 REQ-22 publish honours the isolated repository's required signing instead of bypassing it", async () => {
20
+ f = new MarketFixture();
21
+ const repo = await f.prepared();
22
+ const head = f.head(repo);
23
+ const signer = join(f.bin, "test-signer");
24
+ write(
25
+ signer,
26
+ `#!${process.execPath} --no-env-file\nprocess.stderr.write('Test signer refused approval\\n'); process.exit(1);\n`,
27
+ );
28
+ chmodSync(signer, 0o755);
29
+ f.git(f.root, "config", "--global", "gpg.program", signer);
30
+ f.git(f.root, "config", "--global", "gpg.format", "openpgp");
31
+ f.git(f.root, "config", "--global", "commit.gpgsign", "true");
32
+
33
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
34
+
35
+ expect(r.code).toBe(1);
36
+ expect(f.head(repo)).toBe(head);
37
+ expect(f.version()).toBe("0.3.1");
38
+ expect(existsSync(f.folder())).toBe(true);
39
+ f.assertOwned();
40
+ }, 60_000);
41
+
42
+ test.each(["protected", "fork"] as const)(
43
+ "TEST-28 REQ-13 %s PR preserves default branch, install and local copy",
44
+ async (kind) => {
45
+ f = new MarketFixture();
46
+ const repo = await f.prepared();
47
+ const head = f.head(repo);
48
+ if (kind === "protected") f.protect(repo);
49
+ else
50
+ f.setModes({
51
+ push: false,
52
+ forks: true,
53
+ forkName: "teammate/renamed-market",
54
+ });
55
+
56
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
57
+
58
+ expect(r.code).toBe(0);
59
+ expect(r.result.outcome).toBe("pull-request");
60
+ expect(r.result.pullRequest).toMatch(
61
+ /^https:\/\/github\.com\/acme\/market\/pull\//,
62
+ );
63
+ expect(f.head(repo)).toBe(head);
64
+ expect(f.version()).toBe("0.3.1");
65
+ expect(readFileSync(join(f.folder(), "SKILL.md"), "utf8")).toBe(EDITED);
66
+ if (r.result.localCopy !== undefined)
67
+ expect(r.result.localCopy.action).toBe("kept");
68
+ const remote =
69
+ kind === "protected"
70
+ ? repo.bare
71
+ : join(f.root, "remotes", "teammate", "renamed-market.git");
72
+ const branches = f
73
+ .git(
74
+ f.root,
75
+ "--git-dir",
76
+ remote,
77
+ "for-each-ref",
78
+ "--format=%(refname:short)",
79
+ "refs/heads",
80
+ )
81
+ .split("\n");
82
+ const branch = branches.find((name) => name.startsWith("magus/"));
83
+ expect(branch).toBeDefined();
84
+ expect(
85
+ f.git(f.root, "--git-dir", remote, "show", `${branch}:${SKILL_REL}`),
86
+ ).toBe(EDITED.trimEnd());
87
+ f.assertOwned();
88
+ },
89
+ 60_000,
90
+ );
91
+
92
+ test.each([
93
+ { modes: { push: false, forks: false }, error: "fork-not-allowed" },
94
+ {
95
+ modes: {
96
+ push: false,
97
+ forks: true,
98
+ forkName: "other/market",
99
+ forkParent: "acme/market",
100
+ },
101
+ error: null,
102
+ },
103
+ {
104
+ modes: {
105
+ push: false,
106
+ forks: true,
107
+ forkName: "teammate/market",
108
+ forkParent: "wrong/parent",
109
+ },
110
+ error: null,
111
+ },
112
+ ])(
113
+ "TEST-28 REQ-13 refused or misidentified forks never receive a publish: %j",
114
+ async ({ modes, error }) => {
115
+ f = new MarketFixture();
116
+ const repo = await f.prepared();
117
+ f.setModes(modes);
118
+ const head = f.head(repo);
119
+
120
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
121
+
122
+ expect(r.code).toBe(1);
123
+ expect(r.result.outcome).toBe("failed");
124
+ if (error) expect(r.result.error?.code).toBe(error);
125
+ expect(f.head(repo)).toBe(head);
126
+ expect(f.version()).toBe("0.3.1");
127
+ expect(existsSync(f.folder())).toBe(true);
128
+ if ("forkName" in modes) {
129
+ const fork = join(f.root, "remotes", `${modes.forkName}.git`);
130
+ if (existsSync(fork))
131
+ expect(
132
+ f.git(
133
+ f.root,
134
+ "--git-dir",
135
+ fork,
136
+ "for-each-ref",
137
+ "--format=%(refname:short)",
138
+ "refs/heads",
139
+ ),
140
+ ).toBe("main");
141
+ }
142
+ f.assertOwned();
143
+ },
144
+ 60_000,
145
+ );
146
+
147
+ test.each([false, true])(
148
+ "TEST-28 REQ-13 a denied push cannot silently change target to a fork; gh missing=%s",
149
+ async (missingGh) => {
150
+ f = new MarketFixture();
151
+ const repo = await f.prepared();
152
+ f.protect(repo, true);
153
+ if (missingGh) f.missingGh();
154
+ const head = f.head(repo);
155
+
156
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
157
+
158
+ expect(r.code).toBe(1);
159
+ expect(r.result.error?.code).toBe("no-access");
160
+ expect(f.head(repo)).toBe(head);
161
+ expect(f.version()).toBe("0.3.1");
162
+ expect(existsSync(join(f.root, "remotes", "teammate"))).toBe(false);
163
+ expect(existsSync(f.folder())).toBe(true);
164
+ f.assertOwned();
165
+ },
166
+ 60_000,
167
+ );
168
+
169
+ test("TEST-28 REQ-32 a failed PR after branch push never claims nothing changed", async () => {
170
+ f = new MarketFixture();
171
+ const repo = await f.prepared();
172
+ f.protect(repo);
173
+ f.setModes({ prFail: true });
174
+ const head = f.head(repo);
175
+
176
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
177
+
178
+ expect(r.code).toBe(1);
179
+ expect(r.result.message.toLowerCase()).not.toContain("nothing changed");
180
+ expect(
181
+ f.git(
182
+ f.root,
183
+ "--git-dir",
184
+ repo.bare,
185
+ "for-each-ref",
186
+ "--format=%(refname:short)",
187
+ "refs/heads",
188
+ ),
189
+ ).toContain("magus/");
190
+ expect(f.head(repo)).toBe(head);
191
+ expect(existsSync(f.folder())).toBe(true);
192
+ f.assertOwned();
193
+ }, 60_000);
194
+
195
+ test.each([
196
+ { order: ["user", "project"] as const },
197
+ { order: ["project", "user"] as const },
198
+ ])(
199
+ "TEST-29 REQ-14 USER and PROJECT publishes succeed in registry order %j",
200
+ ({ order }) => {
201
+ f = new MarketFixture();
202
+ const repo = f.seed();
203
+ f.register(repo);
204
+ for (const scope of order) f.install(ID, scope);
205
+ f.edit();
206
+ write(join(f.folder(), "SKILL.md"), EDITED);
207
+
208
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
209
+
210
+ expect(r.code).toBe(0);
211
+ expect(r.result.outcome).toBe("ok");
212
+ expect(r.result.installs?.map((row) => row.status)).toEqual([
213
+ "updated",
214
+ "updated",
215
+ ]);
216
+ expect(r.result.installs?.every((row) => row.loaded === "0.3.2")).toBe(
217
+ true,
218
+ );
219
+ expect(existsSync(f.folder())).toBe(false);
220
+ expectPublished(f, repo);
221
+ f.assertOwned();
222
+ },
223
+ 60_000,
224
+ );
225
+
226
+ test.each(["cli-error", "dirty-clone", "misdirected"] as const)(
227
+ "TEST-29 REQ-14 install move %s is partial after a real push and keeps the edit",
228
+ async (mode) => {
229
+ f = new MarketFixture();
230
+ const repo = await f.prepared();
231
+ if (mode === "cli-error")
232
+ f.setModes({ failInstall: "Plugin not found in marketplace" });
233
+ if (mode === "dirty-clone")
234
+ write(
235
+ join(f.clone(repo), "README.md"),
236
+ "Local changes in Claude's clone\n",
237
+ );
238
+ if (mode === "misdirected") {
239
+ const other = join(f.root, "other-project");
240
+ f.git(f.project, "worktree", "add", "-b", "other", other);
241
+ f.install(ID, "project", other);
242
+ f.install(ID, "project");
243
+ f.setModes({ misdirect: true });
244
+ }
245
+
246
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
247
+
248
+ expect(r.code).toBe(1);
249
+ expect(r.result.outcome).toBe("partial");
250
+ expect(r.result.error?.code).toBe("install-failed");
251
+ expect(r.result.installs?.some((row) => row.status === "failed")).toBe(
252
+ true,
253
+ );
254
+ expect(
255
+ r.result.installs
256
+ ?.filter((row) => row.status === "failed")
257
+ .every((row) => row.fix?.includes("magus update")),
258
+ ).toBe(true);
259
+ expect(r.result.localCopy?.action).toBe("kept");
260
+ expect(readFileSync(join(f.folder(), "SKILL.md"), "utf8")).toBe(EDITED);
261
+ expectPublished(f, repo);
262
+ expect(r.result.message.toLowerCase()).not.toContain("nothing changed");
263
+ f.assertOwned();
264
+ },
265
+ 60_000,
266
+ );
267
+
268
+ test.each([
269
+ { mode: "missing", exit: 1, error: "gh-missing" },
270
+ { mode: "auth", exit: 4, error: "gh-auth" },
271
+ { mode: "network", exit: 1, error: "no-network" },
272
+ ])(
273
+ "TEST-46 REQ-22 create handles gh $mode with exit $exit",
274
+ ({ mode, exit, error }) => {
275
+ f = new MarketFixture();
276
+ if (mode === "missing") f.missingGh();
277
+ if (mode === "auth") f.setModes({ auth: false });
278
+ if (mode === "network") f.setModes({ ghNetwork: true });
279
+ const before = f.snapshot();
280
+
281
+ const r = f.market(["create", "acme/new-market", "--private", "--yes"]);
282
+
283
+ expect(r.code).toBe(exit);
284
+ expect(r.result.error?.code).toBe(error);
285
+ expect(f.snapshot()).toEqual(before);
286
+ f.assertOwned();
287
+ },
288
+ 30_000,
289
+ );
290
+
291
+ test("TEST-46 REQ-22 validation failure never pushes or removes the source", async () => {
292
+ f = new MarketFixture();
293
+ const repo = await f.prepared();
294
+ const head = f.head(repo);
295
+ f.setModes({ validateFail: true });
296
+
297
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
298
+
299
+ expect(r.code).toBe(1);
300
+ expect(r.result.error?.code).toBe("validate-failed");
301
+ expect(f.head(repo)).toBe(head);
302
+ expect(existsSync(f.folder())).toBe(true);
303
+ expect(f.git(f.working(repo), "diff", "--name-only")).toBe("");
304
+ expect(
305
+ f.git(f.working(repo), "ls-files", "--others", "--exclude-standard"),
306
+ ).toBe("");
307
+ f.assertOwned();
308
+ }, 60_000);