@kolisachint/hoocode-agent 0.5.2 → 0.5.4
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/CHANGELOG.md +31 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -7
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +10 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/capabilities/deferral.d.ts +78 -0
- package/dist/core/capabilities/deferral.d.ts.map +1 -0
- package/dist/core/capabilities/deferral.js +99 -0
- package/dist/core/capabilities/deferral.js.map +1 -0
- package/dist/core/capabilities/dense.d.ts +51 -0
- package/dist/core/capabilities/dense.d.ts.map +1 -0
- package/dist/core/capabilities/dense.js +121 -0
- package/dist/core/capabilities/dense.js.map +1 -0
- package/dist/core/capabilities/lexical.d.ts +43 -0
- package/dist/core/capabilities/lexical.d.ts.map +1 -0
- package/dist/core/capabilities/lexical.js +104 -0
- package/dist/core/capabilities/lexical.js.map +1 -0
- package/dist/core/capabilities/registry.d.ts +56 -0
- package/dist/core/capabilities/registry.d.ts.map +1 -0
- package/dist/core/capabilities/registry.js +0 -0
- package/dist/core/capabilities/registry.js.map +1 -0
- package/dist/core/capabilities/search.d.ts +47 -0
- package/dist/core/capabilities/search.d.ts.map +1 -0
- package/dist/core/capabilities/search.js +82 -0
- package/dist/core/capabilities/search.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +27 -3
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +61 -7
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +56 -9
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
- package/dist/core/extensions/plugins/authoring.js +0 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -1
- package/dist/core/extensions/plugins/drift.d.ts +75 -0
- package/dist/core/extensions/plugins/drift.d.ts.map +1 -0
- package/dist/core/extensions/plugins/drift.js +194 -0
- package/dist/core/extensions/plugins/drift.js.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/claude.js +5 -0
- package/dist/core/extensions/plugins/formats/claude.js.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.d.ts +6 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.js +57 -16
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
- package/dist/core/extensions/plugins/formats/index.d.ts +39 -1
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/index.js +90 -13
- package/dist/core/extensions/plugins/formats/index.js.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +14 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.js +37 -4
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
- package/dist/core/extensions/plugins/formats/platform-targets.d.ts +48 -24
- package/dist/core/extensions/plugins/formats/platform-targets.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/platform-targets.js +62 -30
- package/dist/core/extensions/plugins/formats/platform-targets.js.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.d.ts +22 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.js +81 -4
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
- package/dist/core/extensions/plugins/formats/types.d.ts +10 -1
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/types.js.map +1 -1
- package/dist/core/extensions/plugins/gates.d.ts +64 -0
- package/dist/core/extensions/plugins/gates.d.ts.map +1 -0
- package/dist/core/extensions/plugins/gates.js +297 -0
- package/dist/core/extensions/plugins/gates.js.map +1 -0
- package/dist/core/extensions/plugins/hooks-bridge.d.ts +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.js +12 -8
- package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -1
- package/dist/core/extensions/plugins/index.d.ts +36 -5
- package/dist/core/extensions/plugins/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/index.js +66 -15
- package/dist/core/extensions/plugins/index.js.map +1 -1
- package/dist/core/extensions/plugins/install.d.ts +43 -24
- package/dist/core/extensions/plugins/install.d.ts.map +1 -1
- package/dist/core/extensions/plugins/install.js +167 -54
- package/dist/core/extensions/plugins/install.js.map +1 -1
- package/dist/core/extensions/plugins/locations.d.ts +84 -0
- package/dist/core/extensions/plugins/locations.d.ts.map +1 -0
- package/dist/core/extensions/plugins/locations.js +137 -0
- package/dist/core/extensions/plugins/locations.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +21 -1
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +2 -2
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/packaging.d.ts +115 -0
- package/dist/core/extensions/plugins/packaging.d.ts.map +1 -0
- package/dist/core/extensions/plugins/packaging.js +362 -0
- package/dist/core/extensions/plugins/packaging.js.map +1 -0
- package/dist/core/extensions/plugins/smoke.d.ts +33 -0
- package/dist/core/extensions/plugins/smoke.d.ts.map +1 -0
- package/dist/core/extensions/plugins/smoke.js +231 -0
- package/dist/core/extensions/plugins/smoke.js.map +1 -0
- package/dist/core/extensions/plugins/trigger-eval.d.ts +136 -0
- package/dist/core/extensions/plugins/trigger-eval.d.ts.map +1 -0
- package/dist/core/extensions/plugins/trigger-eval.js +0 -0
- package/dist/core/extensions/plugins/trigger-eval.js.map +1 -0
- package/dist/core/package-manager.d.ts +6 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +15 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/skills.d.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +23 -3
- package/dist/core/skills.js.map +1 -1
- package/dist/core/tools/package-plugin.d.ts +19 -0
- package/dist/core/tools/package-plugin.d.ts.map +1 -0
- package/dist/core/tools/package-plugin.js +81 -0
- package/dist/core/tools/package-plugin.js.map +1 -0
- package/dist/core/tools/plugin-tool-names.d.ts +1 -0
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
- package/dist/core/tools/plugin-tool-names.js +6 -0
- package/dist/core/tools/plugin-tool-names.js.map +1 -1
- package/dist/core/tools/plugins.d.ts.map +1 -1
- package/dist/core/tools/plugins.js +48 -7
- package/dist/core/tools/plugins.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +49 -21
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/core/tools/webfetch.d.ts +3 -1
- package/dist/core/tools/webfetch.d.ts.map +1 -1
- package/dist/core/tools/webfetch.js +8 -2
- package/dist/core/tools/webfetch.js.map +1 -1
- package/dist/core/tools/websearch.d.ts +2 -0
- package/dist/core/tools/websearch.d.ts.map +1 -1
- package/dist/core/tools/websearch.js +2 -1
- package/dist/core/tools/websearch.js.map +1 -1
- package/dist/core/tools/webtools-shared.d.ts +23 -2
- package/dist/core/tools/webtools-shared.d.ts.map +1 -1
- package/dist/core/tools/webtools-shared.js +59 -9
- package/dist/core/tools/webtools-shared.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +14 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +77 -18
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/mcp-deferred.d.ts +39 -2
- package/dist/extensions/core/mcp-deferred.d.ts.map +1 -1
- package/dist/extensions/core/mcp-deferred.js +74 -10
- package/dist/extensions/core/mcp-deferred.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +52 -9
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/extensions/core/scaffold.d.ts +3 -3
- package/dist/extensions/core/scaffold.d.ts.map +1 -1
- package/dist/extensions/core/scaffold.js +9 -9
- package/dist/extensions/core/scaffold.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +25 -7
- package/dist/main.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +8 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +12 -3
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/providers.md +11 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaging — everything that has to happen before a plugin can be published,
|
|
3
|
+
* and nothing that publishes it.
|
|
4
|
+
*
|
|
5
|
+
* The split is the point. Preparing a release is mechanical: run the gates at
|
|
6
|
+
* their strictest, write a README, produce the marketplace index entry. All of
|
|
7
|
+
* that is automated. *Publishing* — pushing an artifact into a marketplace that
|
|
8
|
+
* other agents install from autonomously — stays a human act, because an agent
|
|
9
|
+
* that can do it unattended is a supply-chain compromise primitive. See
|
|
10
|
+
* docs/plugin-system-architecture.md §3.2–§3.3.
|
|
11
|
+
*
|
|
12
|
+
* Packaging works **in place**, in the plugin's production home. There is no
|
|
13
|
+
* separate output location: by the time a plugin reaches here it is already in
|
|
14
|
+
* its target platform's layout, and copying it somewhere else would just create
|
|
15
|
+
* a fourth directory role to confuse with the other three (§8.3).
|
|
16
|
+
*
|
|
17
|
+
* The two platforms are not symmetric here, and the lane is built for the harder
|
|
18
|
+
* one. A `claude` plugin is already live locally from `~/.claude/skills/<id>/`,
|
|
19
|
+
* so publishing is optional and additive. For `github`, local installs are
|
|
20
|
+
* deprecated — publishing is the *only* route into the ecosystem — so the
|
|
21
|
+
* GitHub flow (a PR against a marketplace repo's index) is the primary case and
|
|
22
|
+
* the Claude submission is the variant.
|
|
23
|
+
*/
|
|
24
|
+
import type { CapabilityDoc } from "../../capabilities/registry.js";
|
|
25
|
+
import { type PluginPlatform } from "./formats/platform-targets.js";
|
|
26
|
+
import { type GateResult } from "./gates.js";
|
|
27
|
+
import type { NormalizedPlugin } from "./manifest.js";
|
|
28
|
+
import { type TriggerJudge } from "./trigger-eval.js";
|
|
29
|
+
/**
|
|
30
|
+
* Where packaging records what it produced, at the plugin root.
|
|
31
|
+
*
|
|
32
|
+
* A dotfile beside the existing `.authored.json` provenance marker rather than a
|
|
33
|
+
* visible `marketplace-entry.json`: the plugin directory is the thing that gets
|
|
34
|
+
* copied into a marketplace repo, and a build record is not part of the artifact.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PUBLISH_RECORD_FILE = ".hoocode-publish.json";
|
|
37
|
+
/** A `marketplace.json` `plugins[]` entry, ready to paste into an index. */
|
|
38
|
+
export interface MarketplaceEntry {
|
|
39
|
+
name: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
version?: string;
|
|
42
|
+
source: string | {
|
|
43
|
+
source: "git";
|
|
44
|
+
url: string;
|
|
45
|
+
} | {
|
|
46
|
+
source: "github";
|
|
47
|
+
repo: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface PublishRecord {
|
|
51
|
+
packagedAt: string;
|
|
52
|
+
platform: PluginPlatform;
|
|
53
|
+
entry: MarketplaceEntry;
|
|
54
|
+
/** Whether the gates were green when this record was written. */
|
|
55
|
+
ok: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface PackageResult {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
dir: string;
|
|
60
|
+
platform: PluginPlatform;
|
|
61
|
+
/** Files packaging created or refreshed, relative to the plugin root. */
|
|
62
|
+
written: string[];
|
|
63
|
+
entry: MarketplaceEntry;
|
|
64
|
+
evaluation: GateResult;
|
|
65
|
+
/** Human-facing next steps; deliberately instructions, not actions. */
|
|
66
|
+
instructions: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Which ecosystem this plugin is being packaged for.
|
|
70
|
+
*
|
|
71
|
+
* Where it lives is the strongest evidence — the production homes are per
|
|
72
|
+
* platform, so a plugin under `~/.agents/publish/github/` is a github artifact
|
|
73
|
+
* whatever its manifests say. A plugin outside a production home (installed, or
|
|
74
|
+
* authored by an older version) falls back to what it declares, then to the
|
|
75
|
+
* session default.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolvePackagePlatform(plugin: NormalizedPlugin): PluginPlatform;
|
|
78
|
+
/** True when the entry still needs a human to say where the plugin will be hosted. */
|
|
79
|
+
export declare function entryNeedsSource(entry: MarketplaceEntry): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Package a plugin for publication: gate it at full strictness, write a README,
|
|
82
|
+
* produce its marketplace entry, and record what it produced. Touches no remote.
|
|
83
|
+
*/
|
|
84
|
+
export declare function packagePlugin(plugin: NormalizedPlugin, platformOverride?: PluginPlatform, options?: {
|
|
85
|
+
judge?: TriggerJudge;
|
|
86
|
+
distractors?: readonly CapabilityDoc[];
|
|
87
|
+
}): Promise<PackageResult>;
|
|
88
|
+
export interface StageResult {
|
|
89
|
+
ok: boolean;
|
|
90
|
+
message: string;
|
|
91
|
+
/** Path the plugin was copied to, relative to the marketplace root. */
|
|
92
|
+
pluginPath?: string;
|
|
93
|
+
/** The index that was written, relative to the marketplace root. */
|
|
94
|
+
indexPath?: string;
|
|
95
|
+
/** True when the marketplace had no index for this platform and one was created. */
|
|
96
|
+
createdIndex?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Copy a packaged plugin into a **local** marketplace checkout and add its index
|
|
100
|
+
* entry, leaving the result uncommitted.
|
|
101
|
+
*
|
|
102
|
+
* This is the far side of the automation line drawn in §3.3, and it is where the
|
|
103
|
+
* line actually falls. Everything here is local, reversible, and shows up in
|
|
104
|
+
* `git status` — the human reviews a diff and decides. What stays manual is the
|
|
105
|
+
* step that leaves the machine: no commit, no push, no PR. Those are what make a
|
|
106
|
+
* plugin installable by other people's agents, and that is the act install is
|
|
107
|
+
* trusting a human to have performed.
|
|
108
|
+
*
|
|
109
|
+
* Only reachable from `/plugin publish`, never from a tool: a human typed the
|
|
110
|
+
* command, which is the whole point.
|
|
111
|
+
*/
|
|
112
|
+
export declare function stageIntoMarketplace(plugin: NormalizedPlugin, platform: PluginPlatform, marketplaceDir: string): StageResult;
|
|
113
|
+
/** Read back the record packaging left, for callers that want to show it. */
|
|
114
|
+
export declare function readPublishRecord(dir: string): PublishRecord | undefined;
|
|
115
|
+
//# sourceMappingURL=packaging.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packaging.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/packaging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAoB,KAAK,cAAc,EAA0B,MAAM,+BAA+B,CAAC;AAC9G,OAAO,EAAwC,KAAK,UAAU,EAAgC,MAAM,YAAY,CAAC;AAEjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAKN,KAAK,YAAY,EAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAK3D,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACrF;AAED,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,gBAAgB,CAAC;IACxB,iEAAiE;IACjE,EAAE,EAAE,OAAO,CAAC;CACZ;AAED,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,cAAc,CAAC;IACzB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAQ/E;AA4GD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAEjE;AAwBD;;;GAGG;AACH,wBAAsB,aAAa,CAClC,MAAM,EAAE,gBAAgB,EACxB,gBAAgB,CAAC,EAAE,cAAc,EACjC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,YAAY,CAAC;IAAC,WAAW,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;CAAO,GAC5E,OAAO,CAAC,aAAa,CAAC,CAwFxB;AAiBD,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,cAAc,EACxB,cAAc,EAAE,MAAM,GACpB,WAAW,CAyDb;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAOxE","sourcesContent":["/**\n * Packaging — everything that has to happen before a plugin can be published,\n * and nothing that publishes it.\n *\n * The split is the point. Preparing a release is mechanical: run the gates at\n * their strictest, write a README, produce the marketplace index entry. All of\n * that is automated. *Publishing* — pushing an artifact into a marketplace that\n * other agents install from autonomously — stays a human act, because an agent\n * that can do it unattended is a supply-chain compromise primitive. See\n * docs/plugin-system-architecture.md §3.2–§3.3.\n *\n * Packaging works **in place**, in the plugin's production home. There is no\n * separate output location: by the time a plugin reaches here it is already in\n * its target platform's layout, and copying it somewhere else would just create\n * a fourth directory role to confuse with the other three (§8.3).\n *\n * The two platforms are not symmetric here, and the lane is built for the harder\n * one. A `claude` plugin is already live locally from `~/.claude/skills/<id>/`,\n * so publishing is optional and additive. For `github`, local installs are\n * deprecated — publishing is the *only* route into the ecosystem — so the\n * GitHub flow (a PR against a marketplace repo's index) is the primary case and\n * the Claude submission is the variant.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { CapabilityDoc } from \"../../capabilities/registry.js\";\nimport { isPluginPlatform, type PluginPlatform, resolvePluginPlatforms } from \"./formats/platform-targets.js\";\nimport { formatGateFindings, type GateFinding, type GateResult, runStaticGates, withFindings } from \"./gates.js\";\nimport { productionRoot } from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { runSmokeGate } from \"./smoke.js\";\nimport {\n\tdistractorCandidates,\n\tloadTriggerCases,\n\townCandidates,\n\trunTriggerEval,\n\ttype TriggerJudge,\n\ttriggerFindings,\n} from \"./trigger-eval.js\";\n\n/**\n * Where packaging records what it produced, at the plugin root.\n *\n * A dotfile beside the existing `.authored.json` provenance marker rather than a\n * visible `marketplace-entry.json`: the plugin directory is the thing that gets\n * copied into a marketplace repo, and a build record is not part of the artifact.\n */\nexport const PUBLISH_RECORD_FILE = \".hoocode-publish.json\";\n\n/** Placeholder `source` for a plugin that is not in a git checkout — deliberately unusable as-is. */\nconst SOURCE_PLACEHOLDER = \"<REPLACE: git URL, or path within the marketplace repo>\";\n\n/** A `marketplace.json` `plugins[]` entry, ready to paste into an index. */\nexport interface MarketplaceEntry {\n\tname: string;\n\tdescription?: string;\n\tversion?: string;\n\tsource: string | { source: \"git\"; url: string } | { source: \"github\"; repo: string };\n}\n\nexport interface PublishRecord {\n\tpackagedAt: string;\n\tplatform: PluginPlatform;\n\tentry: MarketplaceEntry;\n\t/** Whether the gates were green when this record was written. */\n\tok: boolean;\n}\n\nexport interface PackageResult {\n\tok: boolean;\n\tdir: string;\n\tplatform: PluginPlatform;\n\t/** Files packaging created or refreshed, relative to the plugin root. */\n\twritten: string[];\n\tentry: MarketplaceEntry;\n\tevaluation: GateResult;\n\t/** Human-facing next steps; deliberately instructions, not actions. */\n\tinstructions: string;\n}\n\n/**\n * Which ecosystem this plugin is being packaged for.\n *\n * Where it lives is the strongest evidence — the production homes are per\n * platform, so a plugin under `~/.agents/publish/github/` is a github artifact\n * whatever its manifests say. A plugin outside a production home (installed, or\n * authored by an older version) falls back to what it declares, then to the\n * session default.\n */\nexport function resolvePackagePlatform(plugin: NormalizedPlugin): PluginPlatform {\n\tconst root = path.resolve(plugin.root);\n\tfor (const platform of [\"claude\", \"github\"] as const) {\n\t\tconst home = path.resolve(productionRoot(platform));\n\t\tif (root === home || root.startsWith(`${home}${path.sep}`)) return platform;\n\t}\n\tconst declared = plugin.supportPlatform.filter(isPluginPlatform);\n\treturn declared[0] ?? resolvePluginPlatforms()[0];\n}\n\n/** Best-effort origin URL of the repo containing `dir`, if it is in one. */\nfunction gitOriginUrl(dir: string): string | undefined {\n\ttry {\n\t\tconst url = execFileSync(\"git\", [\"-C\", dir, \"remote\", \"get-url\", \"origin\"], {\n\t\t\tencoding: \"utf8\",\n\t\t\ttimeout: 5_000,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t}).trim();\n\t\treturn url || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction capabilityLines(plugin: NormalizedPlugin): string[] {\n\tconst lines: string[] = [];\n\tconst add = (label: string, present: unknown) => {\n\t\tif (present) lines.push(`- ${label}`);\n\t};\n\tadd(\"Skills\", plugin.skillsDir);\n\tadd(\"Slash commands\", plugin.commandsDir);\n\tadd(\"Subagents\", plugin.agentsDir);\n\tadd(\"Hooks\", plugin.hooks);\n\tadd(\"MCP servers\", plugin.mcpServers);\n\treturn lines;\n}\n\nfunction installSection(plugin: NormalizedPlugin, platform: PluginPlatform): string[] {\n\tif (platform === \"claude\") {\n\t\treturn [\n\t\t\t\"## Install\",\n\t\t\t\"\",\n\t\t\t\"```bash\",\n\t\t\t\"claude plugin marketplace add <marketplace-repo>\",\n\t\t\t`claude plugin install ${plugin.id}@<marketplace>`,\n\t\t\t\"```\",\n\t\t\t\"\",\n\t\t\t`Or copy this directory to \\`~/.claude/skills/${plugin.id}/\\` — a folder there`,\n\t\t\t\"carrying a `.claude-plugin/plugin.json` loads with no install step.\",\n\t\t];\n\t}\n\treturn [\n\t\t\"## Install\",\n\t\t\"\",\n\t\t\"```bash\",\n\t\t\"copilot plugin marketplace add <marketplace-repo>\",\n\t\t`copilot plugin install ${plugin.id}@<marketplace>`,\n\t\t\"```\",\n\t];\n}\n\n/**\n * Generate a README from what the plugin actually contains.\n *\n * Never overwrites: a hand-written README is the author's description of their\n * own plugin, and a generated one derived from the directory listing is strictly\n * worse. This only fills the gap where there is nothing at all.\n */\nfunction writeReadme(plugin: NormalizedPlugin, platform: PluginPlatform): string | undefined {\n\tconst file = path.join(plugin.root, \"README.md\");\n\tif (existsSync(file)) return undefined;\n\n\tconst capabilities = capabilityLines(plugin);\n\tconst body = [\n\t\t`# ${plugin.id}`,\n\t\t\"\",\n\t\tplugin.description ?? \"_No description provided._\",\n\t\t\"\",\n\t\t...(capabilities.length > 0 ? [\"## Provides\", \"\", ...capabilities, \"\"] : []),\n\t\t...installSection(plugin, platform),\n\t\t\"\",\n\t\t...(plugin.unsupportedSurfaces?.length\n\t\t\t? [\n\t\t\t\t\t\"## Notes\",\n\t\t\t\t\t\"\",\n\t\t\t\t\t`This plugin ships surfaces hoocode does not load: ${plugin.unsupportedSurfaces.join(\", \")}.`,\n\t\t\t\t\t\"They are preserved as-is and are the target platform's concern.\",\n\t\t\t\t\t\"\",\n\t\t\t\t]\n\t\t\t: []),\n\t].join(\"\\n\");\n\n\twriteFileSync(file, `${body.trimEnd()}\\n`, \"utf8\");\n\treturn \"README.md\";\n}\n\n/**\n * Build the marketplace index entry.\n *\n * `source` is where the *published* plugin will live, which packaging cannot\n * know: the artifact is on this machine and has not been pushed anywhere. When\n * the plugin sits in a git checkout its origin is a plausible starting point;\n * otherwise the field is a clearly-marked placeholder for the human to fill.\n * Guessing here would produce an index entry that resolves to nothing, which is\n * worse than an obvious blank.\n */\nfunction buildEntry(plugin: NormalizedPlugin): MarketplaceEntry {\n\tconst origin = gitOriginUrl(plugin.root);\n\treturn {\n\t\tname: plugin.id,\n\t\t...(plugin.description ? { description: plugin.description } : {}),\n\t\t...(plugin.version ? { version: plugin.version } : {}),\n\t\tsource: origin ? { source: \"git\", url: origin } : SOURCE_PLACEHOLDER,\n\t};\n}\n\n/** True when the entry still needs a human to say where the plugin will be hosted. */\nexport function entryNeedsSource(entry: MarketplaceEntry): boolean {\n\treturn entry.source === SOURCE_PLACEHOLDER;\n}\n\nfunction publishSteps(plugin: NormalizedPlugin, platform: PluginPlatform): string[] {\n\tif (platform === \"github\") {\n\t\treturn [\n\t\t\t\"Publish (GitHub Copilot) — this is the only route into the ecosystem,\",\n\t\t\t\"local installs being deprecated:\",\n\t\t\t\"\",\n\t\t\t\" 1. Copy this directory into your marketplace repo.\",\n\t\t\t\" 2. Add the entry above to its `.github/marketplace.json` `plugins` array.\",\n\t\t\t\" 3. Open a pull request; the plugin is installable once it merges.\",\n\t\t];\n\t}\n\treturn [\n\t\t`Publish (Claude Code) — optional: the plugin is already live locally from`,\n\t\t`\\`~/.claude/skills/${plugin.id}/\\`. To share it:`,\n\t\t\"\",\n\t\t\" 1. Copy this directory into your marketplace repo.\",\n\t\t\" 2. Add the entry above to its `.claude-plugin/marketplace.json` `plugins` array.\",\n\t\t\" 3. Open a pull request, or submit to the community directory for review\",\n\t\t\" (approved plugins are pinned to a commit SHA).\",\n\t];\n}\n\n/**\n * Package a plugin for publication: gate it at full strictness, write a README,\n * produce its marketplace entry, and record what it produced. Touches no remote.\n */\nexport async function packagePlugin(\n\tplugin: NormalizedPlugin,\n\tplatformOverride?: PluginPlatform,\n\toptions: { judge?: TriggerJudge; distractors?: readonly CapabilityDoc[] } = {},\n): Promise<PackageResult> {\n\tconst platform = platformOverride ?? resolvePackagePlatform(plugin);\n\n\t// A plugin can only be published into an ecosystem whose manifest it carries.\n\t// `claude plugin validate` says so too, but as a wall of validator output about\n\t// a missing file; a plugin in the native layout has simply been authored for\n\t// the wrong target, and that is worth saying in one sentence. Nothing is\n\t// written — there is no artifact to package.\n\tif (!plugin.supportPlatform.includes(platform)) {\n\t\tconst finding: GateFinding = {\n\t\t\tgate: \"G1\",\n\t\t\tseverity: \"error\",\n\t\t\tmessage:\n\t\t\t\t`Plugin \"${plugin.id}\" carries no ${platform} manifest (it offers: ${plugin.supportPlatform.join(\", \")}), ` +\n\t\t\t\t`so it cannot be published to that ecosystem. Re-author it with --platform ${platform}.`,\n\t\t};\n\t\tconst evaluation: GateResult = { ok: false, findings: [finding], conformance: \"round-trip\", plugin };\n\t\treturn {\n\t\t\tok: false,\n\t\t\tdir: plugin.root,\n\t\t\tplatform,\n\t\t\twritten: [],\n\t\t\tentry: buildEntry(plugin),\n\t\t\tevaluation,\n\t\t\tinstructions: `Plugin \"${plugin.id}\" cannot be packaged for ${platform}.\\n\\n${formatGateFindings(evaluation)}`,\n\t\t};\n\t}\n\n\t// Strict: warnings that are tolerable in a local plugin — an unresolvable\n\t// binary, a vendor-validator nit — are not tolerable in something other people\n\t// will install on the strength of the marketplace vouching for it.\n\tlet evaluation = runStaticGates(plugin.root, { platform, strict: true });\n\tif (evaluation.plugin) {\n\t\tevaluation = withFindings(evaluation, await runSmokeGate(evaluation.plugin), true);\n\t}\n\n\t// G4 is the publish path's gate (§4.3) and nothing else's: it costs a model\n\t// call, and the question it asks — is this description actually worth the\n\t// context it occupies — only becomes other people's problem at publication.\n\t// With no judge or no gold set it reports `not-run`, which is `info`, so a\n\t// machine without a model is never blocked from publishing by a check it\n\t// could not perform.\n\tconst own = ownCandidates(plugin);\n\tif (own.length > 0) {\n\t\tconst outcome = await runTriggerEval(\n\t\t\tplugin.id,\n\t\t\t[...own, ...distractorCandidates(plugin.id, options.distractors ?? [])],\n\t\t\tloadTriggerCases(plugin.root),\n\t\t\toptions.judge,\n\t\t);\n\t\tevaluation = withFindings(evaluation, triggerFindings(outcome), true);\n\t}\n\n\tconst written: string[] = [];\n\tconst readme = writeReadme(plugin, platform);\n\tif (readme) written.push(readme);\n\n\tconst entry = buildEntry(plugin);\n\tconst record: PublishRecord = {\n\t\tpackagedAt: new Date().toISOString(),\n\t\tplatform,\n\t\tentry,\n\t\tok: evaluation.ok,\n\t};\n\twriteFileSync(path.join(plugin.root, PUBLISH_RECORD_FILE), `${JSON.stringify(record, null, 2)}\\n`, \"utf8\");\n\twritten.push(PUBLISH_RECORD_FILE);\n\n\tconst instructions = [\n\t\tevaluation.ok\n\t\t\t? `Plugin \"${plugin.id}\" is ready to publish (${platform}). The remaining step is yours.`\n\t\t\t: `Plugin \"${plugin.id}\" is NOT ready to publish — these must pass first:`,\n\t\t\"\",\n\t\tformatGateFindings(evaluation),\n\t\t\"\",\n\t\t\"Marketplace entry:\",\n\t\t\"```json\",\n\t\tJSON.stringify(entry, null, 2),\n\t\t\"```\",\n\t\t...(entryNeedsSource(entry) ? [\"\", \"Replace `source` with where the plugin will be hosted.\"] : []),\n\t\t\"\",\n\t\t...publishSteps(plugin, platform),\n\t\t\"\",\n\t\t\"Publishing is deliberately not automated: an agent that can push executable\",\n\t\t\"code into a marketplace other agents install from unattended is a supply-chain\",\n\t\t\"risk, so the last step stays with you.\",\n\t].join(\"\\n\");\n\n\treturn { ok: evaluation.ok, dir: plugin.root, platform, written, entry, evaluation, instructions };\n}\n\n// ── Staging into a local marketplace checkout ────────────────────────────────\n\n/**\n * Files that exist for hoocode's benefit and have no business in a published\n * artifact: the provenance marker and packaging's own build record.\n */\nconst INTERNAL_FILES = new Set([PUBLISH_RECORD_FILE, \".authored.json\"]);\n\n/** Where each platform's marketplace index lives, and the subdirectory plugins are vendored into. */\nconst INDEX_FILE: Record<PluginPlatform, string> = {\n\tclaude: path.join(\".claude-plugin\", \"marketplace.json\"),\n\tgithub: path.join(\".github\", \"marketplace.json\"),\n};\nconst VENDOR_DIR = \"plugins\";\n\nexport interface StageResult {\n\tok: boolean;\n\tmessage: string;\n\t/** Path the plugin was copied to, relative to the marketplace root. */\n\tpluginPath?: string;\n\t/** The index that was written, relative to the marketplace root. */\n\tindexPath?: string;\n\t/** True when the marketplace had no index for this platform and one was created. */\n\tcreatedIndex?: boolean;\n}\n\ninterface RawIndex {\n\tname?: string;\n\tplugins?: Array<Record<string, unknown>>;\n\t[key: string]: unknown;\n}\n\n/**\n * Copy a packaged plugin into a **local** marketplace checkout and add its index\n * entry, leaving the result uncommitted.\n *\n * This is the far side of the automation line drawn in §3.3, and it is where the\n * line actually falls. Everything here is local, reversible, and shows up in\n * `git status` — the human reviews a diff and decides. What stays manual is the\n * step that leaves the machine: no commit, no push, no PR. Those are what make a\n * plugin installable by other people's agents, and that is the act install is\n * trusting a human to have performed.\n *\n * Only reachable from `/plugin publish`, never from a tool: a human typed the\n * command, which is the whole point.\n */\nexport function stageIntoMarketplace(\n\tplugin: NormalizedPlugin,\n\tplatform: PluginPlatform,\n\tmarketplaceDir: string,\n): StageResult {\n\tconst root = path.resolve(marketplaceDir);\n\tif (!existsSync(root)) {\n\t\treturn { ok: false, message: `Marketplace directory not found: ${root}` };\n\t}\n\tconst pluginRoot = path.resolve(plugin.root);\n\tif (pluginRoot === root || pluginRoot.startsWith(`${root}${path.sep}`)) {\n\t\treturn { ok: false, message: `Plugin \"${plugin.id}\" already lives inside ${root}; nothing to stage.` };\n\t}\n\n\tconst indexRel = INDEX_FILE[platform];\n\tconst indexAbs = path.join(root, indexRel);\n\tlet index: RawIndex;\n\tlet createdIndex = false;\n\tif (existsSync(indexAbs)) {\n\t\ttry {\n\t\t\tindex = JSON.parse(readFileSync(indexAbs, \"utf8\")) as RawIndex;\n\t\t} catch (error) {\n\t\t\treturn { ok: false, message: `${indexRel} is not valid JSON (${(error as Error).message}); fix it first.` };\n\t\t}\n\t} else {\n\t\t// A marketplace repo that does not have an index yet is a normal starting\n\t\t// point, and refusing here would mean the first plugin can never be staged.\n\t\tindex = { name: path.basename(root), plugins: [] };\n\t\tcreatedIndex = true;\n\t}\n\tif (!Array.isArray(index.plugins)) index.plugins = [];\n\n\tconst dest = path.join(root, VENDOR_DIR, plugin.id);\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(path.dirname(dest), { recursive: true });\n\tcpSync(pluginRoot, dest, {\n\t\trecursive: true,\n\t\tfilter: (src) => !INTERNAL_FILES.has(path.basename(src)),\n\t});\n\n\t// Vendored in the repo, so the source is a path relative to it — not the\n\t// git origin `packagePlugin` guessed, which points at wherever the plugin was\n\t// authored rather than at the marketplace.\n\tconst entry: MarketplaceEntry = { ...buildEntry(plugin), source: `./${VENDOR_DIR}/${plugin.id}` };\n\tconst existingAt = index.plugins.findIndex((p) => p?.name === plugin.id);\n\tif (existingAt >= 0) index.plugins[existingAt] = { ...index.plugins[existingAt], ...entry };\n\telse index.plugins.push(entry as unknown as Record<string, unknown>);\n\n\tmkdirSync(path.dirname(indexAbs), { recursive: true });\n\twriteFileSync(indexAbs, `${JSON.stringify(index, null, 2)}\\n`, \"utf8\");\n\n\tconst verb = existingAt >= 0 ? \"Updated\" : \"Added\";\n\treturn {\n\t\tok: true,\n\t\tmessage:\n\t\t\t`${verb} \"${plugin.id}\" in ${path.basename(root)}: copied to ${VENDOR_DIR}/${plugin.id}/ and ` +\n\t\t\t`${createdIndex ? \"created\" : \"updated\"} ${indexRel}. Nothing was committed or pushed.`,\n\t\tpluginPath: `${VENDOR_DIR}/${plugin.id}`,\n\t\tindexPath: indexRel,\n\t\tcreatedIndex,\n\t};\n}\n\n/** Read back the record packaging left, for callers that want to show it. */\nexport function readPublishRecord(dir: string): PublishRecord | undefined {\n\ttry {\n\t\tconst file = path.join(dir, PUBLISH_RECORD_FILE);\n\t\treturn existsSync(file) ? (JSON.parse(readFileSync(file, \"utf8\")) as PublishRecord) : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaging — everything that has to happen before a plugin can be published,
|
|
3
|
+
* and nothing that publishes it.
|
|
4
|
+
*
|
|
5
|
+
* The split is the point. Preparing a release is mechanical: run the gates at
|
|
6
|
+
* their strictest, write a README, produce the marketplace index entry. All of
|
|
7
|
+
* that is automated. *Publishing* — pushing an artifact into a marketplace that
|
|
8
|
+
* other agents install from autonomously — stays a human act, because an agent
|
|
9
|
+
* that can do it unattended is a supply-chain compromise primitive. See
|
|
10
|
+
* docs/plugin-system-architecture.md §3.2–§3.3.
|
|
11
|
+
*
|
|
12
|
+
* Packaging works **in place**, in the plugin's production home. There is no
|
|
13
|
+
* separate output location: by the time a plugin reaches here it is already in
|
|
14
|
+
* its target platform's layout, and copying it somewhere else would just create
|
|
15
|
+
* a fourth directory role to confuse with the other three (§8.3).
|
|
16
|
+
*
|
|
17
|
+
* The two platforms are not symmetric here, and the lane is built for the harder
|
|
18
|
+
* one. A `claude` plugin is already live locally from `~/.claude/skills/<id>/`,
|
|
19
|
+
* so publishing is optional and additive. For `github`, local installs are
|
|
20
|
+
* deprecated — publishing is the *only* route into the ecosystem — so the
|
|
21
|
+
* GitHub flow (a PR against a marketplace repo's index) is the primary case and
|
|
22
|
+
* the Claude submission is the variant.
|
|
23
|
+
*/
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { isPluginPlatform, resolvePluginPlatforms } from "./formats/platform-targets.js";
|
|
28
|
+
import { formatGateFindings, runStaticGates, withFindings } from "./gates.js";
|
|
29
|
+
import { productionRoot } from "./locations.js";
|
|
30
|
+
import { runSmokeGate } from "./smoke.js";
|
|
31
|
+
import { distractorCandidates, loadTriggerCases, ownCandidates, runTriggerEval, triggerFindings, } from "./trigger-eval.js";
|
|
32
|
+
/**
|
|
33
|
+
* Where packaging records what it produced, at the plugin root.
|
|
34
|
+
*
|
|
35
|
+
* A dotfile beside the existing `.authored.json` provenance marker rather than a
|
|
36
|
+
* visible `marketplace-entry.json`: the plugin directory is the thing that gets
|
|
37
|
+
* copied into a marketplace repo, and a build record is not part of the artifact.
|
|
38
|
+
*/
|
|
39
|
+
export const PUBLISH_RECORD_FILE = ".hoocode-publish.json";
|
|
40
|
+
/** Placeholder `source` for a plugin that is not in a git checkout — deliberately unusable as-is. */
|
|
41
|
+
const SOURCE_PLACEHOLDER = "<REPLACE: git URL, or path within the marketplace repo>";
|
|
42
|
+
/**
|
|
43
|
+
* Which ecosystem this plugin is being packaged for.
|
|
44
|
+
*
|
|
45
|
+
* Where it lives is the strongest evidence — the production homes are per
|
|
46
|
+
* platform, so a plugin under `~/.agents/publish/github/` is a github artifact
|
|
47
|
+
* whatever its manifests say. A plugin outside a production home (installed, or
|
|
48
|
+
* authored by an older version) falls back to what it declares, then to the
|
|
49
|
+
* session default.
|
|
50
|
+
*/
|
|
51
|
+
export function resolvePackagePlatform(plugin) {
|
|
52
|
+
const root = path.resolve(plugin.root);
|
|
53
|
+
for (const platform of ["claude", "github"]) {
|
|
54
|
+
const home = path.resolve(productionRoot(platform));
|
|
55
|
+
if (root === home || root.startsWith(`${home}${path.sep}`))
|
|
56
|
+
return platform;
|
|
57
|
+
}
|
|
58
|
+
const declared = plugin.supportPlatform.filter(isPluginPlatform);
|
|
59
|
+
return declared[0] ?? resolvePluginPlatforms()[0];
|
|
60
|
+
}
|
|
61
|
+
/** Best-effort origin URL of the repo containing `dir`, if it is in one. */
|
|
62
|
+
function gitOriginUrl(dir) {
|
|
63
|
+
try {
|
|
64
|
+
const url = execFileSync("git", ["-C", dir, "remote", "get-url", "origin"], {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 5_000,
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
+
}).trim();
|
|
69
|
+
return url || undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function capabilityLines(plugin) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
const add = (label, present) => {
|
|
78
|
+
if (present)
|
|
79
|
+
lines.push(`- ${label}`);
|
|
80
|
+
};
|
|
81
|
+
add("Skills", plugin.skillsDir);
|
|
82
|
+
add("Slash commands", plugin.commandsDir);
|
|
83
|
+
add("Subagents", plugin.agentsDir);
|
|
84
|
+
add("Hooks", plugin.hooks);
|
|
85
|
+
add("MCP servers", plugin.mcpServers);
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
function installSection(plugin, platform) {
|
|
89
|
+
if (platform === "claude") {
|
|
90
|
+
return [
|
|
91
|
+
"## Install",
|
|
92
|
+
"",
|
|
93
|
+
"```bash",
|
|
94
|
+
"claude plugin marketplace add <marketplace-repo>",
|
|
95
|
+
`claude plugin install ${plugin.id}@<marketplace>`,
|
|
96
|
+
"```",
|
|
97
|
+
"",
|
|
98
|
+
`Or copy this directory to \`~/.claude/skills/${plugin.id}/\` — a folder there`,
|
|
99
|
+
"carrying a `.claude-plugin/plugin.json` loads with no install step.",
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
return [
|
|
103
|
+
"## Install",
|
|
104
|
+
"",
|
|
105
|
+
"```bash",
|
|
106
|
+
"copilot plugin marketplace add <marketplace-repo>",
|
|
107
|
+
`copilot plugin install ${plugin.id}@<marketplace>`,
|
|
108
|
+
"```",
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Generate a README from what the plugin actually contains.
|
|
113
|
+
*
|
|
114
|
+
* Never overwrites: a hand-written README is the author's description of their
|
|
115
|
+
* own plugin, and a generated one derived from the directory listing is strictly
|
|
116
|
+
* worse. This only fills the gap where there is nothing at all.
|
|
117
|
+
*/
|
|
118
|
+
function writeReadme(plugin, platform) {
|
|
119
|
+
const file = path.join(plugin.root, "README.md");
|
|
120
|
+
if (existsSync(file))
|
|
121
|
+
return undefined;
|
|
122
|
+
const capabilities = capabilityLines(plugin);
|
|
123
|
+
const body = [
|
|
124
|
+
`# ${plugin.id}`,
|
|
125
|
+
"",
|
|
126
|
+
plugin.description ?? "_No description provided._",
|
|
127
|
+
"",
|
|
128
|
+
...(capabilities.length > 0 ? ["## Provides", "", ...capabilities, ""] : []),
|
|
129
|
+
...installSection(plugin, platform),
|
|
130
|
+
"",
|
|
131
|
+
...(plugin.unsupportedSurfaces?.length
|
|
132
|
+
? [
|
|
133
|
+
"## Notes",
|
|
134
|
+
"",
|
|
135
|
+
`This plugin ships surfaces hoocode does not load: ${plugin.unsupportedSurfaces.join(", ")}.`,
|
|
136
|
+
"They are preserved as-is and are the target platform's concern.",
|
|
137
|
+
"",
|
|
138
|
+
]
|
|
139
|
+
: []),
|
|
140
|
+
].join("\n");
|
|
141
|
+
writeFileSync(file, `${body.trimEnd()}\n`, "utf8");
|
|
142
|
+
return "README.md";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build the marketplace index entry.
|
|
146
|
+
*
|
|
147
|
+
* `source` is where the *published* plugin will live, which packaging cannot
|
|
148
|
+
* know: the artifact is on this machine and has not been pushed anywhere. When
|
|
149
|
+
* the plugin sits in a git checkout its origin is a plausible starting point;
|
|
150
|
+
* otherwise the field is a clearly-marked placeholder for the human to fill.
|
|
151
|
+
* Guessing here would produce an index entry that resolves to nothing, which is
|
|
152
|
+
* worse than an obvious blank.
|
|
153
|
+
*/
|
|
154
|
+
function buildEntry(plugin) {
|
|
155
|
+
const origin = gitOriginUrl(plugin.root);
|
|
156
|
+
return {
|
|
157
|
+
name: plugin.id,
|
|
158
|
+
...(plugin.description ? { description: plugin.description } : {}),
|
|
159
|
+
...(plugin.version ? { version: plugin.version } : {}),
|
|
160
|
+
source: origin ? { source: "git", url: origin } : SOURCE_PLACEHOLDER,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** True when the entry still needs a human to say where the plugin will be hosted. */
|
|
164
|
+
export function entryNeedsSource(entry) {
|
|
165
|
+
return entry.source === SOURCE_PLACEHOLDER;
|
|
166
|
+
}
|
|
167
|
+
function publishSteps(plugin, platform) {
|
|
168
|
+
if (platform === "github") {
|
|
169
|
+
return [
|
|
170
|
+
"Publish (GitHub Copilot) — this is the only route into the ecosystem,",
|
|
171
|
+
"local installs being deprecated:",
|
|
172
|
+
"",
|
|
173
|
+
" 1. Copy this directory into your marketplace repo.",
|
|
174
|
+
" 2. Add the entry above to its `.github/marketplace.json` `plugins` array.",
|
|
175
|
+
" 3. Open a pull request; the plugin is installable once it merges.",
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
return [
|
|
179
|
+
`Publish (Claude Code) — optional: the plugin is already live locally from`,
|
|
180
|
+
`\`~/.claude/skills/${plugin.id}/\`. To share it:`,
|
|
181
|
+
"",
|
|
182
|
+
" 1. Copy this directory into your marketplace repo.",
|
|
183
|
+
" 2. Add the entry above to its `.claude-plugin/marketplace.json` `plugins` array.",
|
|
184
|
+
" 3. Open a pull request, or submit to the community directory for review",
|
|
185
|
+
" (approved plugins are pinned to a commit SHA).",
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Package a plugin for publication: gate it at full strictness, write a README,
|
|
190
|
+
* produce its marketplace entry, and record what it produced. Touches no remote.
|
|
191
|
+
*/
|
|
192
|
+
export async function packagePlugin(plugin, platformOverride, options = {}) {
|
|
193
|
+
const platform = platformOverride ?? resolvePackagePlatform(plugin);
|
|
194
|
+
// A plugin can only be published into an ecosystem whose manifest it carries.
|
|
195
|
+
// `claude plugin validate` says so too, but as a wall of validator output about
|
|
196
|
+
// a missing file; a plugin in the native layout has simply been authored for
|
|
197
|
+
// the wrong target, and that is worth saying in one sentence. Nothing is
|
|
198
|
+
// written — there is no artifact to package.
|
|
199
|
+
if (!plugin.supportPlatform.includes(platform)) {
|
|
200
|
+
const finding = {
|
|
201
|
+
gate: "G1",
|
|
202
|
+
severity: "error",
|
|
203
|
+
message: `Plugin "${plugin.id}" carries no ${platform} manifest (it offers: ${plugin.supportPlatform.join(", ")}), ` +
|
|
204
|
+
`so it cannot be published to that ecosystem. Re-author it with --platform ${platform}.`,
|
|
205
|
+
};
|
|
206
|
+
const evaluation = { ok: false, findings: [finding], conformance: "round-trip", plugin };
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
dir: plugin.root,
|
|
210
|
+
platform,
|
|
211
|
+
written: [],
|
|
212
|
+
entry: buildEntry(plugin),
|
|
213
|
+
evaluation,
|
|
214
|
+
instructions: `Plugin "${plugin.id}" cannot be packaged for ${platform}.\n\n${formatGateFindings(evaluation)}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Strict: warnings that are tolerable in a local plugin — an unresolvable
|
|
218
|
+
// binary, a vendor-validator nit — are not tolerable in something other people
|
|
219
|
+
// will install on the strength of the marketplace vouching for it.
|
|
220
|
+
let evaluation = runStaticGates(plugin.root, { platform, strict: true });
|
|
221
|
+
if (evaluation.plugin) {
|
|
222
|
+
evaluation = withFindings(evaluation, await runSmokeGate(evaluation.plugin), true);
|
|
223
|
+
}
|
|
224
|
+
// G4 is the publish path's gate (§4.3) and nothing else's: it costs a model
|
|
225
|
+
// call, and the question it asks — is this description actually worth the
|
|
226
|
+
// context it occupies — only becomes other people's problem at publication.
|
|
227
|
+
// With no judge or no gold set it reports `not-run`, which is `info`, so a
|
|
228
|
+
// machine without a model is never blocked from publishing by a check it
|
|
229
|
+
// could not perform.
|
|
230
|
+
const own = ownCandidates(plugin);
|
|
231
|
+
if (own.length > 0) {
|
|
232
|
+
const outcome = await runTriggerEval(plugin.id, [...own, ...distractorCandidates(plugin.id, options.distractors ?? [])], loadTriggerCases(plugin.root), options.judge);
|
|
233
|
+
evaluation = withFindings(evaluation, triggerFindings(outcome), true);
|
|
234
|
+
}
|
|
235
|
+
const written = [];
|
|
236
|
+
const readme = writeReadme(plugin, platform);
|
|
237
|
+
if (readme)
|
|
238
|
+
written.push(readme);
|
|
239
|
+
const entry = buildEntry(plugin);
|
|
240
|
+
const record = {
|
|
241
|
+
packagedAt: new Date().toISOString(),
|
|
242
|
+
platform,
|
|
243
|
+
entry,
|
|
244
|
+
ok: evaluation.ok,
|
|
245
|
+
};
|
|
246
|
+
writeFileSync(path.join(plugin.root, PUBLISH_RECORD_FILE), `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
247
|
+
written.push(PUBLISH_RECORD_FILE);
|
|
248
|
+
const instructions = [
|
|
249
|
+
evaluation.ok
|
|
250
|
+
? `Plugin "${plugin.id}" is ready to publish (${platform}). The remaining step is yours.`
|
|
251
|
+
: `Plugin "${plugin.id}" is NOT ready to publish — these must pass first:`,
|
|
252
|
+
"",
|
|
253
|
+
formatGateFindings(evaluation),
|
|
254
|
+
"",
|
|
255
|
+
"Marketplace entry:",
|
|
256
|
+
"```json",
|
|
257
|
+
JSON.stringify(entry, null, 2),
|
|
258
|
+
"```",
|
|
259
|
+
...(entryNeedsSource(entry) ? ["", "Replace `source` with where the plugin will be hosted."] : []),
|
|
260
|
+
"",
|
|
261
|
+
...publishSteps(plugin, platform),
|
|
262
|
+
"",
|
|
263
|
+
"Publishing is deliberately not automated: an agent that can push executable",
|
|
264
|
+
"code into a marketplace other agents install from unattended is a supply-chain",
|
|
265
|
+
"risk, so the last step stays with you.",
|
|
266
|
+
].join("\n");
|
|
267
|
+
return { ok: evaluation.ok, dir: plugin.root, platform, written, entry, evaluation, instructions };
|
|
268
|
+
}
|
|
269
|
+
// ── Staging into a local marketplace checkout ────────────────────────────────
|
|
270
|
+
/**
|
|
271
|
+
* Files that exist for hoocode's benefit and have no business in a published
|
|
272
|
+
* artifact: the provenance marker and packaging's own build record.
|
|
273
|
+
*/
|
|
274
|
+
const INTERNAL_FILES = new Set([PUBLISH_RECORD_FILE, ".authored.json"]);
|
|
275
|
+
/** Where each platform's marketplace index lives, and the subdirectory plugins are vendored into. */
|
|
276
|
+
const INDEX_FILE = {
|
|
277
|
+
claude: path.join(".claude-plugin", "marketplace.json"),
|
|
278
|
+
github: path.join(".github", "marketplace.json"),
|
|
279
|
+
};
|
|
280
|
+
const VENDOR_DIR = "plugins";
|
|
281
|
+
/**
|
|
282
|
+
* Copy a packaged plugin into a **local** marketplace checkout and add its index
|
|
283
|
+
* entry, leaving the result uncommitted.
|
|
284
|
+
*
|
|
285
|
+
* This is the far side of the automation line drawn in §3.3, and it is where the
|
|
286
|
+
* line actually falls. Everything here is local, reversible, and shows up in
|
|
287
|
+
* `git status` — the human reviews a diff and decides. What stays manual is the
|
|
288
|
+
* step that leaves the machine: no commit, no push, no PR. Those are what make a
|
|
289
|
+
* plugin installable by other people's agents, and that is the act install is
|
|
290
|
+
* trusting a human to have performed.
|
|
291
|
+
*
|
|
292
|
+
* Only reachable from `/plugin publish`, never from a tool: a human typed the
|
|
293
|
+
* command, which is the whole point.
|
|
294
|
+
*/
|
|
295
|
+
export function stageIntoMarketplace(plugin, platform, marketplaceDir) {
|
|
296
|
+
const root = path.resolve(marketplaceDir);
|
|
297
|
+
if (!existsSync(root)) {
|
|
298
|
+
return { ok: false, message: `Marketplace directory not found: ${root}` };
|
|
299
|
+
}
|
|
300
|
+
const pluginRoot = path.resolve(plugin.root);
|
|
301
|
+
if (pluginRoot === root || pluginRoot.startsWith(`${root}${path.sep}`)) {
|
|
302
|
+
return { ok: false, message: `Plugin "${plugin.id}" already lives inside ${root}; nothing to stage.` };
|
|
303
|
+
}
|
|
304
|
+
const indexRel = INDEX_FILE[platform];
|
|
305
|
+
const indexAbs = path.join(root, indexRel);
|
|
306
|
+
let index;
|
|
307
|
+
let createdIndex = false;
|
|
308
|
+
if (existsSync(indexAbs)) {
|
|
309
|
+
try {
|
|
310
|
+
index = JSON.parse(readFileSync(indexAbs, "utf8"));
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
return { ok: false, message: `${indexRel} is not valid JSON (${error.message}); fix it first.` };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// A marketplace repo that does not have an index yet is a normal starting
|
|
318
|
+
// point, and refusing here would mean the first plugin can never be staged.
|
|
319
|
+
index = { name: path.basename(root), plugins: [] };
|
|
320
|
+
createdIndex = true;
|
|
321
|
+
}
|
|
322
|
+
if (!Array.isArray(index.plugins))
|
|
323
|
+
index.plugins = [];
|
|
324
|
+
const dest = path.join(root, VENDOR_DIR, plugin.id);
|
|
325
|
+
rmSync(dest, { recursive: true, force: true });
|
|
326
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
327
|
+
cpSync(pluginRoot, dest, {
|
|
328
|
+
recursive: true,
|
|
329
|
+
filter: (src) => !INTERNAL_FILES.has(path.basename(src)),
|
|
330
|
+
});
|
|
331
|
+
// Vendored in the repo, so the source is a path relative to it — not the
|
|
332
|
+
// git origin `packagePlugin` guessed, which points at wherever the plugin was
|
|
333
|
+
// authored rather than at the marketplace.
|
|
334
|
+
const entry = { ...buildEntry(plugin), source: `./${VENDOR_DIR}/${plugin.id}` };
|
|
335
|
+
const existingAt = index.plugins.findIndex((p) => p?.name === plugin.id);
|
|
336
|
+
if (existingAt >= 0)
|
|
337
|
+
index.plugins[existingAt] = { ...index.plugins[existingAt], ...entry };
|
|
338
|
+
else
|
|
339
|
+
index.plugins.push(entry);
|
|
340
|
+
mkdirSync(path.dirname(indexAbs), { recursive: true });
|
|
341
|
+
writeFileSync(indexAbs, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
342
|
+
const verb = existingAt >= 0 ? "Updated" : "Added";
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
message: `${verb} "${plugin.id}" in ${path.basename(root)}: copied to ${VENDOR_DIR}/${plugin.id}/ and ` +
|
|
346
|
+
`${createdIndex ? "created" : "updated"} ${indexRel}. Nothing was committed or pushed.`,
|
|
347
|
+
pluginPath: `${VENDOR_DIR}/${plugin.id}`,
|
|
348
|
+
indexPath: indexRel,
|
|
349
|
+
createdIndex,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/** Read back the record packaging left, for callers that want to show it. */
|
|
353
|
+
export function readPublishRecord(dir) {
|
|
354
|
+
try {
|
|
355
|
+
const file = path.join(dir, PUBLISH_RECORD_FILE);
|
|
356
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : undefined;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
//# sourceMappingURL=packaging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packaging.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/packaging.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Packaging — everything that has to happen before a plugin can be published,\n * and nothing that publishes it.\n *\n * The split is the point. Preparing a release is mechanical: run the gates at\n * their strictest, write a README, produce the marketplace index entry. All of\n * that is automated. *Publishing* — pushing an artifact into a marketplace that\n * other agents install from autonomously — stays a human act, because an agent\n * that can do it unattended is a supply-chain compromise primitive. See\n * docs/plugin-system-architecture.md §3.2–§3.3.\n *\n * Packaging works **in place**, in the plugin's production home. There is no\n * separate output location: by the time a plugin reaches here it is already in\n * its target platform's layout, and copying it somewhere else would just create\n * a fourth directory role to confuse with the other three (§8.3).\n *\n * The two platforms are not symmetric here, and the lane is built for the harder\n * one. A `claude` plugin is already live locally from `~/.claude/skills/<id>/`,\n * so publishing is optional and additive. For `github`, local installs are\n * deprecated — publishing is the *only* route into the ecosystem — so the\n * GitHub flow (a PR against a marketplace repo's index) is the primary case and\n * the Claude submission is the variant.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { CapabilityDoc } from \"../../capabilities/registry.js\";\nimport { isPluginPlatform, type PluginPlatform, resolvePluginPlatforms } from \"./formats/platform-targets.js\";\nimport { formatGateFindings, type GateFinding, type GateResult, runStaticGates, withFindings } from \"./gates.js\";\nimport { productionRoot } from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { runSmokeGate } from \"./smoke.js\";\nimport {\n\tdistractorCandidates,\n\tloadTriggerCases,\n\townCandidates,\n\trunTriggerEval,\n\ttype TriggerJudge,\n\ttriggerFindings,\n} from \"./trigger-eval.js\";\n\n/**\n * Where packaging records what it produced, at the plugin root.\n *\n * A dotfile beside the existing `.authored.json` provenance marker rather than a\n * visible `marketplace-entry.json`: the plugin directory is the thing that gets\n * copied into a marketplace repo, and a build record is not part of the artifact.\n */\nexport const PUBLISH_RECORD_FILE = \".hoocode-publish.json\";\n\n/** Placeholder `source` for a plugin that is not in a git checkout — deliberately unusable as-is. */\nconst SOURCE_PLACEHOLDER = \"<REPLACE: git URL, or path within the marketplace repo>\";\n\n/** A `marketplace.json` `plugins[]` entry, ready to paste into an index. */\nexport interface MarketplaceEntry {\n\tname: string;\n\tdescription?: string;\n\tversion?: string;\n\tsource: string | { source: \"git\"; url: string } | { source: \"github\"; repo: string };\n}\n\nexport interface PublishRecord {\n\tpackagedAt: string;\n\tplatform: PluginPlatform;\n\tentry: MarketplaceEntry;\n\t/** Whether the gates were green when this record was written. */\n\tok: boolean;\n}\n\nexport interface PackageResult {\n\tok: boolean;\n\tdir: string;\n\tplatform: PluginPlatform;\n\t/** Files packaging created or refreshed, relative to the plugin root. */\n\twritten: string[];\n\tentry: MarketplaceEntry;\n\tevaluation: GateResult;\n\t/** Human-facing next steps; deliberately instructions, not actions. */\n\tinstructions: string;\n}\n\n/**\n * Which ecosystem this plugin is being packaged for.\n *\n * Where it lives is the strongest evidence — the production homes are per\n * platform, so a plugin under `~/.agents/publish/github/` is a github artifact\n * whatever its manifests say. A plugin outside a production home (installed, or\n * authored by an older version) falls back to what it declares, then to the\n * session default.\n */\nexport function resolvePackagePlatform(plugin: NormalizedPlugin): PluginPlatform {\n\tconst root = path.resolve(plugin.root);\n\tfor (const platform of [\"claude\", \"github\"] as const) {\n\t\tconst home = path.resolve(productionRoot(platform));\n\t\tif (root === home || root.startsWith(`${home}${path.sep}`)) return platform;\n\t}\n\tconst declared = plugin.supportPlatform.filter(isPluginPlatform);\n\treturn declared[0] ?? resolvePluginPlatforms()[0];\n}\n\n/** Best-effort origin URL of the repo containing `dir`, if it is in one. */\nfunction gitOriginUrl(dir: string): string | undefined {\n\ttry {\n\t\tconst url = execFileSync(\"git\", [\"-C\", dir, \"remote\", \"get-url\", \"origin\"], {\n\t\t\tencoding: \"utf8\",\n\t\t\ttimeout: 5_000,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t}).trim();\n\t\treturn url || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction capabilityLines(plugin: NormalizedPlugin): string[] {\n\tconst lines: string[] = [];\n\tconst add = (label: string, present: unknown) => {\n\t\tif (present) lines.push(`- ${label}`);\n\t};\n\tadd(\"Skills\", plugin.skillsDir);\n\tadd(\"Slash commands\", plugin.commandsDir);\n\tadd(\"Subagents\", plugin.agentsDir);\n\tadd(\"Hooks\", plugin.hooks);\n\tadd(\"MCP servers\", plugin.mcpServers);\n\treturn lines;\n}\n\nfunction installSection(plugin: NormalizedPlugin, platform: PluginPlatform): string[] {\n\tif (platform === \"claude\") {\n\t\treturn [\n\t\t\t\"## Install\",\n\t\t\t\"\",\n\t\t\t\"```bash\",\n\t\t\t\"claude plugin marketplace add <marketplace-repo>\",\n\t\t\t`claude plugin install ${plugin.id}@<marketplace>`,\n\t\t\t\"```\",\n\t\t\t\"\",\n\t\t\t`Or copy this directory to \\`~/.claude/skills/${plugin.id}/\\` — a folder there`,\n\t\t\t\"carrying a `.claude-plugin/plugin.json` loads with no install step.\",\n\t\t];\n\t}\n\treturn [\n\t\t\"## Install\",\n\t\t\"\",\n\t\t\"```bash\",\n\t\t\"copilot plugin marketplace add <marketplace-repo>\",\n\t\t`copilot plugin install ${plugin.id}@<marketplace>`,\n\t\t\"```\",\n\t];\n}\n\n/**\n * Generate a README from what the plugin actually contains.\n *\n * Never overwrites: a hand-written README is the author's description of their\n * own plugin, and a generated one derived from the directory listing is strictly\n * worse. This only fills the gap where there is nothing at all.\n */\nfunction writeReadme(plugin: NormalizedPlugin, platform: PluginPlatform): string | undefined {\n\tconst file = path.join(plugin.root, \"README.md\");\n\tif (existsSync(file)) return undefined;\n\n\tconst capabilities = capabilityLines(plugin);\n\tconst body = [\n\t\t`# ${plugin.id}`,\n\t\t\"\",\n\t\tplugin.description ?? \"_No description provided._\",\n\t\t\"\",\n\t\t...(capabilities.length > 0 ? [\"## Provides\", \"\", ...capabilities, \"\"] : []),\n\t\t...installSection(plugin, platform),\n\t\t\"\",\n\t\t...(plugin.unsupportedSurfaces?.length\n\t\t\t? [\n\t\t\t\t\t\"## Notes\",\n\t\t\t\t\t\"\",\n\t\t\t\t\t`This plugin ships surfaces hoocode does not load: ${plugin.unsupportedSurfaces.join(\", \")}.`,\n\t\t\t\t\t\"They are preserved as-is and are the target platform's concern.\",\n\t\t\t\t\t\"\",\n\t\t\t\t]\n\t\t\t: []),\n\t].join(\"\\n\");\n\n\twriteFileSync(file, `${body.trimEnd()}\\n`, \"utf8\");\n\treturn \"README.md\";\n}\n\n/**\n * Build the marketplace index entry.\n *\n * `source` is where the *published* plugin will live, which packaging cannot\n * know: the artifact is on this machine and has not been pushed anywhere. When\n * the plugin sits in a git checkout its origin is a plausible starting point;\n * otherwise the field is a clearly-marked placeholder for the human to fill.\n * Guessing here would produce an index entry that resolves to nothing, which is\n * worse than an obvious blank.\n */\nfunction buildEntry(plugin: NormalizedPlugin): MarketplaceEntry {\n\tconst origin = gitOriginUrl(plugin.root);\n\treturn {\n\t\tname: plugin.id,\n\t\t...(plugin.description ? { description: plugin.description } : {}),\n\t\t...(plugin.version ? { version: plugin.version } : {}),\n\t\tsource: origin ? { source: \"git\", url: origin } : SOURCE_PLACEHOLDER,\n\t};\n}\n\n/** True when the entry still needs a human to say where the plugin will be hosted. */\nexport function entryNeedsSource(entry: MarketplaceEntry): boolean {\n\treturn entry.source === SOURCE_PLACEHOLDER;\n}\n\nfunction publishSteps(plugin: NormalizedPlugin, platform: PluginPlatform): string[] {\n\tif (platform === \"github\") {\n\t\treturn [\n\t\t\t\"Publish (GitHub Copilot) — this is the only route into the ecosystem,\",\n\t\t\t\"local installs being deprecated:\",\n\t\t\t\"\",\n\t\t\t\" 1. Copy this directory into your marketplace repo.\",\n\t\t\t\" 2. Add the entry above to its `.github/marketplace.json` `plugins` array.\",\n\t\t\t\" 3. Open a pull request; the plugin is installable once it merges.\",\n\t\t];\n\t}\n\treturn [\n\t\t`Publish (Claude Code) — optional: the plugin is already live locally from`,\n\t\t`\\`~/.claude/skills/${plugin.id}/\\`. To share it:`,\n\t\t\"\",\n\t\t\" 1. Copy this directory into your marketplace repo.\",\n\t\t\" 2. Add the entry above to its `.claude-plugin/marketplace.json` `plugins` array.\",\n\t\t\" 3. Open a pull request, or submit to the community directory for review\",\n\t\t\" (approved plugins are pinned to a commit SHA).\",\n\t];\n}\n\n/**\n * Package a plugin for publication: gate it at full strictness, write a README,\n * produce its marketplace entry, and record what it produced. Touches no remote.\n */\nexport async function packagePlugin(\n\tplugin: NormalizedPlugin,\n\tplatformOverride?: PluginPlatform,\n\toptions: { judge?: TriggerJudge; distractors?: readonly CapabilityDoc[] } = {},\n): Promise<PackageResult> {\n\tconst platform = platformOverride ?? resolvePackagePlatform(plugin);\n\n\t// A plugin can only be published into an ecosystem whose manifest it carries.\n\t// `claude plugin validate` says so too, but as a wall of validator output about\n\t// a missing file; a plugin in the native layout has simply been authored for\n\t// the wrong target, and that is worth saying in one sentence. Nothing is\n\t// written — there is no artifact to package.\n\tif (!plugin.supportPlatform.includes(platform)) {\n\t\tconst finding: GateFinding = {\n\t\t\tgate: \"G1\",\n\t\t\tseverity: \"error\",\n\t\t\tmessage:\n\t\t\t\t`Plugin \"${plugin.id}\" carries no ${platform} manifest (it offers: ${plugin.supportPlatform.join(\", \")}), ` +\n\t\t\t\t`so it cannot be published to that ecosystem. Re-author it with --platform ${platform}.`,\n\t\t};\n\t\tconst evaluation: GateResult = { ok: false, findings: [finding], conformance: \"round-trip\", plugin };\n\t\treturn {\n\t\t\tok: false,\n\t\t\tdir: plugin.root,\n\t\t\tplatform,\n\t\t\twritten: [],\n\t\t\tentry: buildEntry(plugin),\n\t\t\tevaluation,\n\t\t\tinstructions: `Plugin \"${plugin.id}\" cannot be packaged for ${platform}.\\n\\n${formatGateFindings(evaluation)}`,\n\t\t};\n\t}\n\n\t// Strict: warnings that are tolerable in a local plugin — an unresolvable\n\t// binary, a vendor-validator nit — are not tolerable in something other people\n\t// will install on the strength of the marketplace vouching for it.\n\tlet evaluation = runStaticGates(plugin.root, { platform, strict: true });\n\tif (evaluation.plugin) {\n\t\tevaluation = withFindings(evaluation, await runSmokeGate(evaluation.plugin), true);\n\t}\n\n\t// G4 is the publish path's gate (§4.3) and nothing else's: it costs a model\n\t// call, and the question it asks — is this description actually worth the\n\t// context it occupies — only becomes other people's problem at publication.\n\t// With no judge or no gold set it reports `not-run`, which is `info`, so a\n\t// machine without a model is never blocked from publishing by a check it\n\t// could not perform.\n\tconst own = ownCandidates(plugin);\n\tif (own.length > 0) {\n\t\tconst outcome = await runTriggerEval(\n\t\t\tplugin.id,\n\t\t\t[...own, ...distractorCandidates(plugin.id, options.distractors ?? [])],\n\t\t\tloadTriggerCases(plugin.root),\n\t\t\toptions.judge,\n\t\t);\n\t\tevaluation = withFindings(evaluation, triggerFindings(outcome), true);\n\t}\n\n\tconst written: string[] = [];\n\tconst readme = writeReadme(plugin, platform);\n\tif (readme) written.push(readme);\n\n\tconst entry = buildEntry(plugin);\n\tconst record: PublishRecord = {\n\t\tpackagedAt: new Date().toISOString(),\n\t\tplatform,\n\t\tentry,\n\t\tok: evaluation.ok,\n\t};\n\twriteFileSync(path.join(plugin.root, PUBLISH_RECORD_FILE), `${JSON.stringify(record, null, 2)}\\n`, \"utf8\");\n\twritten.push(PUBLISH_RECORD_FILE);\n\n\tconst instructions = [\n\t\tevaluation.ok\n\t\t\t? `Plugin \"${plugin.id}\" is ready to publish (${platform}). The remaining step is yours.`\n\t\t\t: `Plugin \"${plugin.id}\" is NOT ready to publish — these must pass first:`,\n\t\t\"\",\n\t\tformatGateFindings(evaluation),\n\t\t\"\",\n\t\t\"Marketplace entry:\",\n\t\t\"```json\",\n\t\tJSON.stringify(entry, null, 2),\n\t\t\"```\",\n\t\t...(entryNeedsSource(entry) ? [\"\", \"Replace `source` with where the plugin will be hosted.\"] : []),\n\t\t\"\",\n\t\t...publishSteps(plugin, platform),\n\t\t\"\",\n\t\t\"Publishing is deliberately not automated: an agent that can push executable\",\n\t\t\"code into a marketplace other agents install from unattended is a supply-chain\",\n\t\t\"risk, so the last step stays with you.\",\n\t].join(\"\\n\");\n\n\treturn { ok: evaluation.ok, dir: plugin.root, platform, written, entry, evaluation, instructions };\n}\n\n// ── Staging into a local marketplace checkout ────────────────────────────────\n\n/**\n * Files that exist for hoocode's benefit and have no business in a published\n * artifact: the provenance marker and packaging's own build record.\n */\nconst INTERNAL_FILES = new Set([PUBLISH_RECORD_FILE, \".authored.json\"]);\n\n/** Where each platform's marketplace index lives, and the subdirectory plugins are vendored into. */\nconst INDEX_FILE: Record<PluginPlatform, string> = {\n\tclaude: path.join(\".claude-plugin\", \"marketplace.json\"),\n\tgithub: path.join(\".github\", \"marketplace.json\"),\n};\nconst VENDOR_DIR = \"plugins\";\n\nexport interface StageResult {\n\tok: boolean;\n\tmessage: string;\n\t/** Path the plugin was copied to, relative to the marketplace root. */\n\tpluginPath?: string;\n\t/** The index that was written, relative to the marketplace root. */\n\tindexPath?: string;\n\t/** True when the marketplace had no index for this platform and one was created. */\n\tcreatedIndex?: boolean;\n}\n\ninterface RawIndex {\n\tname?: string;\n\tplugins?: Array<Record<string, unknown>>;\n\t[key: string]: unknown;\n}\n\n/**\n * Copy a packaged plugin into a **local** marketplace checkout and add its index\n * entry, leaving the result uncommitted.\n *\n * This is the far side of the automation line drawn in §3.3, and it is where the\n * line actually falls. Everything here is local, reversible, and shows up in\n * `git status` — the human reviews a diff and decides. What stays manual is the\n * step that leaves the machine: no commit, no push, no PR. Those are what make a\n * plugin installable by other people's agents, and that is the act install is\n * trusting a human to have performed.\n *\n * Only reachable from `/plugin publish`, never from a tool: a human typed the\n * command, which is the whole point.\n */\nexport function stageIntoMarketplace(\n\tplugin: NormalizedPlugin,\n\tplatform: PluginPlatform,\n\tmarketplaceDir: string,\n): StageResult {\n\tconst root = path.resolve(marketplaceDir);\n\tif (!existsSync(root)) {\n\t\treturn { ok: false, message: `Marketplace directory not found: ${root}` };\n\t}\n\tconst pluginRoot = path.resolve(plugin.root);\n\tif (pluginRoot === root || pluginRoot.startsWith(`${root}${path.sep}`)) {\n\t\treturn { ok: false, message: `Plugin \"${plugin.id}\" already lives inside ${root}; nothing to stage.` };\n\t}\n\n\tconst indexRel = INDEX_FILE[platform];\n\tconst indexAbs = path.join(root, indexRel);\n\tlet index: RawIndex;\n\tlet createdIndex = false;\n\tif (existsSync(indexAbs)) {\n\t\ttry {\n\t\t\tindex = JSON.parse(readFileSync(indexAbs, \"utf8\")) as RawIndex;\n\t\t} catch (error) {\n\t\t\treturn { ok: false, message: `${indexRel} is not valid JSON (${(error as Error).message}); fix it first.` };\n\t\t}\n\t} else {\n\t\t// A marketplace repo that does not have an index yet is a normal starting\n\t\t// point, and refusing here would mean the first plugin can never be staged.\n\t\tindex = { name: path.basename(root), plugins: [] };\n\t\tcreatedIndex = true;\n\t}\n\tif (!Array.isArray(index.plugins)) index.plugins = [];\n\n\tconst dest = path.join(root, VENDOR_DIR, plugin.id);\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(path.dirname(dest), { recursive: true });\n\tcpSync(pluginRoot, dest, {\n\t\trecursive: true,\n\t\tfilter: (src) => !INTERNAL_FILES.has(path.basename(src)),\n\t});\n\n\t// Vendored in the repo, so the source is a path relative to it — not the\n\t// git origin `packagePlugin` guessed, which points at wherever the plugin was\n\t// authored rather than at the marketplace.\n\tconst entry: MarketplaceEntry = { ...buildEntry(plugin), source: `./${VENDOR_DIR}/${plugin.id}` };\n\tconst existingAt = index.plugins.findIndex((p) => p?.name === plugin.id);\n\tif (existingAt >= 0) index.plugins[existingAt] = { ...index.plugins[existingAt], ...entry };\n\telse index.plugins.push(entry as unknown as Record<string, unknown>);\n\n\tmkdirSync(path.dirname(indexAbs), { recursive: true });\n\twriteFileSync(indexAbs, `${JSON.stringify(index, null, 2)}\\n`, \"utf8\");\n\n\tconst verb = existingAt >= 0 ? \"Updated\" : \"Added\";\n\treturn {\n\t\tok: true,\n\t\tmessage:\n\t\t\t`${verb} \"${plugin.id}\" in ${path.basename(root)}: copied to ${VENDOR_DIR}/${plugin.id}/ and ` +\n\t\t\t`${createdIndex ? \"created\" : \"updated\"} ${indexRel}. Nothing was committed or pushed.`,\n\t\tpluginPath: `${VENDOR_DIR}/${plugin.id}`,\n\t\tindexPath: indexRel,\n\t\tcreatedIndex,\n\t};\n}\n\n/** Read back the record packaging left, for callers that want to show it. */\nexport function readPublishRecord(dir: string): PublishRecord | undefined {\n\ttry {\n\t\tconst file = path.join(dir, PUBLISH_RECORD_FILE);\n\t\treturn existsSync(file) ? (JSON.parse(readFileSync(file, \"utf8\")) as PublishRecord) : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G3 — behavioral smoke for a plugin's executable capabilities.
|
|
3
|
+
*
|
|
4
|
+
* G1 and G2 read files. G3 is the first gate that finds out whether the thing
|
|
5
|
+
* actually runs, which is the most common real failure: a plugin that installs
|
|
6
|
+
* cleanly and breaks the session at the next tool call.
|
|
7
|
+
*
|
|
8
|
+
* **What this is and is not.** Hooks run with `cwd`, `HOME`, `TMPDIR` and the
|
|
9
|
+
* plugin data dir redirected into a throwaway directory, and with a hard
|
|
10
|
+
* timeout. That reduces blast radius; it is *not* containment. Without OS-level
|
|
11
|
+
* sandboxing a shell command can still write wherever it likes, and claiming
|
|
12
|
+
* otherwise in a confirmation prompt would be worse than saying nothing — the
|
|
13
|
+
* whole point of showing gate results to a human is that they are true.
|
|
14
|
+
*
|
|
15
|
+
* **Why it runs before the human confirms.** It does execute not-yet-approved
|
|
16
|
+
* code, which is a real cost. Against it: the code was authored in this session
|
|
17
|
+
* from the user's own request rather than fetched from anywhere, G2 has already
|
|
18
|
+
* screened it for destructive shapes, and absent the smoke test the very same
|
|
19
|
+
* command runs moments later anyway — unscreened, unredirected, and in the real
|
|
20
|
+
* working directory. Running it once under redirection to find out whether it
|
|
21
|
+
* even works is the smaller risk. G3 is therefore authored-only: it is never
|
|
22
|
+
* applied to a marketplace plugin, where the code is someone else's and
|
|
23
|
+
* executing it pre-consent would not be defensible.
|
|
24
|
+
*/
|
|
25
|
+
import type { GateFinding } from "./gates.js";
|
|
26
|
+
import type { NormalizedPlugin } from "./manifest.js";
|
|
27
|
+
export interface SmokeOptions {
|
|
28
|
+
/** Skip the whole gate (no UI to report into, or an explicit opt-out). */
|
|
29
|
+
skip?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Run G3 over a plugin's executable capabilities. Returns findings; empty means nothing to test. */
|
|
32
|
+
export declare function runSmokeGate(plugin: NormalizedPlugin, opts?: SmokeOptions): Promise<GateFinding[]>;
|
|
33
|
+
//# sourceMappingURL=smoke.d.ts.map
|