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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The market screens' state and actions.
|
|
3
3
|
*
|
|
4
4
|
* Reading is read-only: the overview (`marketOverview`), this project's skill
|
|
5
|
-
* copies (`marketStatus`)
|
|
5
|
+
* copies (`marketStatus`), the installs (`installedVersionAt`) and the
|
|
6
|
+
* skills and plugin folders here that no marketplace holds (`localItems`)
|
|
6
7
|
* write nothing, so opening the screens changes nothing on the machine.
|
|
7
8
|
*
|
|
8
9
|
* Every change is a planner from the market services, shown in a confirm
|
|
@@ -14,13 +15,16 @@
|
|
|
14
15
|
|
|
15
16
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
16
17
|
import { viewerLogin, viewerOrgs } from "../../services/github-cli.js";
|
|
18
|
+
import { localItems } from "../../services/market-local.js";
|
|
17
19
|
import {
|
|
20
|
+
planPluginAdd,
|
|
18
21
|
planPluginCreate,
|
|
19
22
|
planPluginDelete,
|
|
20
23
|
} from "../../services/market-ops-plugin.js";
|
|
21
24
|
import {
|
|
22
25
|
localSkillFolders,
|
|
23
26
|
marketStatus,
|
|
27
|
+
planPluginPublish,
|
|
24
28
|
planPublish,
|
|
25
29
|
} from "../../services/market-ops-publish.js";
|
|
26
30
|
import {
|
|
@@ -36,6 +40,8 @@ import {
|
|
|
36
40
|
import {
|
|
37
41
|
type MarketOverviewEntry,
|
|
38
42
|
applyMarketPlan,
|
|
43
|
+
companyPluginNames,
|
|
44
|
+
companySkillNames,
|
|
39
45
|
errorResult,
|
|
40
46
|
marketFetch,
|
|
41
47
|
marketOverview,
|
|
@@ -44,6 +50,7 @@ import {
|
|
|
44
50
|
import { installedVersionAt } from "../../services/market-publish.js";
|
|
45
51
|
import type {
|
|
46
52
|
InstallScope,
|
|
53
|
+
LocalItem,
|
|
47
54
|
MarketResult,
|
|
48
55
|
PlanAnswer,
|
|
49
56
|
SkillStatusRow,
|
|
@@ -51,10 +58,11 @@ import type {
|
|
|
51
58
|
import {
|
|
52
59
|
type ScopeVersions,
|
|
53
60
|
marketFailureReport,
|
|
61
|
+
plural,
|
|
54
62
|
resultNote,
|
|
55
63
|
} from "../adapters/marketAdapter.js";
|
|
56
64
|
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
57
|
-
import type
|
|
65
|
+
import { type AsyncData, asyncValue } from "../state/types.js";
|
|
58
66
|
|
|
59
67
|
export type MarketRoute =
|
|
60
68
|
| { screen: "marketplaces" }
|
|
@@ -66,6 +74,8 @@ export interface MarketData {
|
|
|
66
74
|
status: SkillStatusRow[];
|
|
67
75
|
/** Installed versions by `plugin@marketplace`. */
|
|
68
76
|
installs: Record<string, ScopeVersions>;
|
|
77
|
+
/** Skills and plugin folders here that no company marketplace holds. */
|
|
78
|
+
local: LocalItem[];
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
const SCOPES: readonly InstallScope[] = ["user", "project", "local"];
|
|
@@ -95,11 +105,16 @@ async function readInstalls(
|
|
|
95
105
|
/** Everything the screens draw, read without writing anything. */
|
|
96
106
|
async function readMarketData(cwd: string): Promise<MarketData> {
|
|
97
107
|
const marketplaces = await marketOverview();
|
|
98
|
-
const [status, installs] = await Promise.all([
|
|
108
|
+
const [status, installs, local] = await Promise.all([
|
|
99
109
|
marketStatus(undefined, cwd).then((r) => r.skills ?? []),
|
|
100
110
|
readInstalls(marketplaces, cwd),
|
|
111
|
+
localItems(
|
|
112
|
+
cwd,
|
|
113
|
+
companySkillNames(marketplaces),
|
|
114
|
+
companyPluginNames(marketplaces),
|
|
115
|
+
),
|
|
101
116
|
]);
|
|
102
|
-
return { marketplaces, status, installs };
|
|
117
|
+
return { marketplaces, status, installs, local };
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/** What a confirmed action is, for its dialog and its failure popup. */
|
|
@@ -302,6 +317,14 @@ export function useMarketState() {
|
|
|
302
317
|
0,
|
|
303
318
|
);
|
|
304
319
|
if (!owner) return;
|
|
320
|
+
const company = (
|
|
321
|
+
await modal.input(
|
|
322
|
+
"Create a company marketplace",
|
|
323
|
+
'Company name (shown as "by <name>" in magus):',
|
|
324
|
+
owner,
|
|
325
|
+
)
|
|
326
|
+
)?.trim();
|
|
327
|
+
if (!company) return;
|
|
305
328
|
const repo = (
|
|
306
329
|
await modal.input(
|
|
307
330
|
"Create a company marketplace",
|
|
@@ -325,7 +348,7 @@ export function useMarketState() {
|
|
|
325
348
|
title: `Create ${owner}/${repo}?`,
|
|
326
349
|
subject: `${owner}/${repo}`,
|
|
327
350
|
},
|
|
328
|
-
() => planCreate({ repo: `${owner}/${repo}`, visibility }),
|
|
351
|
+
() => planCreate({ repo: `${owner}/${repo}`, visibility, company }),
|
|
329
352
|
);
|
|
330
353
|
}, [modal, runPlanned, showFailures]);
|
|
331
354
|
|
|
@@ -352,8 +375,124 @@ export function useMarketState() {
|
|
|
352
375
|
[cwd, runPlanned],
|
|
353
376
|
);
|
|
354
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Add a local skill or plugin folder to a company marketplace: pick the
|
|
380
|
+
* marketplace (when there is more than one), then for a skill the plugin
|
|
381
|
+
* it goes into, a new one included. One plan, one consent.
|
|
382
|
+
*/
|
|
383
|
+
const addLocal = useCallback(
|
|
384
|
+
async (item: LocalItem) => {
|
|
385
|
+
const marketplaces = asyncValue(data)?.marketplaces ?? [];
|
|
386
|
+
if (marketplaces.length === 0) {
|
|
387
|
+
await showFailures({
|
|
388
|
+
headline: `No company marketplace to add ${item.name} to`,
|
|
389
|
+
rows: [
|
|
390
|
+
{
|
|
391
|
+
subject: item.name,
|
|
392
|
+
reason: "this machine has no company marketplace yet",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
fix: "Press c to create one, or o to connect one, then press a again",
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const title = `Add ${item.name} to a company marketplace`;
|
|
400
|
+
const marketplace =
|
|
401
|
+
marketplaces.length === 1
|
|
402
|
+
? marketplaces[0]?.name
|
|
403
|
+
: await modal.select(
|
|
404
|
+
title,
|
|
405
|
+
"Which marketplace?",
|
|
406
|
+
marketplaces.map((m) => ({
|
|
407
|
+
label: m.name,
|
|
408
|
+
value: m.name,
|
|
409
|
+
...(m.company ? { meta: `by ${m.company}` } : {}),
|
|
410
|
+
})),
|
|
411
|
+
0,
|
|
412
|
+
);
|
|
413
|
+
if (!marketplace) return;
|
|
414
|
+
if (item.kind === "plugin") {
|
|
415
|
+
await runPlanned(
|
|
416
|
+
{
|
|
417
|
+
title: `Add the plugin ${item.name} to ${marketplace}?`,
|
|
418
|
+
subject: item.name,
|
|
419
|
+
marketplace,
|
|
420
|
+
},
|
|
421
|
+
() => planPluginAdd({ path: item.path, to: marketplace }, cwd),
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const entry = marketplaces.find((m) => m.name === marketplace);
|
|
426
|
+
const NEW = "+new";
|
|
427
|
+
const choice = await modal.select(
|
|
428
|
+
title,
|
|
429
|
+
`Which plugin in ${marketplace}?`,
|
|
430
|
+
[
|
|
431
|
+
...(entry?.plugins ?? []).map((p) => ({
|
|
432
|
+
label: p.name,
|
|
433
|
+
value: p.name,
|
|
434
|
+
meta: plural(p.skills.length, "skill"),
|
|
435
|
+
})),
|
|
436
|
+
{ label: "+ New plugin", value: NEW },
|
|
437
|
+
],
|
|
438
|
+
0,
|
|
439
|
+
);
|
|
440
|
+
if (!choice) return;
|
|
441
|
+
let plugin = choice;
|
|
442
|
+
if (choice === NEW) {
|
|
443
|
+
const name = (
|
|
444
|
+
await modal.input(
|
|
445
|
+
`New plugin in ${marketplace}`,
|
|
446
|
+
`Plugin name (it is created with ${item.name} in it):`,
|
|
447
|
+
item.name,
|
|
448
|
+
)
|
|
449
|
+
)?.trim();
|
|
450
|
+
if (!name) return;
|
|
451
|
+
plugin = name;
|
|
452
|
+
}
|
|
453
|
+
const ref = `${plugin}@${marketplace}`;
|
|
454
|
+
await runPlanned(
|
|
455
|
+
{
|
|
456
|
+
title:
|
|
457
|
+
choice === NEW
|
|
458
|
+
? `Create ${ref} with ${item.name}?`
|
|
459
|
+
: `Add ${item.name} to ${ref}?`,
|
|
460
|
+
subject: item.name,
|
|
461
|
+
marketplace,
|
|
462
|
+
},
|
|
463
|
+
() =>
|
|
464
|
+
planSkillAdd(
|
|
465
|
+
{
|
|
466
|
+
skill: item.name,
|
|
467
|
+
plugin: ref,
|
|
468
|
+
from: item.scope === "user" ? "global" : "project",
|
|
469
|
+
keepLocal: false,
|
|
470
|
+
...(choice === NEW ? { createPlugin: true } : {}),
|
|
471
|
+
},
|
|
472
|
+
cwd,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
},
|
|
476
|
+
[cwd, data, modal, runPlanned, showFailures],
|
|
477
|
+
);
|
|
478
|
+
|
|
355
479
|
// ─── Screen 2 actions ───────────────────────────────────────────────────
|
|
356
480
|
|
|
481
|
+
/** Push every skill of a plugin edited here, as one new version. */
|
|
482
|
+
const publishPlugin = useCallback(
|
|
483
|
+
async (ref: string, marketplace: string) => {
|
|
484
|
+
await runPlanned(
|
|
485
|
+
{
|
|
486
|
+
title: `Push a new version of ${ref}?`,
|
|
487
|
+
subject: ref,
|
|
488
|
+
marketplace,
|
|
489
|
+
},
|
|
490
|
+
() => planPluginPublish({ plugin: ref, keepLocal: false }, cwd),
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
[cwd, runPlanned],
|
|
494
|
+
);
|
|
495
|
+
|
|
357
496
|
const newPlugin = useCallback(
|
|
358
497
|
async (marketplace: string) => {
|
|
359
498
|
const name = (
|
|
@@ -541,6 +680,8 @@ export function useMarketState() {
|
|
|
541
680
|
create,
|
|
542
681
|
connect,
|
|
543
682
|
remove,
|
|
683
|
+
addLocal,
|
|
684
|
+
publishPlugin,
|
|
544
685
|
newPlugin,
|
|
545
686
|
deletePlugin,
|
|
546
687
|
addSkill,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display-path.ts — a folder as a person reads it: relative to the project
|
|
3
|
+
* when it is inside it, `~/…` when it is under the home folder, else as is.
|
|
4
|
+
* For display only; JSON and every file operation keep the absolute path.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
/** `child` is `parent` itself or somewhere under it. */
|
|
11
|
+
function within(parent: string, child: string): string | null {
|
|
12
|
+
const rel = path.relative(parent, child);
|
|
13
|
+
if (rel === "") return "";
|
|
14
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
15
|
+
return rel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `abs` shortened for display: `.claude/skills/greet` inside `cwd`,
|
|
20
|
+
* `~/.claude/skills/team-tools` under the home folder, otherwise unchanged.
|
|
21
|
+
* The home folder is `$HOME` when set, as Claude Code and the shell read it.
|
|
22
|
+
*/
|
|
23
|
+
export function displayPath(
|
|
24
|
+
abs: string,
|
|
25
|
+
cwd: string,
|
|
26
|
+
home: string = process.env.HOME || os.homedir(),
|
|
27
|
+
): string {
|
|
28
|
+
const inProject = within(path.resolve(cwd), path.resolve(abs));
|
|
29
|
+
if (inProject !== null) return inProject === "" ? "." : inProject;
|
|
30
|
+
const inHome = home ? within(path.resolve(home), path.resolve(abs)) : null;
|
|
31
|
+
if (inHome !== null) return inHome === "" ? "~" : `~/${inHome}`;
|
|
32
|
+
return abs;
|
|
33
|
+
}
|