@uniqbit/mate-core 0.18.5 → 1.0.0-canary.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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +177 -138
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +176 -3
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +5 -13
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +4 -0
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +9 -17
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +48 -40
- package/src/lib/orchestrator/adapters/opencode.ts +2 -0
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
- package/src/lib/orchestrator/projection-entries.ts +45 -14
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -0
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +45 -5
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -1
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +15 -1
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -11
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -2
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +151 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +97 -21
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +35 -0
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +284 -8
- package/src/tools/setup/providers/opencode.ts +180 -1
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +126 -10
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/tools/setup/runtime-documents.ts +0 -291
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { MATE_CLAUDE_PLUGIN_NAME } from "../../../lib/package-paths";
|
|
7
|
+
import type { CapabilityContributionInput } from "../plugin";
|
|
8
|
+
|
|
9
|
+
/** What Claude reads to load a folder as a plugin, relative to that folder. */
|
|
10
|
+
export const CLAUDE_PLUGIN_MANIFEST = path.join(".claude-plugin", "plugin.json");
|
|
11
|
+
|
|
12
|
+
/** Where a folder publishes itself as a marketplace for plain sessions, relative to that folder. */
|
|
13
|
+
export const CLAUDE_PLUGIN_MARKETPLACE = path.join(".claude-plugin", "marketplace.json");
|
|
14
|
+
|
|
15
|
+
/** The owner name that marks a marketplace file as Mate's. */
|
|
16
|
+
const MATE_MARKETPLACE_OWNER = "Mate";
|
|
17
|
+
|
|
18
|
+
const MATE_MARKETPLACE_PREFIX = "mate-plugin-";
|
|
19
|
+
|
|
20
|
+
/** The marketplace name Mate writes for a plugin. */
|
|
21
|
+
function mateMarketplaceName(plugin: string): string {
|
|
22
|
+
return `${MATE_MARKETPLACE_PREFIX}${plugin}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const RESERVED_MARKETPLACE_NAMES = new Set([
|
|
26
|
+
"claude-plugins-official",
|
|
27
|
+
"inline",
|
|
28
|
+
"builtin",
|
|
29
|
+
"skills-dir",
|
|
30
|
+
"synced",
|
|
31
|
+
"github",
|
|
32
|
+
"npm",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** Claude Code's marketplace name rules: letters, digits, `.`, `_`, `-`, and no reserved name. */
|
|
36
|
+
function isValidMarketplaceName(name: string): boolean {
|
|
37
|
+
return (
|
|
38
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) &&
|
|
39
|
+
!RESERVED_MARKETPLACE_NAMES.has(name) &&
|
|
40
|
+
!name.startsWith("claudeai-")
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when `directory` holds a Claude plugin manifest. */
|
|
45
|
+
async function hasClaudePluginManifest(directory: string): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
return (await fs.stat(path.join(directory, CLAUDE_PLUGIN_MANIFEST))).isFile();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Why a launch leaves a declared Claude plugin folder out, in the order they are checked. */
|
|
54
|
+
export type ClaudePluginDirectoryProblem =
|
|
55
|
+
| "not-absolute"
|
|
56
|
+
| "bundled-mate-plugin"
|
|
57
|
+
| "missing-manifest"
|
|
58
|
+
| "mate-plugin-name";
|
|
59
|
+
|
|
60
|
+
/** How a load warning words each problem, after the Capability and the folder. */
|
|
61
|
+
const LOAD_WARNINGS: Record<ClaudePluginDirectoryProblem, string> = {
|
|
62
|
+
"not-absolute": "is not an absolute path; it was left out.",
|
|
63
|
+
"bundled-mate-plugin":
|
|
64
|
+
"is Mate's bundled plugin, which every managed launch loads already; it was left out.",
|
|
65
|
+
"missing-manifest": `holds no ${CLAUDE_PLUGIN_MANIFEST}; it was left out.`,
|
|
66
|
+
"mate-plugin-name": `uses Mate's plugin name ${MATE_CLAUDE_PLUGIN_NAME}; it was left out so it cannot replace Mate's guardrail hooks.`,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The `name` a folder's plugin manifest declares; undefined when the manifest
|
|
71
|
+
* does not parse or holds no string `name`.
|
|
72
|
+
*/
|
|
73
|
+
async function claudePluginManifestName(directory: string): Promise<string | undefined> {
|
|
74
|
+
try {
|
|
75
|
+
const manifest = JSON.parse(
|
|
76
|
+
await fs.readFile(path.join(directory, CLAUDE_PLUGIN_MANIFEST), "utf8"),
|
|
77
|
+
) as { name?: unknown } | null;
|
|
78
|
+
return typeof manifest?.name === "string" ? manifest.name : undefined;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The first problem that leaves `directory` out, if any. `isBundledPlugin` says
|
|
86
|
+
* it resolves to Mate's bundled plugin, which is named as such rather than as a
|
|
87
|
+
* folder using Mate's plugin name.
|
|
88
|
+
*/
|
|
89
|
+
async function claudePluginDirectoryProblem(
|
|
90
|
+
directory: string,
|
|
91
|
+
isBundledPlugin: boolean,
|
|
92
|
+
): Promise<ClaudePluginDirectoryProblem | undefined> {
|
|
93
|
+
if (!path.isAbsolute(directory)) return "not-absolute";
|
|
94
|
+
if (isBundledPlugin) return "bundled-mate-plugin";
|
|
95
|
+
if (!(await hasClaudePluginManifest(directory))) return "missing-manifest";
|
|
96
|
+
return (await claudePluginManifestName(directory)) === MATE_CLAUDE_PLUGIN_NAME
|
|
97
|
+
? "mate-plugin-name"
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Declared folders of the Capabilities `enabled` selects, in registration order. */
|
|
102
|
+
function* declaredClaudePluginDirectories(
|
|
103
|
+
contributions: CapabilityContributionInput[],
|
|
104
|
+
enabled = true,
|
|
105
|
+
): Generator<{ capability: string; directory: string }> {
|
|
106
|
+
for (const input of contributions) {
|
|
107
|
+
if (input.enabled !== enabled) continue;
|
|
108
|
+
for (const { path: directory } of input.contributions.pluginDirectories ?? []) {
|
|
109
|
+
yield { capability: input.pluginId, directory };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** One folder however it is spelled: trailing separators and symlinks resolve to one key. */
|
|
115
|
+
async function claudePluginDirectoryKey(directory: string): Promise<string> {
|
|
116
|
+
if (!path.isAbsolute(directory)) return directory;
|
|
117
|
+
const resolved = path.resolve(directory);
|
|
118
|
+
return fs.realpath(resolved).catch(() => resolved);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Publication for plain sessions: each folder is its own `directory`
|
|
123
|
+
// marketplace, so Claude Code loads it in place from a wrapped repository's
|
|
124
|
+
// settings. Mate reuses an author's marketplace file and owns only its own.
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/** Marketplace names the user's Claude Code configuration declares, each with its recorded sources. */
|
|
128
|
+
export type ClaudeUserMarketplaces = ReadonlyMap<string, readonly unknown[]>;
|
|
129
|
+
|
|
130
|
+
/** Claude Code's own config directory, as Claude Code resolves it. */
|
|
131
|
+
function claudeConfigDir(env: NodeJS.ProcessEnv): string {
|
|
132
|
+
return env.CLAUDE_CONFIG_DIR || path.join(env.HOME || os.homedir(), ".claude");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readJsonRecord(file: string): Promise<Record<string, unknown>> {
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
138
|
+
return isRecord(parsed) ? parsed : {};
|
|
139
|
+
} catch {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The user-scope marketplaces a published name must not shadow. Claude Code
|
|
150
|
+
* rewrites its user record of a name to whichever source the current project
|
|
151
|
+
* declares, so a same-named project marketplace would redirect the user's own.
|
|
152
|
+
*
|
|
153
|
+
* Read from the user's `settings.json`, and from Claude Code's record of known
|
|
154
|
+
* marketplaces for every source other than a `directory`: a recorded directory
|
|
155
|
+
* is what any project's own local marketplace leaves behind, a wrapped
|
|
156
|
+
* repository's included, and is not the user's choice.
|
|
157
|
+
*/
|
|
158
|
+
export async function readClaudeUserMarketplaces(
|
|
159
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
160
|
+
): Promise<ClaudeUserMarketplaces> {
|
|
161
|
+
const configDir = claudeConfigDir(env);
|
|
162
|
+
const marketplaces = new Map<string, unknown[]>();
|
|
163
|
+
const add = (name: string, source: unknown) => {
|
|
164
|
+
marketplaces.set(name, [...(marketplaces.get(name) ?? []), source]);
|
|
165
|
+
};
|
|
166
|
+
const settings = await readJsonRecord(path.join(configDir, "settings.json"));
|
|
167
|
+
if (isRecord(settings.extraKnownMarketplaces)) {
|
|
168
|
+
for (const [name, entry] of Object.entries(settings.extraKnownMarketplaces)) {
|
|
169
|
+
add(name, isRecord(entry) ? entry.source : undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const known = await readJsonRecord(path.join(configDir, "plugins", "known_marketplaces.json"));
|
|
173
|
+
for (const [name, entry] of Object.entries(known)) {
|
|
174
|
+
const source = isRecord(entry) ? entry.source : undefined;
|
|
175
|
+
if (isRecord(source) && source.source === "directory") continue;
|
|
176
|
+
add(name, source);
|
|
177
|
+
}
|
|
178
|
+
return marketplaces;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A recorded source that is the `directory` marketplace at `directory` itself. */
|
|
182
|
+
async function isDirectorySourceAt(source: unknown, directory: string): Promise<boolean> {
|
|
183
|
+
if (!isRecord(source) || source.source !== "directory" || typeof source.path !== "string") {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return (
|
|
187
|
+
path.resolve(source.path) === path.resolve(directory) ||
|
|
188
|
+
(await claudePluginDirectoryKey(source.path)) === (await claudePluginDirectoryKey(directory))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Why a loadable folder does not reach plain sessions. */
|
|
193
|
+
export type ClaudePluginUnpublishedReason =
|
|
194
|
+
| "foreign-marketplace"
|
|
195
|
+
| "invalid-marketplace-name"
|
|
196
|
+
| "name-conflict"
|
|
197
|
+
| "missing-marketplace";
|
|
198
|
+
|
|
199
|
+
/** The kinds of a folder that claims a marketplace name: published, or published once a pass writes Mate's file. */
|
|
200
|
+
type MarketplaceClaimKind = "author-marketplace" | "mate-marketplace" | "pending-marketplace";
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* What a folder is for plain sessions. `author-marketplace` and
|
|
204
|
+
* `mate-marketplace` are published; `pending-marketplace` (no file, or a stale
|
|
205
|
+
* one of Mate's) is published once a pass writes Mate's current file; the rest
|
|
206
|
+
* never are.
|
|
207
|
+
*/
|
|
208
|
+
type ClaudePluginFolderClass =
|
|
209
|
+
| { kind: "not-loadable"; problem: ClaudePluginDirectoryProblem }
|
|
210
|
+
| { kind: MarketplaceClaimKind; plugin: string; marketplace: string }
|
|
211
|
+
| { kind: "name-conflict"; plugin: string; marketplace: string }
|
|
212
|
+
| { kind: "foreign-marketplace"; plugin: string }
|
|
213
|
+
| { kind: "invalid-marketplace-name"; plugin?: string };
|
|
214
|
+
|
|
215
|
+
type MarketplaceFile = { state: "absent" } | { state: "present"; text?: string; json: unknown };
|
|
216
|
+
|
|
217
|
+
async function readMarketplaceFile(directory: string): Promise<MarketplaceFile> {
|
|
218
|
+
let text: string;
|
|
219
|
+
try {
|
|
220
|
+
text = await fs.readFile(path.join(directory, CLAUDE_PLUGIN_MARKETPLACE), "utf8");
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { state: "absent" };
|
|
223
|
+
return { state: "present", json: undefined };
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
return { state: "present", text, json: JSON.parse(text) as unknown };
|
|
227
|
+
} catch {
|
|
228
|
+
return { state: "present", text, json: undefined };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Mate's file: owned by `Mate` under a `mate-plugin-` name. */
|
|
233
|
+
function isMateMarketplace(json: unknown): boolean {
|
|
234
|
+
return (
|
|
235
|
+
isRecord(json) &&
|
|
236
|
+
isRecord(json.owner) &&
|
|
237
|
+
json.owner.name === MATE_MARKETPLACE_OWNER &&
|
|
238
|
+
typeof json.name === "string" &&
|
|
239
|
+
json.name.startsWith(MATE_MARKETPLACE_PREFIX)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Lists `plugin` as the folder itself, so Claude loads it in place. */
|
|
244
|
+
function listsPluginInPlace(json: unknown, plugin: string): boolean {
|
|
245
|
+
return (
|
|
246
|
+
isRecord(json) &&
|
|
247
|
+
Array.isArray(json.plugins) &&
|
|
248
|
+
json.plugins.some(
|
|
249
|
+
(entry) =>
|
|
250
|
+
isRecord(entry) && entry.name === plugin && (entry.source === "./" || entry.source === "."),
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The marketplace file Mate writes for `plugin`, byte-stable across passes. */
|
|
256
|
+
function mateMarketplaceText(plugin: string): string {
|
|
257
|
+
return `${JSON.stringify(
|
|
258
|
+
{
|
|
259
|
+
name: mateMarketplaceName(plugin),
|
|
260
|
+
owner: { name: MATE_MARKETPLACE_OWNER },
|
|
261
|
+
plugins: [{ name: plugin, source: "./" }],
|
|
262
|
+
},
|
|
263
|
+
null,
|
|
264
|
+
2,
|
|
265
|
+
)}\n`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The marketplace a folder's file claims for `plugin`; undefined for a foreign
|
|
270
|
+
* file. A file of Mate's counts only while it is exactly what Mate writes; any
|
|
271
|
+
* other is pending, like an absent one, so a failed rewrite never leaves the
|
|
272
|
+
* wrap enabling a stale marketplace.
|
|
273
|
+
*/
|
|
274
|
+
function marketplaceClaim(
|
|
275
|
+
file: MarketplaceFile,
|
|
276
|
+
plugin: string,
|
|
277
|
+
): { kind: MarketplaceClaimKind; marketplace: string } | undefined {
|
|
278
|
+
if (file.state === "absent" || isMateMarketplace(file.json)) {
|
|
279
|
+
const current = file.state === "present" && file.text === mateMarketplaceText(plugin);
|
|
280
|
+
return {
|
|
281
|
+
kind: current ? "mate-marketplace" : "pending-marketplace",
|
|
282
|
+
marketplace: mateMarketplaceName(plugin),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
listsPluginInPlace(file.json, plugin) &&
|
|
287
|
+
isRecord(file.json) &&
|
|
288
|
+
typeof file.json.name === "string"
|
|
289
|
+
) {
|
|
290
|
+
return { kind: "author-marketplace", marketplace: file.json.name };
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** One folder's class, read-only. */
|
|
296
|
+
async function classifyFolder(
|
|
297
|
+
directory: string,
|
|
298
|
+
isBundledPlugin: boolean,
|
|
299
|
+
userMarketplaces: ClaudeUserMarketplaces,
|
|
300
|
+
): Promise<ClaudePluginFolderClass> {
|
|
301
|
+
const problem = await claudePluginDirectoryProblem(directory, isBundledPlugin);
|
|
302
|
+
if (problem) return { kind: "not-loadable", problem };
|
|
303
|
+
const plugin = await claudePluginManifestName(directory);
|
|
304
|
+
if (plugin === undefined) return { kind: "invalid-marketplace-name" };
|
|
305
|
+
|
|
306
|
+
const claim = marketplaceClaim(await readMarketplaceFile(directory), plugin);
|
|
307
|
+
if (!claim) return { kind: "foreign-marketplace", plugin };
|
|
308
|
+
const { kind, marketplace } = claim;
|
|
309
|
+
if (!isValidMarketplaceName(marketplace)) {
|
|
310
|
+
return kind === "author-marketplace"
|
|
311
|
+
? { kind: "foreign-marketplace", plugin }
|
|
312
|
+
: { kind: "invalid-marketplace-name", plugin };
|
|
313
|
+
}
|
|
314
|
+
for (const source of userMarketplaces.get(marketplace) ?? []) {
|
|
315
|
+
if (!(await isDirectorySourceAt(source, directory))) {
|
|
316
|
+
return { kind: "name-conflict", plugin, marketplace };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { kind, plugin, marketplace };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** One declaration and its folder's class. */
|
|
323
|
+
interface ClassifiedFolder {
|
|
324
|
+
capability: string;
|
|
325
|
+
/** The path as the Capability declared it. */
|
|
326
|
+
declared: string;
|
|
327
|
+
/** Absolute: what `--plugin-dir` passes and the wrap's settings name. */
|
|
328
|
+
path: string;
|
|
329
|
+
key: string;
|
|
330
|
+
/** The folder's first declaration; a pass acts on and names it once. */
|
|
331
|
+
first: boolean;
|
|
332
|
+
class: ClaudePluginFolderClass;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Published, or published once a pass writes Mate's file. */
|
|
336
|
+
function claimsMarketplace(
|
|
337
|
+
folderClass: ClaudePluginFolderClass,
|
|
338
|
+
): folderClass is Extract<ClaudePluginFolderClass, { kind: MarketplaceClaimKind }> {
|
|
339
|
+
return (
|
|
340
|
+
folderClass.kind === "author-marketplace" ||
|
|
341
|
+
folderClass.kind === "mate-marketplace" ||
|
|
342
|
+
folderClass.kind === "pending-marketplace"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Every enabled declaration, classified. A marketplace name two folders would
|
|
348
|
+
* both publish under goes to the first; the later one is a `name-conflict`,
|
|
349
|
+
* since one settings key cannot name two sources.
|
|
350
|
+
*/
|
|
351
|
+
async function classifyFolders(
|
|
352
|
+
contributions: CapabilityContributionInput[],
|
|
353
|
+
bundledPluginRoot: string,
|
|
354
|
+
userMarketplaces: ClaudeUserMarketplaces,
|
|
355
|
+
): Promise<ClassifiedFolder[]> {
|
|
356
|
+
const bundledKey = await claudePluginDirectoryKey(bundledPluginRoot);
|
|
357
|
+
const classes = new Map<string, ClaudePluginFolderClass>();
|
|
358
|
+
const claimed = new Map<string, string>();
|
|
359
|
+
const folders: ClassifiedFolder[] = [];
|
|
360
|
+
for (const { capability, directory } of declaredClaudePluginDirectories(contributions)) {
|
|
361
|
+
const key = await claudePluginDirectoryKey(directory);
|
|
362
|
+
const first = !classes.has(key);
|
|
363
|
+
let folderClass =
|
|
364
|
+
classes.get(key) ?? (await classifyFolder(directory, key === bundledKey, userMarketplaces));
|
|
365
|
+
if (claimsMarketplace(folderClass)) {
|
|
366
|
+
const owner = claimed.get(folderClass.marketplace);
|
|
367
|
+
if (owner === undefined) claimed.set(folderClass.marketplace, key);
|
|
368
|
+
else if (owner !== key) folderClass = { ...folderClass, kind: "name-conflict" };
|
|
369
|
+
}
|
|
370
|
+
if (first) classes.set(key, folderClass);
|
|
371
|
+
folders.push({
|
|
372
|
+
capability,
|
|
373
|
+
declared: directory,
|
|
374
|
+
path: path.isAbsolute(directory) ? path.resolve(directory) : directory,
|
|
375
|
+
key,
|
|
376
|
+
first,
|
|
377
|
+
class: folderClass,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return folders;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A folder plain sessions load: the Claude wrap enables `plugin@marketplace`. */
|
|
384
|
+
export interface PublishedClaudePluginFolder {
|
|
385
|
+
/** Absolute, as the launch passes it. */
|
|
386
|
+
path: string;
|
|
387
|
+
plugin: string;
|
|
388
|
+
marketplace: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** One declared Claude plugin folder, as an operator inspects it. */
|
|
392
|
+
export interface ClaudePluginDirectoryReport {
|
|
393
|
+
capability: string;
|
|
394
|
+
path: string;
|
|
395
|
+
/**
|
|
396
|
+
* `mate claude` passes the folder: an absolute path holding a plugin manifest,
|
|
397
|
+
* neither Mate's bundled plugin nor using Mate's plugin name.
|
|
398
|
+
*/
|
|
399
|
+
loadable: boolean;
|
|
400
|
+
/** Why `mate claude` leaves the folder out; absent when it passes it. */
|
|
401
|
+
problem?: ClaudePluginDirectoryProblem;
|
|
402
|
+
/** The marketplace plain sessions in a wrapped repository load it through; Mate writes its own at the next pass. */
|
|
403
|
+
marketplace?: string;
|
|
404
|
+
/** Why plain sessions do not load a loadable folder. */
|
|
405
|
+
unpublishedReason?: ClaudePluginUnpublishedReason;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** A Capability whose contributions could not be read, so its Claude plugin folders are unknown. */
|
|
409
|
+
export interface ClaudePluginContributionFailure {
|
|
410
|
+
capability: string;
|
|
411
|
+
reason: string;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** One loadable folder plain sessions do not load after a publication. */
|
|
415
|
+
export interface ClaudePluginUnpublished {
|
|
416
|
+
capability: string;
|
|
417
|
+
path: string;
|
|
418
|
+
reason: ClaudePluginUnpublishedReason;
|
|
419
|
+
/** The reason as a phrase after the folder's path, as `mate wrap`'s note uses it. */
|
|
420
|
+
detail: string;
|
|
421
|
+
/** The whole sentence a launch prints for the folder. */
|
|
422
|
+
warning: string;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** The enabled Capabilities' Claude plugin folders, as one inspection or publication found them. */
|
|
426
|
+
export interface ClaudePluginFolders {
|
|
427
|
+
/** Loadable folders, deduplicated, in declaration order: what a managed launch may pass. */
|
|
428
|
+
readonly launch: readonly string[];
|
|
429
|
+
/** One sentence per declared folder left out of every launch. */
|
|
430
|
+
readonly loadWarnings: readonly string[];
|
|
431
|
+
/** Folders the Claude wrap enables. */
|
|
432
|
+
readonly published: readonly PublishedClaudePluginFolder[];
|
|
433
|
+
/** Every declaration, duplicates included, in doctor's report shape. */
|
|
434
|
+
readonly report: readonly ClaudePluginDirectoryReport[];
|
|
435
|
+
/** Present only on a publication's result: what it left undone. */
|
|
436
|
+
readonly publication?: {
|
|
437
|
+
/** Loadable folders plain sessions do not load, one entry each, with its sentence. */
|
|
438
|
+
readonly unpublished: readonly ClaudePluginUnpublished[];
|
|
439
|
+
/** Failures removing Mate's file from disabled Capabilities' folders. */
|
|
440
|
+
readonly warnings: readonly string[];
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface ClaudePluginFoldersOptions {
|
|
445
|
+
/** Mate's bundled plugin, which every managed launch passes before these. */
|
|
446
|
+
bundledPluginRoot: string;
|
|
447
|
+
/** The user's marketplaces; read from the user's Claude Code configuration when absent. */
|
|
448
|
+
userMarketplaces?: ClaudeUserMarketplaces;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Why a loadable folder is not published, as a phrase after its path; undefined
|
|
453
|
+
* when it is. `writeError` is a publication's failure writing Mate's file.
|
|
454
|
+
*/
|
|
455
|
+
function unpublishedDetail(
|
|
456
|
+
folderClass: ClaudePluginFolderClass,
|
|
457
|
+
writeError: string | undefined,
|
|
458
|
+
): { reason: ClaudePluginUnpublishedReason; detail: string } | undefined {
|
|
459
|
+
switch (folderClass.kind) {
|
|
460
|
+
case "foreign-marketplace":
|
|
461
|
+
return {
|
|
462
|
+
reason: "foreign-marketplace",
|
|
463
|
+
detail: `holds a ${CLAUDE_PLUGIN_MARKETPLACE} that Mate did not write and that does not list the plugin in place`,
|
|
464
|
+
};
|
|
465
|
+
case "invalid-marketplace-name":
|
|
466
|
+
return {
|
|
467
|
+
reason: "invalid-marketplace-name",
|
|
468
|
+
detail: "has a plugin name that cannot form a Claude Code marketplace name",
|
|
469
|
+
};
|
|
470
|
+
case "name-conflict":
|
|
471
|
+
return {
|
|
472
|
+
reason: "name-conflict",
|
|
473
|
+
detail: `would publish as marketplace ${folderClass.marketplace}, a name already in use with another source`,
|
|
474
|
+
};
|
|
475
|
+
case "pending-marketplace":
|
|
476
|
+
return {
|
|
477
|
+
reason: "missing-marketplace",
|
|
478
|
+
detail: `holds no current ${CLAUDE_PLUGIN_MARKETPLACE}, and Mate could not write one${writeError ? ` (${writeError})` : ""}`,
|
|
479
|
+
};
|
|
480
|
+
default:
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function reportOf({
|
|
486
|
+
capability,
|
|
487
|
+
declared,
|
|
488
|
+
class: folderClass,
|
|
489
|
+
}: ClassifiedFolder): ClaudePluginDirectoryReport {
|
|
490
|
+
if (folderClass.kind === "not-loadable") {
|
|
491
|
+
return { capability, path: declared, loadable: false, problem: folderClass.problem };
|
|
492
|
+
}
|
|
493
|
+
const report: ClaudePluginDirectoryReport = { capability, path: declared, loadable: true };
|
|
494
|
+
if (claimsMarketplace(folderClass)) report.marketplace = folderClass.marketplace;
|
|
495
|
+
else report.unpublishedReason = folderClass.kind;
|
|
496
|
+
return report;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function foldersValue(
|
|
500
|
+
folders: ClassifiedFolder[],
|
|
501
|
+
publication?: { writeErrors: ReadonlyMap<string, string>; warnings: string[] },
|
|
502
|
+
): ClaudePluginFolders {
|
|
503
|
+
const firsts = folders.filter((folder) => folder.first);
|
|
504
|
+
const value: ClaudePluginFolders = {
|
|
505
|
+
launch: firsts.flatMap((folder) => (folder.class.kind === "not-loadable" ? [] : [folder.path])),
|
|
506
|
+
loadWarnings: firsts.flatMap(({ capability, declared, class: folderClass }) =>
|
|
507
|
+
folderClass.kind === "not-loadable"
|
|
508
|
+
? [`${capability}: Claude plugin folder ${declared} ${LOAD_WARNINGS[folderClass.problem]}`]
|
|
509
|
+
: [],
|
|
510
|
+
),
|
|
511
|
+
published: firsts.flatMap(({ path: folder, class: folderClass }) =>
|
|
512
|
+
folderClass.kind === "author-marketplace" || folderClass.kind === "mate-marketplace"
|
|
513
|
+
? [{ path: folder, plugin: folderClass.plugin, marketplace: folderClass.marketplace }]
|
|
514
|
+
: [],
|
|
515
|
+
),
|
|
516
|
+
report: folders.map(reportOf),
|
|
517
|
+
};
|
|
518
|
+
if (!publication) return value;
|
|
519
|
+
return {
|
|
520
|
+
...value,
|
|
521
|
+
publication: {
|
|
522
|
+
unpublished: firsts.flatMap((folder) =>
|
|
523
|
+
unpublishedEntries(folder, publication.writeErrors.get(folder.key)),
|
|
524
|
+
),
|
|
525
|
+
warnings: publication.warnings,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** The folder's entry when a publication left it unpublished; none when it is published. */
|
|
531
|
+
function unpublishedEntries(
|
|
532
|
+
{ capability, path: folder, class: folderClass }: ClassifiedFolder,
|
|
533
|
+
writeError: string | undefined,
|
|
534
|
+
): ClaudePluginUnpublished[] {
|
|
535
|
+
const unpublished = unpublishedDetail(folderClass, writeError);
|
|
536
|
+
if (!unpublished) return [];
|
|
537
|
+
return [
|
|
538
|
+
{
|
|
539
|
+
capability,
|
|
540
|
+
path: folder,
|
|
541
|
+
...unpublished,
|
|
542
|
+
warning: `${capability}: Claude plugin folder ${folder} ${unpublished.detail}; plain \`claude\` sessions do not load it.`,
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* The enabled Capabilities' Claude plugin folders, read-only. A folder Mate has
|
|
549
|
+
* not written a current marketplace file for yet is not published, and its
|
|
550
|
+
* report names the marketplace Mate will write.
|
|
551
|
+
*/
|
|
552
|
+
export async function inspectClaudePluginFolders(
|
|
553
|
+
contributions: CapabilityContributionInput[],
|
|
554
|
+
options: ClaudePluginFoldersOptions,
|
|
555
|
+
): Promise<ClaudePluginFolders> {
|
|
556
|
+
const userMarketplaces = options.userMarketplaces ?? (await readClaudeUserMarketplaces());
|
|
557
|
+
return foldersValue(
|
|
558
|
+
await classifyFolders(contributions, options.bundledPluginRoot, userMarketplaces),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Writes Mate's marketplace file into every enabled folder that has none or a
|
|
564
|
+
* stale one of Mate's, removes Mate's file from folders only disabled
|
|
565
|
+
* Capabilities declare, then inspects. Never touches a file Mate did not write.
|
|
566
|
+
* A file it could not write leaves the folder unpublished, with the error.
|
|
567
|
+
*/
|
|
568
|
+
export async function publishClaudePluginFolders(
|
|
569
|
+
contributions: CapabilityContributionInput[],
|
|
570
|
+
options: ClaudePluginFoldersOptions,
|
|
571
|
+
): Promise<ClaudePluginFolders> {
|
|
572
|
+
const userMarketplaces = options.userMarketplaces ?? (await readClaudeUserMarketplaces());
|
|
573
|
+
const before = await classifyFolders(contributions, options.bundledPluginRoot, userMarketplaces);
|
|
574
|
+
const writeErrors = await writePendingMarketplaces(before);
|
|
575
|
+
const enabledKeys = new Set(before.map((folder) => folder.key));
|
|
576
|
+
const warnings = await removeDisabledMarketplaces(contributions, enabledKeys);
|
|
577
|
+
return foldersValue(
|
|
578
|
+
await classifyFolders(contributions, options.bundledPluginRoot, userMarketplaces),
|
|
579
|
+
{ writeErrors, warnings },
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Writes Mate's file into each pending folder; returns each failure's message by folder key. */
|
|
584
|
+
async function writePendingMarketplaces(folders: ClassifiedFolder[]): Promise<Map<string, string>> {
|
|
585
|
+
const writeErrors = new Map<string, string>();
|
|
586
|
+
for (const folder of folders) {
|
|
587
|
+
if (!folder.first || folder.class.kind !== "pending-marketplace") continue;
|
|
588
|
+
try {
|
|
589
|
+
await fs.writeFile(
|
|
590
|
+
path.join(folder.path, CLAUDE_PLUGIN_MARKETPLACE),
|
|
591
|
+
mateMarketplaceText(folder.class.plugin),
|
|
592
|
+
"utf8",
|
|
593
|
+
);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
writeErrors.set(folder.key, (error as Error).message);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return writeErrors;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Removes Mate's file from folders only disabled Capabilities declare; returns
|
|
603
|
+
* one warning per file it could not remove.
|
|
604
|
+
*/
|
|
605
|
+
async function removeDisabledMarketplaces(
|
|
606
|
+
contributions: CapabilityContributionInput[],
|
|
607
|
+
enabledKeys: ReadonlySet<string>,
|
|
608
|
+
): Promise<string[]> {
|
|
609
|
+
const warnings: string[] = [];
|
|
610
|
+
for (const { capability, directory } of declaredClaudePluginDirectories(contributions, false)) {
|
|
611
|
+
if (!path.isAbsolute(directory)) continue;
|
|
612
|
+
if (enabledKeys.has(await claudePluginDirectoryKey(directory))) continue;
|
|
613
|
+
const file = await readMarketplaceFile(directory);
|
|
614
|
+
if (file.state !== "present" || !isMateMarketplace(file.json)) continue;
|
|
615
|
+
const marketplacePath = path.join(directory, CLAUDE_PLUGIN_MARKETPLACE);
|
|
616
|
+
try {
|
|
617
|
+
await fs.rm(marketplacePath, { force: true });
|
|
618
|
+
} catch (error) {
|
|
619
|
+
warnings.push(
|
|
620
|
+
`${capability}: could not remove ${marketplacePath}: ${(error as Error).message}.`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return warnings;
|
|
625
|
+
}
|