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.
- 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/__tests__/scope-uninstall-wrong-scope.test.ts +341 -0
- 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/claude-cli.ts +13 -0
- 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/services/update-engine.ts +97 -1
- 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
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* the in-domain part of the plugin's skill, commits exactly the planned paths,
|
|
16
16
|
* delivers, moves this machine's installs through `claude plugin` commands,
|
|
17
17
|
* and removes the local copy only when it holds exactly the committed files.
|
|
18
|
+
* A plugin-wide publish does the same for every skill of one plugin edited
|
|
19
|
+
* here, as one commit and one version.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
import fs from "node:fs";
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
installLines,
|
|
57
59
|
installStep,
|
|
58
60
|
pluginViewAt,
|
|
61
|
+
readPlugin,
|
|
59
62
|
versionFiles,
|
|
60
63
|
versionLine,
|
|
61
64
|
} from "./market-ops-content.js";
|
|
@@ -73,6 +76,7 @@ import {
|
|
|
73
76
|
planError,
|
|
74
77
|
} from "./market-ops.js";
|
|
75
78
|
import {
|
|
79
|
+
type InstalledAt,
|
|
76
80
|
type SourceSets,
|
|
77
81
|
copyMismatch,
|
|
78
82
|
installReaches,
|
|
@@ -498,6 +502,41 @@ function sourceFolder(cwd: string, args: PublishArgs): string {
|
|
|
498
502
|
const FORCE_FIX =
|
|
499
503
|
"to replace the plugin's version with this copy, run again with --force";
|
|
500
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Why a publish keeps the local copy at `folder`, or null when it will be
|
|
507
|
+
* removed once the push lands and every install moves. Read-only.
|
|
508
|
+
*/
|
|
509
|
+
async function keepReasonFor(input: {
|
|
510
|
+
folder: string;
|
|
511
|
+
skill: string;
|
|
512
|
+
from: "project" | "global";
|
|
513
|
+
keepLocal: boolean;
|
|
514
|
+
plugin: string;
|
|
515
|
+
pluginId: string;
|
|
516
|
+
installs: readonly InstalledAt[];
|
|
517
|
+
committed: readonly SkillFile[];
|
|
518
|
+
cwd: string;
|
|
519
|
+
}): Promise<{ reason: string | null; managed: boolean }> {
|
|
520
|
+
const managed =
|
|
521
|
+
input.from === "project" && (await profileManaged(input.cwd, input.skill));
|
|
522
|
+
const reason = input.keepLocal
|
|
523
|
+
? "--keep-local"
|
|
524
|
+
: managed
|
|
525
|
+
? PROFILE_KEEPS
|
|
526
|
+
: !installReaches(
|
|
527
|
+
input.from === "global" ? "user" : "project",
|
|
528
|
+
input.installs.map((i) => i.scope),
|
|
529
|
+
)
|
|
530
|
+
? `${input.plugin} is not installed where this copy loads`
|
|
531
|
+
: ((await offHereReason(input.plugin, input.pluginId, input.cwd)) ??
|
|
532
|
+
(await copyMismatch(
|
|
533
|
+
input.folder,
|
|
534
|
+
input.committed,
|
|
535
|
+
"will not be published",
|
|
536
|
+
)));
|
|
537
|
+
return { reason, managed };
|
|
538
|
+
}
|
|
539
|
+
|
|
501
540
|
export async function planPublish(
|
|
502
541
|
args: PublishArgs,
|
|
503
542
|
cwd: string,
|
|
@@ -584,23 +623,21 @@ export async function planPublish(
|
|
|
584
623
|
const target = await deliveryTarget(m.source, m.name);
|
|
585
624
|
const installs = await installedScopes(pluginId, cwd);
|
|
586
625
|
const copyScope = args.from === "global" ? "user" : "project";
|
|
587
|
-
const managed =
|
|
588
|
-
args.from === "project" && (await profileManaged(cwd, args.skill));
|
|
589
626
|
const committed: SkillFile[] = [
|
|
590
627
|
...cmp.sets.published,
|
|
591
628
|
...cmp.theirsOut.map((f) => ({ path: f.path, oid: f.oid })),
|
|
592
629
|
];
|
|
593
|
-
const keepReason =
|
|
594
|
-
|
|
595
|
-
:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
630
|
+
const { reason: keepReason, managed } = await keepReasonFor({
|
|
631
|
+
folder,
|
|
632
|
+
skill: args.skill,
|
|
633
|
+
from: args.from,
|
|
634
|
+
keepLocal: args.keepLocal,
|
|
635
|
+
plugin,
|
|
636
|
+
pluginId,
|
|
637
|
+
installs,
|
|
638
|
+
committed,
|
|
639
|
+
cwd,
|
|
640
|
+
});
|
|
604
641
|
const predicted: LocalCopy = keepReason
|
|
605
642
|
? {
|
|
606
643
|
scope: copyScope,
|
|
@@ -624,7 +661,7 @@ export async function planPublish(
|
|
|
624
661
|
message: `${plugin}: update ${args.skill} skill (${to})`,
|
|
625
662
|
slug: `${plugin}-${to}`,
|
|
626
663
|
prBody: `Updates the skill \`${args.skill}\` in \`${plugin}\` and bumps it to ${to}.\n\nChanged: ${changeList(cmp.changes)}.`,
|
|
627
|
-
verify: { dir: skillDir, files: committed },
|
|
664
|
+
verify: [{ dir: skillDir, files: committed }],
|
|
628
665
|
state,
|
|
629
666
|
}),
|
|
630
667
|
installStep(view, installs, to, state),
|
|
@@ -730,6 +767,316 @@ export async function planPublish(
|
|
|
730
767
|
}
|
|
731
768
|
}
|
|
732
769
|
|
|
770
|
+
// ─── publish a plugin ────────────────────────────────────────────────────────
|
|
771
|
+
|
|
772
|
+
export interface PluginPublishArgs {
|
|
773
|
+
/** `plugin@marketplace`; the marketplace may be left out when there is one. */
|
|
774
|
+
plugin: string;
|
|
775
|
+
keepLocal: boolean;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const STATE_WORDS: Record<"no-base" | "conflict", string> = {
|
|
779
|
+
"no-base": "origin unknown",
|
|
780
|
+
conflict: "changed here and on GitHub",
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
/** One local copy a plugin-wide publish sends. */
|
|
784
|
+
interface PublishedCopy {
|
|
785
|
+
skill: string;
|
|
786
|
+
from: "project" | "global";
|
|
787
|
+
folder: string;
|
|
788
|
+
skillDir: string;
|
|
789
|
+
cmp: LocalCompare;
|
|
790
|
+
committed: SkillFile[];
|
|
791
|
+
keepReason: string | null;
|
|
792
|
+
managed: boolean;
|
|
793
|
+
predicted: LocalCopy;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* `market publish --plugin <p@m>` with no skill: every skill of the plugin
|
|
798
|
+
* whose local copy here has edits the plugin has not seen (`edited` in
|
|
799
|
+
* `market status`), sent as one commit and one patch version. A copy whose
|
|
800
|
+
* origin is unknown, or that changed on both sides, is listed and left out:
|
|
801
|
+
* it goes on its own, by name, with --force. Read-only.
|
|
802
|
+
*/
|
|
803
|
+
export async function planPluginPublish(
|
|
804
|
+
args: PluginPublishArgs,
|
|
805
|
+
cwd: string,
|
|
806
|
+
): Promise<PlanAnswer> {
|
|
807
|
+
const command: MarketCommand = "market publish";
|
|
808
|
+
try {
|
|
809
|
+
const view = await readPlugin(args.plugin);
|
|
810
|
+
const { m, snap, plugin } = view;
|
|
811
|
+
const pluginId = `${plugin}@${m.name}`;
|
|
812
|
+
const prefix = joinRel(view.dir, "skills", "");
|
|
813
|
+
const skills = new Set<string>();
|
|
814
|
+
for (const p of snap.entries.keys()) {
|
|
815
|
+
if (!p.startsWith(`${prefix}/`)) continue;
|
|
816
|
+
const rest = p.slice(prefix.length + 1).split("/");
|
|
817
|
+
if (rest.length === 2 && rest[1] === "SKILL.md" && rest[0]) {
|
|
818
|
+
skills.add(rest[0]);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const folders = localSkillFolders(cwd).filter((f) => skills.has(f.skill));
|
|
822
|
+
const edited: Array<{ f: LocalFolder; cmp: LocalCompare }> = [];
|
|
823
|
+
const leftOut: Array<{
|
|
824
|
+
skill: string;
|
|
825
|
+
from: "project" | "global";
|
|
826
|
+
state: "no-base" | "conflict";
|
|
827
|
+
}> = [];
|
|
828
|
+
for (const f of folders) {
|
|
829
|
+
const cmp = await compareLocal(f.path, view, f.skill);
|
|
830
|
+
if (cmp.state === "fast") edited.push({ f, cmp });
|
|
831
|
+
else if (cmp.state === "no-base" || cmp.state === "conflict") {
|
|
832
|
+
leftOut.push({
|
|
833
|
+
skill: f.skill,
|
|
834
|
+
from: f.from === "user" ? "global" : "project",
|
|
835
|
+
state: cmp.state,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const twice = edited.find(
|
|
840
|
+
(e, i) => edited.findIndex((o) => o.f.skill === e.f.skill) !== i,
|
|
841
|
+
);
|
|
842
|
+
if (twice) {
|
|
843
|
+
throw new MarketError(
|
|
844
|
+
"ambiguous",
|
|
845
|
+
`Both this project and your USER skills have edits to ${twice.f.skill}`,
|
|
846
|
+
`publish one at a time: \`magus market publish ${twice.f.skill} --plugin ${pluginId} --from project\` (or --from global)`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const leftOutLines = leftOut.map(
|
|
850
|
+
(l) =>
|
|
851
|
+
`Not included: ${l.skill} (${STATE_WORDS[l.state]}) — publish it on its own with \`magus market publish ${l.skill} --plugin ${pluginId}${l.from === "global" ? " --from global" : ""} --force\``,
|
|
852
|
+
);
|
|
853
|
+
const base = { marketplace: m.name, plugin };
|
|
854
|
+
if (edited.length === 0) {
|
|
855
|
+
if (leftOut.length > 0) {
|
|
856
|
+
const message = `No skill of ${pluginId} has edits here that it can publish as they are — nothing changed`;
|
|
857
|
+
return {
|
|
858
|
+
kind: "result",
|
|
859
|
+
result: {
|
|
860
|
+
command,
|
|
861
|
+
outcome: "failed",
|
|
862
|
+
message,
|
|
863
|
+
...base,
|
|
864
|
+
plan: leftOutLines,
|
|
865
|
+
steps: [],
|
|
866
|
+
error: {
|
|
867
|
+
code: leftOut[0]?.state ?? "no-base",
|
|
868
|
+
message,
|
|
869
|
+
fix: "publish each one on its own with --force, which asks before it replaces the plugin's version",
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
kind: "result",
|
|
876
|
+
result: {
|
|
877
|
+
command,
|
|
878
|
+
outcome: "unchanged",
|
|
879
|
+
message: `No skill of ${pluginId} has edits here to publish`,
|
|
880
|
+
...base,
|
|
881
|
+
steps: [],
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const { from, to } = bumpFrom(view);
|
|
887
|
+
const target = await deliveryTarget(m.source, m.name);
|
|
888
|
+
const installs = await installedScopes(pluginId, cwd);
|
|
889
|
+
const copies: PublishedCopy[] = [];
|
|
890
|
+
for (const { f, cmp } of edited) {
|
|
891
|
+
const fromWord = f.from === "user" ? "global" : "project";
|
|
892
|
+
const committed: SkillFile[] = [
|
|
893
|
+
...cmp.sets.published,
|
|
894
|
+
...cmp.theirsOut.map((t) => ({ path: t.path, oid: t.oid })),
|
|
895
|
+
];
|
|
896
|
+
const { reason, managed } = await keepReasonFor({
|
|
897
|
+
folder: f.path,
|
|
898
|
+
skill: f.skill,
|
|
899
|
+
from: fromWord,
|
|
900
|
+
keepLocal: args.keepLocal,
|
|
901
|
+
plugin,
|
|
902
|
+
pluginId,
|
|
903
|
+
installs,
|
|
904
|
+
committed,
|
|
905
|
+
cwd,
|
|
906
|
+
});
|
|
907
|
+
const scope = f.from;
|
|
908
|
+
copies.push({
|
|
909
|
+
skill: f.skill,
|
|
910
|
+
from: fromWord,
|
|
911
|
+
folder: f.path,
|
|
912
|
+
skillDir: joinRel(view.dir, "skills", f.skill),
|
|
913
|
+
cmp,
|
|
914
|
+
committed,
|
|
915
|
+
keepReason: reason,
|
|
916
|
+
managed,
|
|
917
|
+
predicted: reason
|
|
918
|
+
? { scope, name: f.skill, action: "kept", reason }
|
|
919
|
+
: { scope, name: f.skill, action: "removed" },
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
const files = [
|
|
923
|
+
...copies.flatMap((c) =>
|
|
924
|
+
c.cmp.changes.map((ch) => ({
|
|
925
|
+
path: joinRel(c.skillDir, ch.path),
|
|
926
|
+
change: ch.change,
|
|
927
|
+
})),
|
|
928
|
+
),
|
|
929
|
+
...versionFiles(view),
|
|
930
|
+
];
|
|
931
|
+
const names = copies.map((c) => c.skill);
|
|
932
|
+
const state: ChangeSpec["state"] & { moved?: boolean } = {};
|
|
933
|
+
const steps: MarketStep[] = [
|
|
934
|
+
...copies.map((c) =>
|
|
935
|
+
replaceStep(
|
|
936
|
+
c.folder,
|
|
937
|
+
c.skillDir,
|
|
938
|
+
plugin,
|
|
939
|
+
c.skill,
|
|
940
|
+
c.cmp,
|
|
941
|
+
c.committed,
|
|
942
|
+
`replace:${c.skill}`,
|
|
943
|
+
),
|
|
944
|
+
),
|
|
945
|
+
bumpStep(view, to),
|
|
946
|
+
...changeSteps({
|
|
947
|
+
label: m.name,
|
|
948
|
+
source: m.source,
|
|
949
|
+
branch: snap.branch,
|
|
950
|
+
target,
|
|
951
|
+
stage: [
|
|
952
|
+
...copies.map((c) => c.skillDir),
|
|
953
|
+
...versionFiles(view).map((f) => f.path),
|
|
954
|
+
],
|
|
955
|
+
files,
|
|
956
|
+
message: `${plugin}: update ${names.join(", ")} (${to})`,
|
|
957
|
+
slug: `${plugin}-${to}`,
|
|
958
|
+
prBody: `Updates ${plural(names.length, "skill")} in \`${plugin}\` and bumps it to ${to}.\n\n${copies.map((c) => `- \`${c.skill}\`: ${changeList(c.cmp.changes)}`).join("\n")}`,
|
|
959
|
+
verify: copies.map((c) => ({ dir: c.skillDir, files: c.committed })),
|
|
960
|
+
state,
|
|
961
|
+
}).map((s) =>
|
|
962
|
+
s.id === "deliver"
|
|
963
|
+
? {
|
|
964
|
+
...s,
|
|
965
|
+
// A pull request stops the apply here: nothing is installed,
|
|
966
|
+
// so every local copy stays until it is merged.
|
|
967
|
+
run: async (ctx: Parameters<MarketStep["run"]>[0]) => {
|
|
968
|
+
const out = await s.run(ctx);
|
|
969
|
+
if (!out?.stop) return out;
|
|
970
|
+
const kept: LocalCopy[] = copies.map((c) => ({
|
|
971
|
+
scope: c.predicted.scope,
|
|
972
|
+
name: c.skill,
|
|
973
|
+
action: "kept",
|
|
974
|
+
reason:
|
|
975
|
+
"the change went out as a pull request; the plugin's version replaces this copy once it is merged",
|
|
976
|
+
}));
|
|
977
|
+
return {
|
|
978
|
+
...out,
|
|
979
|
+
patch: { ...out.patch, localCopies: kept },
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
}
|
|
983
|
+
: s,
|
|
984
|
+
),
|
|
985
|
+
installStep(view, installs, to, state),
|
|
986
|
+
{
|
|
987
|
+
id: "local-copy",
|
|
988
|
+
label: "settled the local copies",
|
|
989
|
+
outside: true,
|
|
990
|
+
async run() {
|
|
991
|
+
const settled: LocalCopy[] = [];
|
|
992
|
+
for (const c of copies) {
|
|
993
|
+
const copy = await settleLocalCopy({
|
|
994
|
+
folder: c.folder,
|
|
995
|
+
committed: c.committed,
|
|
996
|
+
copy: { scope: c.predicted.scope, name: c.skill },
|
|
997
|
+
keepReason: c.keepReason,
|
|
998
|
+
moved: state.moved === true,
|
|
999
|
+
plugin,
|
|
1000
|
+
pluginId,
|
|
1001
|
+
cwd,
|
|
1002
|
+
});
|
|
1003
|
+
if (
|
|
1004
|
+
copy.action === "kept" &&
|
|
1005
|
+
!copy.partial &&
|
|
1006
|
+
!c.managed &&
|
|
1007
|
+
state.commit
|
|
1008
|
+
) {
|
|
1009
|
+
recordBase(c.folder, {
|
|
1010
|
+
marketplace: m.name,
|
|
1011
|
+
repo: m.source,
|
|
1012
|
+
plugin,
|
|
1013
|
+
skill: c.skill,
|
|
1014
|
+
commit: state.commit,
|
|
1015
|
+
tree: c.cmp.sets.hash,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
settled.push(copy);
|
|
1019
|
+
}
|
|
1020
|
+
return { patch: { localCopies: settled } };
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
];
|
|
1024
|
+
const lines = [
|
|
1025
|
+
`Publishes ${plural(copies.length, "edited skill")} to ${pluginId} as one new version: ${names.join(", ")}`,
|
|
1026
|
+
versionLine(plugin, from, to),
|
|
1027
|
+
...copies.flatMap((c) => [
|
|
1028
|
+
`${c.skill} (${whoseCopy(c.from)}): ${plural(c.cmp.changes.length, "file")} — ${changeList(c.cmp.changes)}`,
|
|
1029
|
+
...(c.cmp.sets.notPublished.length > 0
|
|
1030
|
+
? [
|
|
1031
|
+
`${c.skill}, not published: ${c.cmp.sets.notPublished.map((n) => `${n.path} (${n.reason})`).join(", ")}`,
|
|
1032
|
+
]
|
|
1033
|
+
: []),
|
|
1034
|
+
]),
|
|
1035
|
+
...leftOutLines,
|
|
1036
|
+
targetLine(target, snap.branch),
|
|
1037
|
+
"A pull request moves no install and keeps the local copies",
|
|
1038
|
+
...installLines(plugin, installs, to),
|
|
1039
|
+
...copies.map((c) =>
|
|
1040
|
+
c.predicted.action === "removed"
|
|
1041
|
+
? `Afterwards removes ${whoseCopy(c.from)} of ${c.skill}`
|
|
1042
|
+
: `Keeps ${whoseCopy(c.from)} of ${c.skill}: ${c.predicted.reason}`,
|
|
1043
|
+
),
|
|
1044
|
+
];
|
|
1045
|
+
const plan: MarketPlan = {
|
|
1046
|
+
command,
|
|
1047
|
+
lines,
|
|
1048
|
+
planId: computePlanId({
|
|
1049
|
+
op: "publish-plugin",
|
|
1050
|
+
plugin: pluginId,
|
|
1051
|
+
keepLocal: args.keepLocal,
|
|
1052
|
+
branch: snap.branch,
|
|
1053
|
+
commit: snap.commit,
|
|
1054
|
+
skills: copies.map((c) => ({
|
|
1055
|
+
skill: c.skill,
|
|
1056
|
+
from: c.from,
|
|
1057
|
+
base: c.cmp.baseCommit,
|
|
1058
|
+
hash: c.cmp.sets.hash,
|
|
1059
|
+
notPublished: c.cmp.sets.notPublished,
|
|
1060
|
+
localCopy: c.predicted,
|
|
1061
|
+
})),
|
|
1062
|
+
leftOut,
|
|
1063
|
+
target,
|
|
1064
|
+
version: { from, to },
|
|
1065
|
+
installs,
|
|
1066
|
+
}),
|
|
1067
|
+
summary: { ...base, version: { from, to }, files, target },
|
|
1068
|
+
steps,
|
|
1069
|
+
sync: { name: m.name, url: cloneUrl(m.source) },
|
|
1070
|
+
lock: m.name,
|
|
1071
|
+
replan: () => planPluginPublish({ ...args, plugin: pluginId }, cwd),
|
|
1072
|
+
doneMessage: `${names.join(", ")} ${names.length === 1 ? "is" : "are"} published in ${pluginId} ${to}. Takes effect in new sessions, or after /reload-plugins.`,
|
|
1073
|
+
};
|
|
1074
|
+
return { kind: "plan", plan };
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
return planError(command, error);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
733
1080
|
function capital(text: string): string {
|
|
734
1081
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
735
1082
|
}
|
|
@@ -760,9 +1107,10 @@ function replaceStep(
|
|
|
760
1107
|
skill: string,
|
|
761
1108
|
cmp: LocalCompare,
|
|
762
1109
|
committed: readonly SkillFile[],
|
|
1110
|
+
id = "replace",
|
|
763
1111
|
): MarketStep {
|
|
764
1112
|
return {
|
|
765
|
-
id
|
|
1113
|
+
id,
|
|
766
1114
|
label: `copied ${skill} into ${plugin}`,
|
|
767
1115
|
outside: false,
|
|
768
1116
|
async run(ctx) {
|
|
@@ -71,10 +71,53 @@ import { fastForwardCloneIfPresent } from "./marketplace-refresh.js";
|
|
|
71
71
|
const MANIFEST = ".claude-plugin/marketplace.json";
|
|
72
72
|
const README = "README.md";
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/** The longest company name a marketplace may carry, in characters. */
|
|
75
|
+
export const COMPANY_NAME_MAX = 64;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check a company name for `owner.name`: one line, not blank, at most
|
|
79
|
+
* {@link COMPANY_NAME_MAX} characters once trimmed. Returns it trimmed; a
|
|
80
|
+
* bad one is a usage error (exit 2).
|
|
81
|
+
*/
|
|
82
|
+
export function checkCompanyName(value: string): string {
|
|
83
|
+
const company = value.trim();
|
|
84
|
+
if (company === "") {
|
|
85
|
+
throw new MarketError(
|
|
86
|
+
"invalid-name",
|
|
87
|
+
"The company name is empty",
|
|
88
|
+
"pass --company <name>",
|
|
89
|
+
2,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// Every control character (C0, DEL, C1 — NEL included) and the Unicode
|
|
93
|
+
// line and paragraph separators.
|
|
94
|
+
if (/[\p{Cc}\u2028\u2029]/u.test(company)) {
|
|
95
|
+
throw new MarketError(
|
|
96
|
+
"invalid-name",
|
|
97
|
+
"The company name must be a single line",
|
|
98
|
+
"pass --company <name>",
|
|
99
|
+
2,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if ([...company].length > COMPANY_NAME_MAX) {
|
|
103
|
+
throw new MarketError(
|
|
104
|
+
"invalid-name",
|
|
105
|
+
`The company name is longer than ${COMPANY_NAME_MAX} characters`,
|
|
106
|
+
"pass a shorter --company <name>",
|
|
107
|
+
2,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return company;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function companyLine(company: string): string {
|
|
114
|
+
return `Company name: ${company} (magus shows the marketplace as "by ${company}")`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function newManifest(name: string, company: string, description?: string) {
|
|
75
118
|
return withMarker({
|
|
76
119
|
name,
|
|
77
|
-
owner: { name:
|
|
120
|
+
owner: { name: company },
|
|
78
121
|
metadata: {
|
|
79
122
|
description: description ?? `${name} company plugin marketplace`,
|
|
80
123
|
},
|
|
@@ -171,7 +214,7 @@ function trustStep(
|
|
|
171
214
|
function initSteps(
|
|
172
215
|
name: string,
|
|
173
216
|
source: string,
|
|
174
|
-
|
|
217
|
+
company: string,
|
|
175
218
|
branch: string,
|
|
176
219
|
description: string | undefined,
|
|
177
220
|
): MarketStep[] {
|
|
@@ -186,7 +229,7 @@ function initSteps(
|
|
|
186
229
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
187
230
|
const r = await initWithRemote(dir, cloneUrl(source), branch);
|
|
188
231
|
if (!r.ok) throw gitError(r, name);
|
|
189
|
-
writeJson(dir, MANIFEST, newManifest(name,
|
|
232
|
+
writeJson(dir, MANIFEST, newManifest(name, company, description), name);
|
|
190
233
|
writeFileUnder(dir, README, readme(name, source), linkRefusal(name));
|
|
191
234
|
return undefined;
|
|
192
235
|
},
|
|
@@ -290,6 +333,11 @@ export interface CreateArgs {
|
|
|
290
333
|
visibility: "private" | "public";
|
|
291
334
|
name?: string;
|
|
292
335
|
description?: string;
|
|
336
|
+
/**
|
|
337
|
+
* The company name written as the manifest's `owner.name`; the GitHub
|
|
338
|
+
* owner of `repo` when absent.
|
|
339
|
+
*/
|
|
340
|
+
company?: string;
|
|
293
341
|
}
|
|
294
342
|
|
|
295
343
|
/**
|
|
@@ -324,6 +372,7 @@ export async function planCreate(args: CreateArgs): Promise<PlanAnswer> {
|
|
|
324
372
|
);
|
|
325
373
|
}
|
|
326
374
|
const [owner = "", repoName = ""] = args.repo.split("/");
|
|
375
|
+
const company = checkCompanyName(args.company ?? owner);
|
|
327
376
|
const name = args.name ?? repoName.toLowerCase();
|
|
328
377
|
if (!validPackageName(name)) {
|
|
329
378
|
throw new MarketError(
|
|
@@ -382,7 +431,7 @@ export async function planCreate(args: CreateArgs): Promise<PlanAnswer> {
|
|
|
382
431
|
},
|
|
383
432
|
},
|
|
384
433
|
...[
|
|
385
|
-
...initSteps(name, source,
|
|
434
|
+
...initSteps(name, source, company, branch, args.description),
|
|
386
435
|
registerStep(name, source),
|
|
387
436
|
trustStep(name, source, "create"),
|
|
388
437
|
].map((s) => afterCreate(s, args.repo)),
|
|
@@ -392,9 +441,16 @@ export async function planCreate(args: CreateArgs): Promise<PlanAnswer> {
|
|
|
392
441
|
lines: [
|
|
393
442
|
`Creates the ${args.visibility} GitHub repository ${args.repo}`,
|
|
394
443
|
`Writes a company marketplace named ${name} into it and pushes it to ${branch}`,
|
|
444
|
+
companyLine(company),
|
|
395
445
|
`Adds ${name} to this machine at USER scope and marks it as a company marketplace here`,
|
|
396
446
|
],
|
|
397
|
-
planId: computePlanId({
|
|
447
|
+
planId: computePlanId({
|
|
448
|
+
op: "create",
|
|
449
|
+
...args,
|
|
450
|
+
name,
|
|
451
|
+
company,
|
|
452
|
+
branch,
|
|
453
|
+
}),
|
|
398
454
|
summary: { marketplace: name },
|
|
399
455
|
steps: steps.map((s) =>
|
|
400
456
|
s.id === "init" ||
|
|
@@ -427,6 +483,12 @@ function teammateNote(name: string): string {
|
|
|
427
483
|
export interface ConnectArgs {
|
|
428
484
|
source: string;
|
|
429
485
|
name?: string;
|
|
486
|
+
/**
|
|
487
|
+
* The company name for a manifest connect writes (`init` and `add` only);
|
|
488
|
+
* the GitHub owner of the source when absent. A repository that already
|
|
489
|
+
* holds a marketplace keeps the `owner.name` its manifest gives.
|
|
490
|
+
*/
|
|
491
|
+
company?: string;
|
|
430
492
|
}
|
|
431
493
|
|
|
432
494
|
async function readRemoteManifest(
|
|
@@ -470,6 +532,8 @@ export async function planConnect(
|
|
|
470
532
|
): Promise<PlanAnswer> {
|
|
471
533
|
const command: MarketCommand = "market connect";
|
|
472
534
|
try {
|
|
535
|
+
// The value's form is checked before any GitHub or git work.
|
|
536
|
+
if (args.company !== undefined) checkCompanyName(args.company);
|
|
473
537
|
const source = normaliseSource(args.source, cwd);
|
|
474
538
|
const github = isGithubSource(source);
|
|
475
539
|
const url = cloneUrl(source);
|
|
@@ -495,6 +559,14 @@ export async function planConnect(
|
|
|
495
559
|
2,
|
|
496
560
|
);
|
|
497
561
|
}
|
|
562
|
+
if (args.company !== undefined && !fresh) {
|
|
563
|
+
throw new MarketError(
|
|
564
|
+
"other",
|
|
565
|
+
"--company applies only when connect sets up a repository with no marketplace in it; this one keeps the company name its manifest gives",
|
|
566
|
+
"run it again without --company",
|
|
567
|
+
2,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
498
570
|
const base = path
|
|
499
571
|
.basename(source)
|
|
500
572
|
.replace(/\.git$/, "")
|
|
@@ -549,18 +621,25 @@ export async function planConnect(
|
|
|
549
621
|
: source;
|
|
550
622
|
const steps: MarketStep[] = [];
|
|
551
623
|
const lines: string[] = [];
|
|
552
|
-
const
|
|
624
|
+
const company = fresh
|
|
625
|
+
? checkCompanyName(
|
|
626
|
+
args.company ?? (github ? (source.split("/")[0] ?? name) : name),
|
|
627
|
+
)
|
|
628
|
+
: null;
|
|
553
629
|
const delivered: ChangeSpec["state"] = {};
|
|
554
630
|
if (action === "init") {
|
|
555
631
|
lines.push(
|
|
556
632
|
`${source} holds no marketplace yet: writes a company marketplace named ${name} into it and pushes it to ${branch}`,
|
|
633
|
+
companyLine(company as string),
|
|
557
634
|
);
|
|
558
635
|
steps.push(
|
|
559
|
-
...initSteps(name, source,
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
636
|
+
...initSteps(name, source, company as string, branch, undefined).map(
|
|
637
|
+
(s) => ({
|
|
638
|
+
...s,
|
|
639
|
+
run: (ctx: Parameters<MarketStep["run"]>[0]) =>
|
|
640
|
+
s.run({ ...ctx, clone: workingClonePath(name) }),
|
|
641
|
+
}),
|
|
642
|
+
),
|
|
564
643
|
);
|
|
565
644
|
} else if (action === "mark" || action === "add") {
|
|
566
645
|
const target = await deliveryTarget(source, name);
|
|
@@ -568,6 +647,7 @@ export async function planConnect(
|
|
|
568
647
|
action === "mark"
|
|
569
648
|
? `${name} is a marketplace without the company marker: commits the marker to its manifest`
|
|
570
649
|
: `${source} holds no marketplace yet: commits a company marketplace named ${name} on top of its ${branch} history, leaving its files as they are`,
|
|
650
|
+
...(action === "add" ? [companyLine(company as string)] : []),
|
|
571
651
|
targetLine(target, branch),
|
|
572
652
|
);
|
|
573
653
|
const state = delivered;
|
|
@@ -597,7 +677,7 @@ export async function planConnect(
|
|
|
597
677
|
writeJson(
|
|
598
678
|
clone,
|
|
599
679
|
MANIFEST,
|
|
600
|
-
newManifest(name,
|
|
680
|
+
newManifest(name, company as string, undefined),
|
|
601
681
|
name,
|
|
602
682
|
);
|
|
603
683
|
return undefined;
|
|
@@ -689,6 +769,7 @@ export async function planConnect(
|
|
|
689
769
|
branch,
|
|
690
770
|
action,
|
|
691
771
|
name,
|
|
772
|
+
company,
|
|
692
773
|
registered,
|
|
693
774
|
}),
|
|
694
775
|
summary: { marketplace: name },
|