magus-cli 7.13.0 → 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.
- package/README.md +7 -4
- package/package.json +4 -4
- package/src/__tests__/market-r2-adapters.test.ts +190 -0
- package/src/__tests__/market-r2-company-cli.test.ts +350 -0
- package/src/__tests__/market-r2-company-unit.test.ts +224 -0
- package/src/__tests__/market-r2-local-items.test.ts +197 -0
- package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
- package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
- package/src/__tests__/market-r2-routing.test.ts +127 -0
- package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
- package/src/__tests__/market-r2b-display-path.test.ts +180 -0
- package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
- package/src/__tests__/market-r2b-local-items.test.ts +253 -0
- package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
- package/src/__tests__/market-router.test.ts +4 -4
- package/src/cli/market-view.ts +59 -18
- package/src/cli/market.ts +128 -44
- package/src/cli/router.ts +42 -10
- package/src/data/marketplaces.ts +1 -0
- package/src/main.tsx +1 -1
- package/src/market-tui.tsx +1 -1
- package/src/services/local-marketplace.ts +8 -0
- package/src/services/market-change.ts +6 -6
- package/src/services/market-local.ts +93 -0
- package/src/services/market-ops-plugin.ts +366 -47
- package/src/services/market-ops-publish.ts +363 -15
- package/src/services/market-ops-repo.ts +94 -13
- package/src/services/market-ops-skill.ts +93 -11
- package/src/services/market-ops.ts +72 -8
- package/src/services/market-publish.ts +10 -1
- package/src/services/market-types.ts +23 -0
- package/src/tui-runtime.tsx +2 -2
- package/src/types/index.ts +2 -0
- package/src/ui/adapters/marketAdapter.ts +123 -4
- package/src/ui/adapters/pluginsAdapter.ts +25 -1
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/market/MarketplaceScreen.tsx +55 -24
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
- package/src/ui/market/MarketplacesScreen.tsx +120 -29
- package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
- package/src/ui/market/marketKeymap.ts +29 -2
- package/src/ui/market/useMarketState.ts +146 -5
- 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
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "7.14.0",
|
|
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.
|
|
69
|
-
"magus-cli-darwin-x64": "7.
|
|
70
|
-
"magus-cli-linux-x64": "7.
|
|
68
|
+
"magus-cli-darwin-arm64": "7.14.0",
|
|
69
|
+
"magus-cli-darwin-x64": "7.14.0",
|
|
70
|
+
"magus-cli-linux-x64": "7.14.0"
|
|
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
|
+
);
|