magus-cli 7.13.0 → 7.14.1

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 (46) hide show
  1. package/README.md +7 -4
  2. package/package.json +4 -4
  3. package/src/__tests__/market-r2-adapters.test.ts +190 -0
  4. package/src/__tests__/market-r2-company-cli.test.ts +350 -0
  5. package/src/__tests__/market-r2-company-unit.test.ts +224 -0
  6. package/src/__tests__/market-r2-local-items.test.ts +197 -0
  7. package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
  8. package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
  9. package/src/__tests__/market-r2-routing.test.ts +127 -0
  10. package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
  11. package/src/__tests__/market-r2b-display-path.test.ts +180 -0
  12. package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
  13. package/src/__tests__/market-r2b-local-items.test.ts +253 -0
  14. package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
  15. package/src/__tests__/market-router.test.ts +4 -4
  16. package/src/__tests__/scope-uninstall-wrong-scope.test.ts +341 -0
  17. package/src/cli/market-view.ts +59 -18
  18. package/src/cli/market.ts +128 -44
  19. package/src/cli/router.ts +42 -10
  20. package/src/data/marketplaces.ts +1 -0
  21. package/src/main.tsx +1 -1
  22. package/src/market-tui.tsx +1 -1
  23. package/src/services/claude-cli.ts +13 -0
  24. package/src/services/local-marketplace.ts +8 -0
  25. package/src/services/market-change.ts +6 -6
  26. package/src/services/market-local.ts +93 -0
  27. package/src/services/market-ops-plugin.ts +366 -47
  28. package/src/services/market-ops-publish.ts +363 -15
  29. package/src/services/market-ops-repo.ts +94 -13
  30. package/src/services/market-ops-skill.ts +93 -11
  31. package/src/services/market-ops.ts +72 -8
  32. package/src/services/market-publish.ts +10 -1
  33. package/src/services/market-types.ts +23 -0
  34. package/src/services/update-engine.ts +97 -1
  35. package/src/tui-runtime.tsx +2 -2
  36. package/src/types/index.ts +2 -0
  37. package/src/ui/adapters/marketAdapter.ts +123 -4
  38. package/src/ui/adapters/pluginsAdapter.ts +25 -1
  39. package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
  40. package/src/ui/market/MarketplaceScreen.tsx +55 -24
  41. package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
  42. package/src/ui/market/MarketplacesScreen.tsx +120 -29
  43. package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
  44. package/src/ui/market/marketKeymap.ts +29 -2
  45. package/src/ui/market/useMarketState.ts +146 -5
  46. package/src/utils/display-path.ts +33 -0
package/README.md CHANGED
@@ -149,15 +149,18 @@ check. So does `magus install --check`.
149
149
  ## Company marketplaces
150
150
 
151
151
  A company marketplace is your team's own Claude Code plugin marketplace on GitHub.
152
- magus lists it first, and `magus market` manages it: create or connect one, add
153
- plugins and skills, copy a skill here with `market skill edit`, and send the edits
154
- back with `market publish` as the plugin's next version. Every change prints its
152
+ magus lists it first, shown as "by" its company name, and `magus edit` (its screens) or
153
+ the `magus market` commands manage it: create or connect one, add
154
+ plugins and skills (a skill of your own, or a plugin folder of your own with
155
+ `market plugin add`), copy a skill here with `market skill edit`, and send the edits
156
+ back with `market publish` as the plugin's next version, one skill or every edited
157
+ skill of a plugin at once. Every change prints its
155
158
  plan and asks first; `--yes=<planId>` applies exactly that plan in a script.
156
159
  Teammates get the marketplace and every new version through `magus update` with
157
160
  the project's profile.
158
161
 
159
162
  ```
160
- magus market Open the company marketplace screens
163
+ magus edit Open the company marketplace screens
161
164
  magus market --help Every market command, its flags and exit codes
162
165
  ```
163
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.13.0",
3
+ "version": "7.14.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -65,8 +65,8 @@
65
65
  "typescript": "^5.6.3"
66
66
  },
67
67
  "optionalDependencies": {
68
- "magus-cli-darwin-arm64": "7.13.0",
69
- "magus-cli-darwin-x64": "7.13.0",
70
- "magus-cli-linux-x64": "7.13.0"
68
+ "magus-cli-darwin-arm64": "7.14.1",
69
+ "magus-cli-darwin-x64": "7.14.1",
70
+ "magus-cli-linux-x64": "7.14.1"
71
71
  }
72
72
  }
@@ -0,0 +1,190 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { MarketOverviewEntry } from "../services/market-ops.js";
3
+ import { companySkillNames } from "../services/market-ops.js";
4
+ import type { LocalItem, SkillStatusRow } from "../services/market-types.js";
5
+ import {
6
+ editedSkills,
7
+ marketplaceItems,
8
+ marketplacesItems,
9
+ nextSelectable,
10
+ pluginsToPush,
11
+ selectable,
12
+ } from "../ui/adapters/marketAdapter.js";
13
+
14
+ function status(
15
+ skill: string,
16
+ state: SkillStatusRow["state"],
17
+ owner: string | null = "review-kit@acme",
18
+ from: SkillStatusRow["from"] = "project",
19
+ ): SkillStatusRow {
20
+ return { skill, state, owner, from, path: [from, skill].join("/") };
21
+ }
22
+ const overview = (): MarketOverviewEntry => ({
23
+ name: "acme",
24
+ repo: "acme/market",
25
+ company: "10xlabs",
26
+ scope: "user",
27
+ freshness: "fresh",
28
+ reason: "",
29
+ fix: "",
30
+ commit: "a".repeat(40),
31
+ plugins: [
32
+ {
33
+ name: "review-kit",
34
+ version: "0.3.1",
35
+ skills: ["review", "lint", "unchanged"],
36
+ },
37
+ { name: "deploy-kit", version: "1.0.0", skills: ["deploy"] },
38
+ ],
39
+ });
40
+
41
+ test("R3.1 TEST-7 editedSkills counts each owned edited skill once and excludes every other state", () => {
42
+ const rows: SkillStatusRow[] = [
43
+ status("review", "edited"),
44
+ status("review", "edited", "review-kit@acme", "user"),
45
+ status("lint", "edited"),
46
+ status("unknown-origin", "no-base"),
47
+ status("both-changed", "conflict"),
48
+ status("unchanged", "same"),
49
+ status("remote-newer", "plugin-newer"),
50
+ status("offline", "not-checked"),
51
+ status("ambiguous", "ambiguous", null),
52
+ status("unowned", "not-owned", null),
53
+ status("elsewhere", "edited", "review-kit@beta"),
54
+ ];
55
+ const before = JSON.stringify(rows);
56
+
57
+ expect(editedSkills(rows, "review-kit@acme").sort()).toEqual([
58
+ "lint",
59
+ "review",
60
+ ]);
61
+ expect(editedSkills(rows, "missing@acme")).toEqual([]);
62
+ expect(JSON.stringify(rows)).toBe(before);
63
+ });
64
+
65
+ test("R3.1 TEST-7 pluginsToPush counts plugins rather than skills and isolates the marketplace", () => {
66
+ const rows = [
67
+ status("review", "edited"),
68
+ status("lint", "edited"),
69
+ status("review", "edited", "review-kit@acme", "user"),
70
+ status("deploy", "edited", "deploy-kit@acme"),
71
+ status("blocked", "conflict", "blocked-kit@acme"),
72
+ status("foreign", "edited", "review-kit@beta"),
73
+ ];
74
+
75
+ expect(pluginsToPush("acme", rows).sort()).toEqual([
76
+ "deploy-kit",
77
+ "review-kit",
78
+ ]);
79
+ expect(pluginsToPush("missing", rows)).toEqual([]);
80
+ });
81
+
82
+ test("R3.1 TEST-8 marketplace and plugin rows carry edited plugin and skill counts without expansion", () => {
83
+ const entry = overview();
84
+ const states = [
85
+ status("review", "edited"),
86
+ status("lint", "edited"),
87
+ status("unchanged", "same"),
88
+ status("deploy", "no-base", "deploy-kit@acme"),
89
+ ];
90
+ const before = JSON.stringify({ entry, states });
91
+
92
+ const first = marketplacesItems([entry], states);
93
+ const marketplace = first.find((item) => item.kind === "marketplace");
94
+ expect(marketplace?.kind).toBe("marketplace");
95
+ if (marketplace?.kind !== "marketplace")
96
+ throw new Error("Marketplace row missing");
97
+ expect(marketplace.toPush).toEqual(["review-kit"]);
98
+
99
+ const plugins = marketplaceItems(entry, {}, states, new Set());
100
+ expect(plugins.map((item) => item.kind)).toEqual(["plugin", "plugin"]);
101
+ const review = plugins.find(
102
+ (item) => item.kind === "plugin" && item.ref === "review-kit@acme",
103
+ );
104
+ const deploy = plugins.find(
105
+ (item) => item.kind === "plugin" && item.ref === "deploy-kit@acme",
106
+ );
107
+ expect(
108
+ review?.kind === "plugin" ? [...review.edited].sort() : undefined,
109
+ ).toEqual(["lint", "review"]);
110
+ expect(deploy?.kind === "plugin" ? deploy.edited : undefined).toEqual([]);
111
+ expect(JSON.stringify({ entry, states })).toBe(before);
112
+ });
113
+
114
+ test("R4.1 TEST-9 local heading and items follow company marketplaces and add actions", () => {
115
+ const local: LocalItem[] = [
116
+ {
117
+ kind: "skill",
118
+ name: "authoring",
119
+ scope: "project",
120
+ path: "project/authoring",
121
+ },
122
+ { kind: "plugin", name: "my-kit", scope: "user", path: "user/my-kit" },
123
+ ];
124
+ const rows = marketplacesItems([overview()], [], local);
125
+
126
+ expect(rows.map((item) => item.kind)).toEqual([
127
+ "marketplace",
128
+ "create",
129
+ "connect",
130
+ "local-heading",
131
+ "local",
132
+ "local",
133
+ ]);
134
+ expect(rows[3]).toEqual({ kind: "local-heading", count: 2 });
135
+ expect(rows.slice(4)).toEqual(local.map((item) => ({ kind: "local", item })));
136
+ expect(selectable(rows[3])).toBe(false);
137
+ expect(selectable(rows[4])).toBe(true);
138
+ expect(nextSelectable(rows, 2, 1)).toBe(4);
139
+ expect(nextSelectable(rows, 4, -1)).toBe(2);
140
+ expect(nextSelectable(rows, 5, 1)).toBe(5);
141
+ });
142
+
143
+ test("R4.1 R4.4 TEST-9 local-only rows remain selectable without any marketplaces; no empty heading", () => {
144
+ const item: LocalItem = {
145
+ kind: "skill",
146
+ name: "solo",
147
+ scope: "project",
148
+ path: "project/solo",
149
+ };
150
+ const rows = marketplacesItems([], [], [item]);
151
+
152
+ expect(rows).toEqual([
153
+ { kind: "create" },
154
+ { kind: "connect" },
155
+ { kind: "local-heading", count: 1 },
156
+ { kind: "local", item },
157
+ ]);
158
+ expect(nextSelectable(rows, 1, 1)).toBe(3);
159
+ expect(marketplacesItems([], [], [])).toEqual([
160
+ { kind: "create" },
161
+ { kind: "connect" },
162
+ ]);
163
+ });
164
+
165
+ test("R4.1 TEST-30 companySkillNames unions every plugin's skill names without duplicates", () => {
166
+ const marketplaces = [
167
+ { plugins: overview().plugins },
168
+ {
169
+ plugins: [
170
+ {
171
+ name: "other-kit",
172
+ version: "1.0.0",
173
+ skills: ["review", "authoring"],
174
+ },
175
+ ],
176
+ },
177
+ { plugins: [] },
178
+ ];
179
+ const before = JSON.stringify(marketplaces);
180
+
181
+ expect([...companySkillNames(marketplaces)].sort()).toEqual([
182
+ "authoring",
183
+ "deploy",
184
+ "lint",
185
+ "review",
186
+ "unchanged",
187
+ ]);
188
+ expect([...companySkillNames([])]).toEqual([]);
189
+ expect(JSON.stringify(marketplaces)).toBe(before);
190
+ });
@@ -0,0 +1,350 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { LocalMarketplace } from "../services/local-marketplace.js";
5
+ import type { Marketplace } from "../types/index.js";
6
+ import {
7
+ MANIFEST,
8
+ MarketFixture,
9
+ readJson,
10
+ sourceModule,
11
+ writeJson,
12
+ } from "./market-fixture.js";
13
+
14
+ let f: MarketFixture | undefined;
15
+ afterEach(() => {
16
+ f?.close();
17
+ f = undefined;
18
+ });
19
+
20
+ test.each([
21
+ {
22
+ label: "split flag",
23
+ flags: ["--company", " 10xlabs "],
24
+ company: "10xlabs",
25
+ },
26
+ { label: "equals flag", flags: ["--company=10xlabs"], company: "10xlabs" },
27
+ {
28
+ label: "64 Unicode code points",
29
+ flags: [`--company=${"🏢".repeat(64)}`],
30
+ company: "🏢".repeat(64),
31
+ },
32
+ { label: "GitHub owner default", flags: [], company: "different-owner" },
33
+ ])(
34
+ "R1.1 R1.2 TEST-10 create writes owner.name from $label",
35
+ ({ flags, company }) => {
36
+ f = new MarketFixture();
37
+
38
+ const result = f.market([
39
+ "create",
40
+ "different-owner/team-tools",
41
+ "--private",
42
+ "--name",
43
+ "team",
44
+ ...flags,
45
+ "--yes",
46
+ ]);
47
+
48
+ expect({ exit: result.code, outcome: result.result.outcome }).toEqual({
49
+ exit: 0,
50
+ outcome: "ok",
51
+ });
52
+ const bare = join(f.root, "remotes", "different-owner", "team-tools.git");
53
+ const manifest = JSON.parse(
54
+ f.git(f.root, "--git-dir", bare, "show", `HEAD:${MANIFEST}`),
55
+ );
56
+ expect(manifest.owner).toEqual({ name: company });
57
+ expect(
58
+ f.market(["list"]).result.marketplaces?.find((row) => row.name === "team")
59
+ ?.company,
60
+ ).toBe(company);
61
+ f.assertOwned();
62
+ },
63
+ 60_000,
64
+ );
65
+
66
+ const invalidCompanies = [
67
+ "",
68
+ " ",
69
+ "Acme\nLabs",
70
+ "Acme\rLabs",
71
+ "Acme\tLabs",
72
+ "Acme\u0000Labs",
73
+ "Acme\u007fLabs",
74
+ "Acme
Labs",
75
+ "Acme
Labs",
76
+ "x".repeat(65),
77
+ "🏢".repeat(65),
78
+ ];
79
+ // NUL cannot be represented in an OS argv; its validation is covered by TEST-2.
80
+ for (const command of ["create", "connect"] as const) {
81
+ test.each(invalidCompanies.filter((name) => !name.includes("\u0000")))(
82
+ `R1.3 TEST-11 ${command} rejects company %j before any GitHub work`,
83
+ (company) => {
84
+ f = new MarketFixture();
85
+ const before = f.snapshot();
86
+ const args =
87
+ command === "create"
88
+ ? ["create", "acme/not-created", "--private"]
89
+ : ["connect", "acme/not-created"];
90
+
91
+ const result = f.market([...args, `--company=${company}`, "--yes"]);
92
+
93
+ expect(result.code).toBe(2);
94
+ expect(f.snapshot()).toEqual(before);
95
+ expect(
96
+ existsSync(join(f.root, "remotes", "acme", "not-created.git")),
97
+ ).toBe(false);
98
+ expect(f.journal().filter((event) => event.tool === "gh")).toEqual([]);
99
+ },
100
+ 30_000,
101
+ );
102
+ test(`R1.3 TEST-11 ${command} rejects a missing --company value without writes`, () => {
103
+ f = new MarketFixture();
104
+ const before = f.snapshot();
105
+ const args =
106
+ command === "create"
107
+ ? ["create", "acme/not-created", "--private"]
108
+ : ["connect", "acme/not-created"];
109
+
110
+ const result = f.cli(["market", ...args, "--company"]);
111
+
112
+ expect(result.code).toBe(2);
113
+ expect(f.snapshot()).toEqual(before);
114
+ expect(f.journal()).toEqual([]);
115
+ }, 30_000);
116
+ }
117
+
118
+ test.each(["create", "connect"] as const)(
119
+ "R1.4 R4.5 TEST-12 %s plan displays the trimmed company and binds consent to it",
120
+ (command) => {
121
+ f = new MarketFixture();
122
+ const repo = command === "connect" ? f.seed({ empty: true }) : undefined;
123
+ const base =
124
+ command === "create"
125
+ ? ["create", "acme/new-market", "--private", "--name", "acme"]
126
+ : ["connect", repo!.slug, "--name", "acme"];
127
+ const before = f.snapshot();
128
+ const plan = f.market([...base, "--company", " 10xlabs "]);
129
+ const equivalent = f.market([...base, "--company=10xlabs"]);
130
+
131
+ expect(plan.code).toBe(3);
132
+ expect(plan.result.outcome).toBe("needs-consent");
133
+ expect(plan.result.plan).toContain(
134
+ 'Company name: 10xlabs (magus shows the marketplace as "by 10xlabs")',
135
+ );
136
+ expect(typeof plan.result.planId).toBe("string");
137
+ expect(plan.result.planId?.length).toBeGreaterThan(0);
138
+ expect(equivalent.result.planId).toBe(plan.result.planId);
139
+ expect(f.snapshot()).toEqual(before);
140
+
141
+ const changed = f.market([
142
+ ...base,
143
+ "--company",
144
+ "Other Labs",
145
+ `--yes=${plan.result.planId}`,
146
+ ]);
147
+
148
+ expect({
149
+ exit: changed.code,
150
+ outcome: changed.result.outcome,
151
+ error: changed.result.error?.code,
152
+ }).toEqual({
153
+ exit: 3,
154
+ outcome: "needs-consent",
155
+ error: "plan-changed",
156
+ });
157
+ expect(changed.result.planId).not.toBe(plan.result.planId);
158
+ expect(changed.result.plan).toContain(
159
+ 'Company name: Other Labs (magus shows the marketplace as "by Other Labs")',
160
+ );
161
+ const bare =
162
+ repo?.bare ?? join(f.root, "remotes", "acme", "new-market.git");
163
+ if (repo) {
164
+ expect(
165
+ f.git(f.root, "--git-dir", bare, "rev-list", "--all", "--count"),
166
+ ).toBe("0");
167
+ } else {
168
+ expect(existsSync(bare)).toBe(false);
169
+ }
170
+ // A refused apply may sync its disposable clone; no repository or registration is changed.
171
+ expect(f.market(["list"]).result.marketplaces).toEqual([]);
172
+
173
+ const applied = f.market([
174
+ ...base,
175
+ "--company=10xlabs",
176
+ `--yes=${plan.result.planId}`,
177
+ ]);
178
+ expect(applied.code).toBe(0);
179
+ const manifest = JSON.parse(
180
+ f.git(f.root, "--git-dir", bare, "show", `HEAD:${MANIFEST}`),
181
+ );
182
+ expect(manifest.owner).toEqual({ name: "10xlabs" });
183
+ f.assertOwned();
184
+ },
185
+ 60_000,
186
+ );
187
+
188
+ test.each([
189
+ {
190
+ empty: true,
191
+ companyFlag: ["--company", " 10xlabs "],
192
+ expected: "10xlabs",
193
+ },
194
+ { empty: false, companyFlag: ["--company=10xlabs"], expected: "10xlabs" },
195
+ { empty: true, companyFlag: [], expected: "acme" },
196
+ { empty: false, companyFlag: [], expected: "acme" },
197
+ ])(
198
+ "R1.1 R1.5 TEST-13 connect writes company only when making a manifest: %j",
199
+ ({ empty, companyFlag, expected }) => {
200
+ f = new MarketFixture();
201
+ const repo = f.seed({ empty, noManifest: true });
202
+ const old = empty ? undefined : f.head(repo);
203
+
204
+ const result = f.market([
205
+ "connect",
206
+ repo.slug,
207
+ "--name",
208
+ "acme",
209
+ ...companyFlag,
210
+ "--yes",
211
+ ]);
212
+
213
+ expect(result.code).toBe(0);
214
+ expect(f.remoteJson(repo, MANIFEST).owner).toEqual({ name: expected });
215
+ if (old) {
216
+ expect(
217
+ f.git(
218
+ f.root,
219
+ "--git-dir",
220
+ repo.bare,
221
+ "rev-list",
222
+ "--count",
223
+ `${old}..HEAD`,
224
+ ),
225
+ ).toBe("1");
226
+ expect(f.remoteFile(repo, "README.md")).toBe(
227
+ "Existing repository documentation",
228
+ );
229
+ }
230
+ f.assertOwned();
231
+ },
232
+ 60_000,
233
+ );
234
+
235
+ test.each([true, false])(
236
+ "R1.5 TEST-14 existing marketplace refuses --company, marker=%s",
237
+ (marker) => {
238
+ f = new MarketFixture();
239
+ const repo = f.seed({ marker });
240
+ const manifest = readJson(join(repo.author, MANIFEST));
241
+ manifest.owner = { name: "Manifest Company" };
242
+ writeJson(join(repo.author, MANIFEST), manifest);
243
+ f.push(repo);
244
+ const before = f.snapshot();
245
+ const head = f.head(repo);
246
+
247
+ const refused = f.market([
248
+ "connect",
249
+ repo.slug,
250
+ "--company",
251
+ "Override",
252
+ "--yes",
253
+ ]);
254
+
255
+ expect(refused.code).toBe(2);
256
+ expect(f.head(repo)).toBe(head);
257
+ expect(f.remoteJson(repo, MANIFEST).owner).toEqual({
258
+ name: "Manifest Company",
259
+ });
260
+ expect(f.snapshot()).toEqual(before);
261
+
262
+ const connected = f.market(["connect", repo.slug, "--yes"]);
263
+ expect(connected.code).toBe(0);
264
+ expect(f.remoteJson(repo, MANIFEST).owner).toEqual({
265
+ name: "Manifest Company",
266
+ });
267
+ expect(f.market(["list"]).result.marketplaces?.[0]?.company).toBe(
268
+ "Manifest Company",
269
+ );
270
+ f.assertOwned();
271
+ },
272
+ 60_000,
273
+ );
274
+
275
+ test.each([
276
+ { owner: { name: " 10xlabs " }, expected: "10xlabs" },
277
+ { owner: { name: " " }, expected: undefined },
278
+ { owner: {}, expected: undefined },
279
+ ])(
280
+ "R1.6 TEST-6 scanner and catalog carry trimmed ownerName or omit the key: %j",
281
+ ({ owner, expected }) => {
282
+ f = new MarketFixture(true);
283
+ const repo = f.seed();
284
+ const manifest = readJson(join(repo.author, MANIFEST));
285
+ manifest.owner = owner;
286
+ writeJson(join(repo.author, MANIFEST), manifest);
287
+ f.push(repo);
288
+ f.register(repo);
289
+ const before = f.snapshot();
290
+
291
+ const data = f.value<{
292
+ local: LocalMarketplace;
293
+ catalog: Marketplace;
294
+ localHasName: boolean;
295
+ catalogHasName: boolean;
296
+ }>(
297
+ `const {scanLocalMarketplaces}=await import(${JSON.stringify(sourceModule("services/local-marketplace.js"))});const {getAllMarketplaces}=await import(${JSON.stringify(sourceModule("data/marketplaces.js"))});const locals=await scanLocalMarketplaces(); const local=locals.get('acme'); const catalog=getAllMarketplaces(locals).find(m=>m.name==='acme');console.log(JSON.stringify({local,catalog,localHasName:Object.hasOwn(local,'ownerName'),catalogHasName:Object.hasOwn(catalog,'ownerName')}));`,
298
+ );
299
+
300
+ expect(data.local.ownerName).toBe(expected);
301
+ expect(data.catalog.ownerName).toBe(expected);
302
+ expect(data.localHasName).toBe(expected !== undefined);
303
+ expect(data.catalogHasName).toBe(expected !== undefined);
304
+ expect(f.snapshot()).toEqual(before);
305
+ },
306
+ 30_000,
307
+ );
308
+
309
+ test.each([
310
+ { owner: { name: " 10xlabs " }, company: "10xlabs" },
311
+ { owner: { name: "" }, company: "acme" },
312
+ ])(
313
+ "R1.7 TEST-15 list JSON and text show company without changing state: %j",
314
+ ({ owner, company }) => {
315
+ f = new MarketFixture(true);
316
+ const repo = f.seed();
317
+ const manifest = readJson(join(repo.author, MANIFEST));
318
+ manifest.owner = owner;
319
+ writeJson(join(repo.author, MANIFEST), manifest);
320
+ f.push(repo);
321
+ f.register(repo);
322
+ const before = f.snapshot();
323
+ const head = f.head(repo);
324
+
325
+ const json = f.market(["list"]);
326
+ const text = f.cli(["market", "list"]);
327
+
328
+ expect(json.code).toBe(0);
329
+ expect(
330
+ json.result.marketplaces?.map(
331
+ ({ name, repo: source, company: value, scope }) => ({
332
+ name,
333
+ repo: source,
334
+ company: value,
335
+ scope,
336
+ }),
337
+ ),
338
+ ).toEqual([{ name: "acme", repo: "acme/market", company, scope: "user" }]);
339
+ expect(text.code).toBe(0);
340
+ const line = text.out
341
+ .split("\n")
342
+ .find((line) => line.includes(`by ${company}`));
343
+ expect(line).toBeDefined();
344
+ expect(line).toContain("acme/market");
345
+ expect(line).toContain("USER");
346
+ expect(f.snapshot()).toEqual(before);
347
+ expect(f.head(repo)).toBe(head);
348
+ },
349
+ 30_000,
350
+ );