vskill 1.0.15 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -9
- package/agents.json +3 -1
- package/dist/agents/agents-registry.d.ts +69 -3
- package/dist/agents/agents-registry.js +203 -0
- package/dist/agents/agents-registry.js.map +1 -1
- package/dist/api/client.d.ts +85 -0
- package/dist/api/client.js +193 -24
- package/dist/api/client.js.map +1 -1
- package/dist/commands/add-lockfile.d.ts +6 -0
- package/dist/commands/add-lockfile.js +10 -0
- package/dist/commands/add-lockfile.js.map +1 -1
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +110 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/auth.d.ts +23 -0
- package/dist/commands/auth.js +105 -11
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/eval/serve.d.ts +2 -0
- package/dist/commands/eval/serve.js +126 -4
- package/dist/commands/eval/serve.js.map +1 -1
- package/dist/commands/orgs.d.ts +21 -0
- package/dist/commands/orgs.js +164 -0
- package/dist/commands/orgs.js.map +1 -0
- package/dist/commands/skill.js +14 -1
- package/dist/commands/skill.js.map +1 -1
- package/dist/commands/whoami.d.ts +29 -0
- package/dist/commands/whoami.js +119 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/discovery/github-tree.d.ts +23 -3
- package/dist/discovery/github-tree.js +172 -24
- package/dist/discovery/github-tree.js.map +1 -1
- package/dist/eval/anthropic-catalog.js +32 -2
- package/dist/eval/anthropic-catalog.js.map +1 -1
- package/dist/eval/batch-judge.js +1 -0
- package/dist/eval/batch-judge.js.map +1 -1
- package/dist/eval/llm.d.ts +1 -1
- package/dist/eval/llm.js +104 -2
- package/dist/eval/llm.js.map +1 -1
- package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.d.ts +2 -0
- package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.js +20 -0
- package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.js.map +1 -0
- package/dist/eval-server/active-tenant-routes.d.ts +15 -0
- package/dist/eval-server/active-tenant-routes.js +101 -0
- package/dist/eval-server/active-tenant-routes.js.map +1 -0
- package/dist/eval-server/api-routes.js +206 -6
- package/dist/eval-server/api-routes.js.map +1 -1
- package/dist/eval-server/desktop-open-routes.d.ts +8 -0
- package/dist/eval-server/desktop-open-routes.js +64 -0
- package/dist/eval-server/desktop-open-routes.js.map +1 -0
- package/dist/eval-server/eval-server.js +90 -6
- package/dist/eval-server/eval-server.js.map +1 -1
- package/dist/eval-server/export-skill-routes.d.ts +9 -0
- package/dist/eval-server/export-skill-routes.js +81 -0
- package/dist/eval-server/export-skill-routes.js.map +1 -0
- package/dist/eval-server/git-routes.d.ts +1 -0
- package/dist/eval-server/git-routes.js +101 -4
- package/dist/eval-server/git-routes.js.map +1 -1
- package/dist/eval-server/install-engine-routes.d.ts +3 -16
- package/dist/eval-server/install-engine-routes.js +9 -124
- package/dist/eval-server/install-engine-routes.js.map +1 -1
- package/dist/eval-server/install-jobs.d.ts +41 -0
- package/dist/eval-server/install-jobs.js +161 -0
- package/dist/eval-server/install-jobs.js.map +1 -0
- package/dist/eval-server/install-skill-routes.d.ts +74 -11
- package/dist/eval-server/install-skill-routes.js +506 -79
- package/dist/eval-server/install-skill-routes.js.map +1 -1
- package/dist/eval-server/install-state-routes.d.ts +25 -0
- package/dist/eval-server/install-state-routes.js +125 -0
- package/dist/eval-server/install-state-routes.js.map +1 -0
- package/dist/eval-server/oauth-github-routes.d.ts +2 -0
- package/dist/eval-server/oauth-github-routes.js +505 -0
- package/dist/eval-server/oauth-github-routes.js.map +1 -0
- package/dist/eval-server/platform-proxy.d.ts +17 -1
- package/dist/eval-server/platform-proxy.js +125 -13
- package/dist/eval-server/platform-proxy.js.map +1 -1
- package/dist/eval-server/plugin-cli-routes.js +9 -9
- package/dist/eval-server/plugin-cli-routes.js.map +1 -1
- package/dist/eval-server/remove-skill-routes.d.ts +18 -0
- package/dist/eval-server/remove-skill-routes.js +145 -0
- package/dist/eval-server/remove-skill-routes.js.map +1 -0
- package/dist/eval-server/router.d.ts +17 -3
- package/dist/eval-server/router.js +166 -9
- package/dist/eval-server/router.js.map +1 -1
- package/dist/eval-server/settings-store.js +1 -1
- package/dist/eval-server/settings-store.js.map +1 -1
- package/dist/eval-server/supported-agents-routes.d.ts +6 -0
- package/dist/eval-server/supported-agents-routes.js +41 -0
- package/dist/eval-server/supported-agents-routes.js.map +1 -0
- package/dist/eval-server/utils/spawn-env.d.ts +1 -0
- package/dist/eval-server/utils/spawn-env.js +47 -0
- package/dist/eval-server/utils/spawn-env.js.map +1 -0
- package/dist/eval-ui/assets/AdvancedTab-D8zbE5fH.js +1 -0
- package/dist/eval-ui/assets/{CreateSkillPage-BmbvQEzE.js → CreateSkillPage-DOBhKdgr.js} +5 -5
- package/dist/eval-ui/assets/FindSkillsPalette-CyMmNPr-.js +2 -0
- package/dist/eval-ui/assets/GeneralTab-DYR9NWC4.js +1 -0
- package/dist/eval-ui/assets/PrivacyTab-CXIqQokl.js +1 -0
- package/dist/eval-ui/assets/SearchPaletteCore-Dn5gQJS_.js +14 -0
- package/dist/eval-ui/assets/SkillDetailPanel-DTrRnyyJ.js +1 -0
- package/dist/eval-ui/assets/UpdateDropdown-Cvr2fe0z.js +1 -0
- package/dist/eval-ui/assets/UpdatesTab-DwJIUDPX.js +1 -0
- package/dist/eval-ui/assets/core-DZAvsxlC.js +1 -0
- package/dist/eval-ui/assets/event-CDYWU2X3.js +1 -0
- package/dist/eval-ui/assets/globals-BRZwPAPF.js +49 -0
- package/dist/eval-ui/assets/globals-C3oEdsJh.css +1 -0
- package/dist/eval-ui/assets/index-D7M0Jdss.js +1 -0
- package/dist/eval-ui/assets/lifecycle-DSleOV-l.js +1 -0
- package/dist/eval-ui/assets/lifecycle-d1Sm9Hts.css +1 -0
- package/dist/eval-ui/assets/main-D2shn1dH.js +87 -0
- package/dist/eval-ui/assets/preferences-BHZXB5dL.css +1 -0
- package/dist/eval-ui/assets/preferences-BKv6X7fK.js +2 -0
- package/dist/eval-ui/assets/useDesktopBridge-DxVWbYqK.js +2 -0
- package/dist/eval-ui/index.html +4 -2
- package/dist/eval-ui/lifecycle.html +33 -0
- package/dist/eval-ui/preferences.html +34 -0
- package/dist/index.js +47 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/bundle-files.d.ts +4 -0
- package/dist/installer/bundle-files.js +97 -0
- package/dist/installer/bundle-files.js.map +1 -0
- package/dist/installer/canonical.d.ts +31 -6
- package/dist/installer/canonical.js +50 -23
- package/dist/installer/canonical.js.map +1 -1
- package/dist/installer/clipboard-export.d.ts +19 -0
- package/dist/installer/clipboard-export.js +88 -0
- package/dist/installer/clipboard-export.js.map +1 -0
- package/dist/installer/frontmatter.js +1 -1
- package/dist/installer/frontmatter.js.map +1 -1
- package/dist/installer/multi-install.d.ts +43 -0
- package/dist/installer/multi-install.js +237 -0
- package/dist/installer/multi-install.js.map +1 -0
- package/dist/installer/transformers/aider.d.ts +2 -0
- package/dist/installer/transformers/aider.js +32 -0
- package/dist/installer/transformers/aider.js.map +1 -0
- package/dist/installer/transformers/continue-dev.d.ts +2 -0
- package/dist/installer/transformers/continue-dev.js +6 -0
- package/dist/installer/transformers/continue-dev.js.map +1 -0
- package/dist/installer/transformers/cursor.d.ts +2 -0
- package/dist/installer/transformers/cursor.js +24 -0
- package/dist/installer/transformers/cursor.js.map +1 -0
- package/dist/installer/transformers/github-copilot.d.ts +2 -0
- package/dist/installer/transformers/github-copilot.js +17 -0
- package/dist/installer/transformers/github-copilot.js.map +1 -0
- package/dist/installer/transformers/index.d.ts +78 -0
- package/dist/installer/transformers/index.js +13 -0
- package/dist/installer/transformers/index.js.map +1 -0
- package/dist/installer/transformers/junie.d.ts +2 -0
- package/dist/installer/transformers/junie.js +6 -0
- package/dist/installer/transformers/junie.js.map +1 -0
- package/dist/installer/transformers/kiro.d.ts +2 -0
- package/dist/installer/transformers/kiro.js +6 -0
- package/dist/installer/transformers/kiro.js.map +1 -0
- package/dist/installer/transformers/trae.d.ts +2 -0
- package/dist/installer/transformers/trae.js +6 -0
- package/dist/installer/transformers/trae.js.map +1 -0
- package/dist/installer/transformers/windsurf.d.ts +2 -0
- package/dist/installer/transformers/windsurf.js +12 -0
- package/dist/installer/transformers/windsurf.js.map +1 -0
- package/dist/installer/yaml-safe-mutate.d.ts +19 -0
- package/dist/installer/yaml-safe-mutate.js +184 -0
- package/dist/installer/yaml-safe-mutate.js.map +1 -0
- package/dist/lib/active-tenant.d.ts +36 -0
- package/dist/lib/active-tenant.js +120 -0
- package/dist/lib/active-tenant.js.map +1 -0
- package/dist/lib/github-fetch.d.ts +1 -0
- package/dist/lib/github-fetch.js +11 -1
- package/dist/lib/github-fetch.js.map +1 -1
- package/dist/lib/keychain.d.ts +15 -2
- package/dist/lib/keychain.js +156 -8
- package/dist/lib/keychain.js.map +1 -1
- package/dist/lib/migration/keychain-migration.d.ts +35 -0
- package/dist/lib/migration/keychain-migration.js +189 -0
- package/dist/lib/migration/keychain-migration.js.map +1 -0
- package/dist/lib/tenant-resolver.d.ts +38 -0
- package/dist/lib/tenant-resolver.js +79 -0
- package/dist/lib/tenant-resolver.js.map +1 -0
- package/dist/lockfile/types.d.ts +8 -0
- package/dist/sidecar/eval-ui-manifest.json +1 -0
- package/dist/sidecar/sea-config.json +57 -0
- package/dist/sidecar/sea-prep.blob +0 -0
- package/dist/sidecar/server.cjs +141627 -0
- package/dist/sidecar/vskill-version.txt +1 -0
- package/dist/studio/lib/ops-log.js +140 -57
- package/dist/studio/lib/ops-log.js.map +1 -1
- package/dist/studio/lib/provenance.js +3 -2
- package/dist/studio/lib/provenance.js.map +1 -1
- package/dist/studio/lib/query.d.ts +1 -0
- package/dist/studio/lib/query.js +7 -0
- package/dist/studio/lib/query.js.map +1 -0
- package/dist/studio/lib/scope-transfer.d.ts +16 -0
- package/dist/studio/lib/scope-transfer.js +52 -25
- package/dist/studio/lib/scope-transfer.js.map +1 -1
- package/dist/studio/routes/index.js +13 -1
- package/dist/studio/routes/index.js.map +1 -1
- package/dist/studio/routes/ops.js +31 -9
- package/dist/studio/routes/ops.js.map +1 -1
- package/dist/studio/routes/promote.js +16 -11
- package/dist/studio/routes/promote.js.map +1 -1
- package/dist/studio/routes/revert.js +14 -18
- package/dist/studio/routes/revert.js.map +1 -1
- package/dist/studio/routes/test-install.js +14 -11
- package/dist/studio/routes/test-install.js.map +1 -1
- package/dist/studio-runtime/lockfile.d.ts +51 -0
- package/dist/studio-runtime/lockfile.js +216 -0
- package/dist/studio-runtime/lockfile.js.map +1 -0
- package/dist/updater/source-fetcher.js +2 -2
- package/dist/updater/source-fetcher.js.map +1 -1
- package/dist/utils/skill-builder-detection.d.ts +14 -1
- package/dist/utils/skill-builder-detection.js +20 -8
- package/dist/utils/skill-builder-detection.js.map +1 -1
- package/dist/utils/skill-creator-detection.d.ts +10 -2
- package/dist/utils/skill-creator-detection.js +12 -43
- package/dist/utils/skill-creator-detection.js.map +1 -1
- package/package.json +17 -1
- package/dist/eval-ui/assets/FindSkillsPalette-D0Zjhm31.js +0 -2
- package/dist/eval-ui/assets/SearchPaletteCore-EhcN1xEa.js +0 -14
- package/dist/eval-ui/assets/SkillDetailPanel-B5J60ffv.js +0 -1
- package/dist/eval-ui/assets/UpdateDropdown-Celf0_Cr.js +0 -1
- package/dist/eval-ui/assets/index-BV7k6fdk.js +0 -124
- package/dist/eval-ui/assets/index-CKLqBL52.css +0 -1
- package/dist/eval-ui/assets/skill-studio-logo-CRyKgIrg.png +0 -0
|
@@ -12,106 +12,447 @@
|
|
|
12
12
|
// 3. Scope allowlist — "project" | "user" | "global" only.
|
|
13
13
|
// 4. Hard-coded command name "vskill" — no path injection.
|
|
14
14
|
//
|
|
15
|
+
// 0845 T-017: extended to accept agentIds[] for in-process multi-install
|
|
16
|
+
// dispatch. Backward-compatible: legacy `agent: string` or no agent field
|
|
17
|
+
// still goes through the CLI-spawn single-agent path (AC-US2-09).
|
|
18
|
+
//
|
|
19
|
+
// 0845 closure fix: server-side fallback resolves `parsedSkill` from the
|
|
20
|
+
// local skill registry (project / personal / plugin cache) when callers
|
|
21
|
+
// omit it on the multi-agent path. Keeps the API surface minimal — the
|
|
22
|
+
// frontend just sends `{ skill, agentIds, scope }`.
|
|
23
|
+
//
|
|
15
24
|
// Endpoints:
|
|
16
|
-
// POST /api/studio/install-skill { skill, scope } → 202 + { jobId }
|
|
25
|
+
// POST /api/studio/install-skill { skill, scope, agentIds?, parsedSkill? } → 202 + { jobId }
|
|
17
26
|
// GET /api/studio/install-skill/:id/stream SSE progress | done
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import { promises as fsPromises } from "node:fs";
|
|
29
|
+
import { join as pathJoin } from "node:path";
|
|
30
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20
31
|
import { sendJson, readBody } from "./router.js";
|
|
32
|
+
import { isLocalhost, mountSpawnStreamRoute, startSpawnJob, } from "./install-jobs.js";
|
|
21
33
|
import { initSSE, sendSSE, sendSSEDone } from "./sse-helpers.js";
|
|
34
|
+
import { installSkillToMultipleAgents, } from "../installer/multi-install.js";
|
|
35
|
+
import { getAgent } from "../agents/agents-registry.js";
|
|
36
|
+
import { locateSkill } from "../clone/skill-locator.js";
|
|
37
|
+
import { extractDescription } from "../installer/frontmatter.js";
|
|
38
|
+
import { collectSkillBundleFiles, sanitizeSkillBundleFiles, } from "../installer/bundle-files.js";
|
|
39
|
+
import { getDefaultKeychain } from "../lib/keychain.js";
|
|
40
|
+
import { ensureLockfile, writeLockfile } from "../lockfile/lockfile.js";
|
|
22
41
|
const SAFE_NAME = /^[a-zA-Z0-9._@/\-]+$/;
|
|
42
|
+
// Agent IDs are slug-shaped: lowercase alphanumeric + hyphens. Stricter
|
|
43
|
+
// than SAFE_NAME to reject `.`, `/`, `@`, and uppercase at the boundary
|
|
44
|
+
// (FR-007 — every entry in agentIds[] must pass this regex).
|
|
45
|
+
const SAFE_AGENT_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
23
46
|
const VALID_SCOPES = new Set(["project", "user", "global"]);
|
|
24
47
|
const INSTALL_TIMEOUT_MS = 180_000; // 3 min — installs can be slow on cold caches.
|
|
25
|
-
const STDOUT_BUFFER_CAP = 1024 * 1024;
|
|
26
48
|
const JOBS = new Map();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
const MULTI_JOBS = new Map();
|
|
50
|
+
function buildArgs(skill, scope) {
|
|
51
|
+
// No shell — every entry is a discrete argv element. Scope is from a
|
|
52
|
+
// closed set; skill is regex-validated. Both flags map to vskill CLI.
|
|
53
|
+
if (scope === "global" || scope === "user")
|
|
54
|
+
return ["install", skill, "--global"];
|
|
55
|
+
return ["install", skill, "--scope", scope];
|
|
56
|
+
}
|
|
57
|
+
function normalizeInstallScope(scope) {
|
|
58
|
+
return scope === "project" ? "project" : "user";
|
|
30
59
|
}
|
|
31
|
-
function
|
|
32
|
-
return
|
|
60
|
+
function userLockDir() {
|
|
61
|
+
return pathJoin(os.homedir(), ".agents");
|
|
33
62
|
}
|
|
34
|
-
function
|
|
63
|
+
function emitMultiJob(job, event, data) {
|
|
35
64
|
job.pastEvents.push({ event, data });
|
|
36
65
|
for (const sub of job.subscribers) {
|
|
37
66
|
try {
|
|
38
67
|
sub(event, data);
|
|
39
68
|
}
|
|
40
|
-
catch { /* subscriber
|
|
69
|
+
catch { /* subscriber dead — ignore */ }
|
|
41
70
|
}
|
|
42
71
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
function isValidParsedSkill(s) {
|
|
73
|
+
if (!s || typeof s !== "object")
|
|
74
|
+
return false;
|
|
75
|
+
const v = s;
|
|
76
|
+
const requiredFieldsOk = typeof v.name === "string" &&
|
|
77
|
+
typeof v.description === "string" &&
|
|
78
|
+
typeof v.body === "string";
|
|
79
|
+
if (!requiredFieldsOk)
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
sanitizeSkillBundleFiles(v.files);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
const SERVER_FALLBACK_FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
90
|
+
const SERVER_FALLBACK_NAME_RE = /^name:\s*(.+?)\s*$/m;
|
|
91
|
+
const SERVER_FALLBACK_VERSION_RE = /^version:\s*(.+?)\s*$/m;
|
|
92
|
+
const DEFAULT_PLATFORM_URL = "https://verified-skill.com";
|
|
93
|
+
function unquoteYaml(value) {
|
|
94
|
+
const trimmed = value.trim();
|
|
95
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
96
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
97
|
+
return trimmed.slice(1, -1);
|
|
98
|
+
}
|
|
99
|
+
return trimmed;
|
|
100
|
+
}
|
|
101
|
+
function parseRawSkillMd(raw, fallbackName, fallbackDescription) {
|
|
102
|
+
const normalized = raw.replace(/^/, "").replace(/\r\n/g, "\n");
|
|
103
|
+
const fmMatch = normalized.match(SERVER_FALLBACK_FRONTMATTER_RE);
|
|
104
|
+
let originalFrontmatter = "";
|
|
105
|
+
let body = normalized;
|
|
106
|
+
let nameFromFm;
|
|
107
|
+
let version;
|
|
108
|
+
let descriptionFromFm;
|
|
109
|
+
if (fmMatch) {
|
|
110
|
+
originalFrontmatter = fmMatch[1];
|
|
111
|
+
body = fmMatch[2];
|
|
112
|
+
const nameMatch = originalFrontmatter.match(SERVER_FALLBACK_NAME_RE);
|
|
113
|
+
if (nameMatch)
|
|
114
|
+
nameFromFm = unquoteYaml(nameMatch[1]);
|
|
115
|
+
const versionMatch = originalFrontmatter.match(SERVER_FALLBACK_VERSION_RE);
|
|
116
|
+
if (versionMatch)
|
|
117
|
+
version = unquoteYaml(versionMatch[1]);
|
|
118
|
+
const descMatch = originalFrontmatter.match(/^description:\s*(.+?)\s*$/m);
|
|
119
|
+
if (descMatch)
|
|
120
|
+
descriptionFromFm = unquoteYaml(descMatch[1]);
|
|
121
|
+
}
|
|
122
|
+
const name = nameFromFm || fallbackName;
|
|
123
|
+
return {
|
|
124
|
+
name,
|
|
125
|
+
description: descriptionFromFm || fallbackDescription || extractDescription(body, name),
|
|
126
|
+
body,
|
|
127
|
+
originalFrontmatter,
|
|
128
|
+
version,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function stripIdentifierVersion(identifier) {
|
|
132
|
+
const slash = identifier.lastIndexOf("/");
|
|
133
|
+
const at = identifier.lastIndexOf("@");
|
|
134
|
+
return at > slash ? identifier.slice(0, at) : identifier;
|
|
135
|
+
}
|
|
136
|
+
function platformApiPath(identifier) {
|
|
137
|
+
const base = stripIdentifierVersion(identifier);
|
|
138
|
+
const parts = base.split("/").filter(Boolean);
|
|
139
|
+
if (parts.length !== 3)
|
|
140
|
+
return null;
|
|
141
|
+
return `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}`;
|
|
142
|
+
}
|
|
143
|
+
function platformBaseUrl(opts) {
|
|
144
|
+
const raw = opts?.platformBaseUrl || process.env.VSKILL_PLATFORM_URL || DEFAULT_PLATFORM_URL;
|
|
145
|
+
return raw.replace(/\/$/, "");
|
|
146
|
+
}
|
|
147
|
+
function normalizePlatformSkill(body) {
|
|
148
|
+
if (!body || typeof body !== "object")
|
|
149
|
+
return null;
|
|
150
|
+
const record = body;
|
|
151
|
+
const candidate = record.skill && typeof record.skill === "object"
|
|
152
|
+
? record.skill
|
|
153
|
+
: record;
|
|
154
|
+
return {
|
|
155
|
+
name: typeof candidate.name === "string" ? candidate.name : undefined,
|
|
156
|
+
displayName: typeof candidate.displayName === "string" ? candidate.displayName : undefined,
|
|
157
|
+
description: typeof candidate.description === "string" ? candidate.description : undefined,
|
|
158
|
+
repoUrl: typeof candidate.repoUrl === "string" ? candidate.repoUrl : undefined,
|
|
159
|
+
skillPath: typeof candidate.skillPath === "string" ? candidate.skillPath : undefined,
|
|
160
|
+
ownerSlug: typeof candidate.ownerSlug === "string" ? candidate.ownerSlug : undefined,
|
|
161
|
+
repoSlug: typeof candidate.repoSlug === "string" ? candidate.repoSlug : undefined,
|
|
162
|
+
skillSlug: typeof candidate.skillSlug === "string" ? candidate.skillSlug : undefined,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function normalizePlatformVersions(body) {
|
|
166
|
+
if (!body || typeof body !== "object")
|
|
167
|
+
return [];
|
|
168
|
+
const versions = body.versions;
|
|
169
|
+
if (!Array.isArray(versions))
|
|
170
|
+
return [];
|
|
171
|
+
return versions
|
|
172
|
+
.filter((v) => Boolean(v) && typeof v === "object")
|
|
173
|
+
.map((v) => ({
|
|
174
|
+
version: typeof v.version === "string" ? v.version : undefined,
|
|
175
|
+
gitSha: typeof v.gitSha === "string" ? v.gitSha : null,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
function parseGitHubRepo(repoUrl) {
|
|
179
|
+
if (!repoUrl)
|
|
180
|
+
return null;
|
|
181
|
+
try {
|
|
182
|
+
const url = new URL(repoUrl);
|
|
183
|
+
if (url.hostname !== "github.com")
|
|
184
|
+
return null;
|
|
185
|
+
const [owner, repoRaw] = url.pathname.replace(/^\/+/, "").split("/");
|
|
186
|
+
const repo = repoRaw?.replace(/\.git$/, "");
|
|
187
|
+
if (!owner || !repo)
|
|
188
|
+
return null;
|
|
189
|
+
return { owner, repo };
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function isSafeSkillPath(skillPath) {
|
|
196
|
+
if (!skillPath)
|
|
197
|
+
return false;
|
|
198
|
+
if (skillPath.startsWith("/") || skillPath.includes("\\"))
|
|
199
|
+
return false;
|
|
200
|
+
const parts = skillPath.split("/");
|
|
201
|
+
if (parts.some((p) => !p || p === "." || p === ".."))
|
|
202
|
+
return false;
|
|
203
|
+
return parts[parts.length - 1] === "SKILL.md";
|
|
204
|
+
}
|
|
205
|
+
function readGitHubToken(opts) {
|
|
206
|
+
if (opts?.githubTokenProvider)
|
|
207
|
+
return opts.githubTokenProvider();
|
|
208
|
+
try {
|
|
209
|
+
return getDefaultKeychain().getGitHubToken();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function fetchSkillMdFromGitHub(opts) {
|
|
216
|
+
const repo = parseGitHubRepo(opts.repoUrl);
|
|
217
|
+
if (!repo || !isSafeSkillPath(opts.skillPath))
|
|
218
|
+
return null;
|
|
219
|
+
const pathPart = opts.skillPath.split("/").map(encodeURIComponent).join("/");
|
|
220
|
+
const refs = Array.from(new Set(opts.refs.filter(Boolean)));
|
|
221
|
+
if (!refs.includes(""))
|
|
222
|
+
refs.push("");
|
|
223
|
+
for (const ref of refs) {
|
|
224
|
+
const url = new URL(`https://api.github.com/repos/${repo.owner}/${repo.repo}/contents/${pathPart}`);
|
|
225
|
+
if (ref)
|
|
226
|
+
url.searchParams.set("ref", ref);
|
|
227
|
+
const headers = {
|
|
228
|
+
Accept: "application/vnd.github.raw",
|
|
229
|
+
"User-Agent": "vskill-skill-studio",
|
|
230
|
+
};
|
|
231
|
+
if (opts.token)
|
|
232
|
+
headers.Authorization = `Bearer ${opts.token}`;
|
|
233
|
+
try {
|
|
234
|
+
const res = await opts.fetchImpl(url.toString(), { headers });
|
|
235
|
+
if (res.ok)
|
|
236
|
+
return await res.text();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Try the next ref.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
async function resolveParsedSkillFromPlatform(identifier, opts) {
|
|
245
|
+
const apiPath = platformApiPath(identifier);
|
|
246
|
+
if (!apiPath)
|
|
247
|
+
return null;
|
|
248
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
249
|
+
const baseUrl = platformBaseUrl(opts);
|
|
250
|
+
let skill = null;
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetchImpl(`${baseUrl}${apiPath}`, { headers: { Accept: "application/json" } });
|
|
253
|
+
if (!res.ok)
|
|
254
|
+
return null;
|
|
255
|
+
skill = normalizePlatformSkill(await res.json());
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (!skill?.repoUrl || !isSafeSkillPath(skill.skillPath))
|
|
261
|
+
return null;
|
|
262
|
+
let versions = [];
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetchImpl(`${baseUrl}${apiPath}/versions`, { headers: { Accept: "application/json" } });
|
|
265
|
+
if (res.ok)
|
|
266
|
+
versions = normalizePlatformVersions(await res.json());
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
versions = [];
|
|
270
|
+
}
|
|
271
|
+
const requestedVersion = (() => {
|
|
272
|
+
const slash = identifier.lastIndexOf("/");
|
|
273
|
+
const at = identifier.lastIndexOf("@");
|
|
274
|
+
return at > slash ? identifier.slice(at + 1) : null;
|
|
275
|
+
})();
|
|
276
|
+
const selectedVersion = requestedVersion
|
|
277
|
+
? versions.find((v) => v.version === requestedVersion)
|
|
278
|
+
: versions[0];
|
|
279
|
+
const refs = [
|
|
280
|
+
selectedVersion?.gitSha ?? "",
|
|
281
|
+
"main",
|
|
282
|
+
"master",
|
|
283
|
+
];
|
|
284
|
+
const raw = await fetchSkillMdFromGitHub({
|
|
285
|
+
repoUrl: skill.repoUrl,
|
|
286
|
+
skillPath: skill.skillPath,
|
|
287
|
+
refs,
|
|
288
|
+
fetchImpl,
|
|
289
|
+
token: readGitHubToken(opts),
|
|
290
|
+
});
|
|
291
|
+
if (!raw)
|
|
292
|
+
return null;
|
|
293
|
+
return parseRawSkillMd(raw, skill.skillSlug || skill.displayName || stripIdentifierVersion(identifier).split("/").pop() || "skill", skill.description);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Server-side fallback: when the frontend posts a multi-agent install
|
|
297
|
+
* without `parsedSkill`, locate the skill on disk (project / personal /
|
|
298
|
+
* plugin cache via {@link locateSkill}), read its SKILL.md, and build a
|
|
299
|
+
* minimal {@link ParsedSkill}. Keeps the POST payload trivial — the
|
|
300
|
+
* frontend doesn't need to fetch, parse, and re-marshal SKILL.md itself.
|
|
301
|
+
*
|
|
302
|
+
* Returns `null` when no on-disk skill matches the identifier (the route
|
|
303
|
+
* surfaces this as a 404 so the caller can retry with an explicit
|
|
304
|
+
* `parsedSkill`).
|
|
305
|
+
*/
|
|
306
|
+
export async function resolveParsedSkillFromIdentifier(identifier, opts) {
|
|
307
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
308
|
+
const home = opts?.home ?? os.homedir();
|
|
309
|
+
const matches = await locateSkill(identifier, { cwd, home });
|
|
310
|
+
if (matches.length > 0) {
|
|
311
|
+
const source = matches[0];
|
|
312
|
+
const skillMdPath = pathJoin(source.skillDir, "SKILL.md");
|
|
313
|
+
try {
|
|
314
|
+
const raw = await fsPromises.readFile(skillMdPath, "utf-8");
|
|
315
|
+
const parsed = parseRawSkillMd(raw, source.skillName);
|
|
316
|
+
const files = await collectSkillBundleFiles(source.skillDir);
|
|
317
|
+
return { ...parsed, files };
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return resolveParsedSkillFromPlatform(identifier, opts);
|
|
324
|
+
}
|
|
325
|
+
function validateAgentIds(ids) {
|
|
326
|
+
if (!Array.isArray(ids))
|
|
327
|
+
return { ok: false, error: "agentIds must be an array" };
|
|
328
|
+
if (ids.length === 0)
|
|
329
|
+
return { ok: false, error: "agentIds[] cannot be empty" };
|
|
330
|
+
const validated = [];
|
|
331
|
+
for (const raw of ids) {
|
|
332
|
+
if (typeof raw !== "string")
|
|
333
|
+
return { ok: false, error: "agentIds[] entries must be strings" };
|
|
334
|
+
const id = raw.trim();
|
|
335
|
+
if (!SAFE_AGENT_ID.test(id))
|
|
336
|
+
return { ok: false, error: `invalid agentId: ${raw}` };
|
|
337
|
+
if (!getAgent(id))
|
|
338
|
+
return { ok: false, error: `unknown agentId: ${id}` };
|
|
339
|
+
validated.push(id);
|
|
340
|
+
}
|
|
341
|
+
return { ok: true, ids: validated };
|
|
342
|
+
}
|
|
343
|
+
function reconstructedSkillMd(skill) {
|
|
344
|
+
const frontmatter = skill.originalFrontmatter.trim()
|
|
345
|
+
? skill.originalFrontmatter.trim()
|
|
346
|
+
: [
|
|
347
|
+
`name: ${skill.name}`,
|
|
348
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
349
|
+
skill.version ? `version: ${skill.version}` : null,
|
|
350
|
+
].filter(Boolean).join("\n");
|
|
351
|
+
return `---\n${frontmatter}\n---\n\n${skill.body.replace(/^\n+/, "")}`;
|
|
352
|
+
}
|
|
353
|
+
function computeSha(content) {
|
|
354
|
+
return createHash("sha256").update(content).digest("hex");
|
|
355
|
+
}
|
|
356
|
+
function sourceFromIdentifier(identifier) {
|
|
357
|
+
const parts = stripIdentifierVersion(identifier).split("/").filter(Boolean);
|
|
358
|
+
if (parts.length >= 2) {
|
|
359
|
+
const [owner, repo, slug] = parts;
|
|
360
|
+
return {
|
|
361
|
+
source: slug
|
|
362
|
+
? `marketplace:${owner}/${repo}#${slug}`
|
|
363
|
+
: `github:${owner}/${repo}`,
|
|
364
|
+
sourceRepoUrl: `https://github.com/${owner}/${repo}`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
source: `registry:${stripIdentifierVersion(identifier)}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function writeMultiInstallLockfile(opts) {
|
|
372
|
+
const installedAgentIds = opts.results
|
|
373
|
+
.filter((result) => result.status === "installed")
|
|
374
|
+
.map((result) => result.agentId);
|
|
375
|
+
if (installedAgentIds.length === 0)
|
|
376
|
+
return;
|
|
377
|
+
const content = reconstructedSkillMd(opts.skill);
|
|
378
|
+
const lockDir = opts.scope === "user" ? userLockDir() : opts.projectRoot;
|
|
379
|
+
const lock = ensureLockfile(lockDir);
|
|
380
|
+
const files = ["SKILL.md", ...Object.keys(opts.skill.files ?? {})].sort();
|
|
381
|
+
lock.skills[opts.skill.name] = {
|
|
382
|
+
version: opts.skill.version || "1.0.0",
|
|
383
|
+
sha: computeSha(content),
|
|
384
|
+
tier: "VERIFIED",
|
|
385
|
+
installedAt: new Date().toISOString(),
|
|
386
|
+
scope: opts.scope,
|
|
387
|
+
files,
|
|
388
|
+
...sourceFromIdentifier(opts.identifier),
|
|
389
|
+
};
|
|
390
|
+
lock.agents = [...new Set([...(lock.agents || []), ...installedAgentIds])];
|
|
391
|
+
writeLockfile(lock, lockDir);
|
|
49
392
|
}
|
|
50
|
-
function
|
|
51
|
-
const args = buildArgs(skill, scope);
|
|
52
|
-
const proc = spawn("vskill", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
393
|
+
async function runMultiInstallJob(opts) {
|
|
53
394
|
const job = {
|
|
54
395
|
id: randomUUID(),
|
|
55
|
-
skill,
|
|
56
|
-
scope,
|
|
57
|
-
proc,
|
|
58
|
-
stdoutBuffer: "",
|
|
59
|
-
stderrBuffer: "",
|
|
60
|
-
exitCode: null,
|
|
61
396
|
done: false,
|
|
62
397
|
subscribers: new Set(),
|
|
63
398
|
pastEvents: [],
|
|
64
399
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
400
|
+
MULTI_JOBS.set(job.id, job);
|
|
401
|
+
// 0850 — terminal trace so users have visible evidence the install ran.
|
|
402
|
+
console.log("[install] start", JSON.stringify({
|
|
403
|
+
skill: opts.identifier,
|
|
404
|
+
scope: opts.scope,
|
|
405
|
+
agentIds: opts.agentIds,
|
|
406
|
+
projectRoot: opts.projectRoot,
|
|
407
|
+
}));
|
|
408
|
+
// Run install async — caller returns the jobId synchronously, then
|
|
409
|
+
// streams progress + final done event as agents complete.
|
|
410
|
+
(async () => {
|
|
411
|
+
try {
|
|
412
|
+
const result = await installSkillToMultipleAgents({
|
|
413
|
+
skill: opts.skill,
|
|
414
|
+
agentIds: opts.agentIds,
|
|
415
|
+
scope: opts.scope,
|
|
416
|
+
projectRoot: opts.projectRoot,
|
|
417
|
+
});
|
|
418
|
+
for (const agentResult of result.agents) {
|
|
419
|
+
console.log("[install] result", JSON.stringify({
|
|
420
|
+
skill: opts.identifier,
|
|
421
|
+
agentId: agentResult.agentId,
|
|
422
|
+
status: agentResult.status,
|
|
423
|
+
path: agentResult.detail,
|
|
424
|
+
}));
|
|
425
|
+
emitMultiJob(job, "result", agentResult);
|
|
426
|
+
}
|
|
427
|
+
writeMultiInstallLockfile({
|
|
428
|
+
identifier: opts.identifier,
|
|
429
|
+
skill: opts.skill,
|
|
430
|
+
scope: opts.scope,
|
|
431
|
+
projectRoot: opts.projectRoot,
|
|
432
|
+
results: result.agents,
|
|
433
|
+
});
|
|
434
|
+
emitMultiJob(job, "done", {
|
|
435
|
+
success: result.errorCount === 0,
|
|
436
|
+
results: result.agents,
|
|
437
|
+
installedCount: result.installedCount,
|
|
438
|
+
exportedCount: result.exportedCount,
|
|
439
|
+
errorCount: result.errorCount,
|
|
440
|
+
});
|
|
71
441
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
job.stderrBuffer = trimRing(job.stderrBuffer + text, STDOUT_BUFFER_CAP);
|
|
76
|
-
for (const line of text.split(/\r?\n/)) {
|
|
77
|
-
if (line.length > 0)
|
|
78
|
-
emitToJob(job, "progress", { stream: "stderr", line });
|
|
442
|
+
catch (err) {
|
|
443
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
444
|
+
emitMultiJob(job, "done", { success: false, error: message });
|
|
79
445
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
proc.kill("SIGTERM");
|
|
86
|
-
}
|
|
87
|
-
catch { /* process already gone */ }
|
|
88
|
-
}, INSTALL_TIMEOUT_MS);
|
|
89
|
-
proc.on("exit", (code) => {
|
|
90
|
-
clearTimeout(timer);
|
|
91
|
-
job.exitCode = timedOut ? -1 : code;
|
|
92
|
-
job.done = true;
|
|
93
|
-
const stderr = timedOut ? "timeout" : job.stderrBuffer.trim();
|
|
94
|
-
emitToJob(job, "done", {
|
|
95
|
-
success: !timedOut && code === 0,
|
|
96
|
-
exitCode: timedOut ? -1 : code,
|
|
97
|
-
stderr,
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
proc.on("error", (err) => {
|
|
101
|
-
clearTimeout(timer);
|
|
102
|
-
job.exitCode = -1;
|
|
103
|
-
job.done = true;
|
|
104
|
-
emitToJob(job, "done", {
|
|
105
|
-
success: false,
|
|
106
|
-
exitCode: -1,
|
|
107
|
-
stderr: err.message || "spawn failed",
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
JOBS.set(job.id, job);
|
|
446
|
+
finally {
|
|
447
|
+
job.done = true;
|
|
448
|
+
}
|
|
449
|
+
})();
|
|
111
450
|
return job;
|
|
112
451
|
}
|
|
113
|
-
export function registerInstallSkillRoutes(router) {
|
|
114
|
-
// POST /api/studio/install-skill
|
|
452
|
+
export function registerInstallSkillRoutes(router, root = process.cwd()) {
|
|
453
|
+
// POST /api/studio/install-skill
|
|
454
|
+
// Body shape (legacy single-agent CLI spawn): { skill: string, scope: "project"|"user"|"global" }
|
|
455
|
+
// Body shape (new multi-agent in-process): { skill: string, scope, agentIds: string[], parsedSkill: ParsedSkill, projectRoot?: string }
|
|
115
456
|
router.post("/api/studio/install-skill", async (req, res) => {
|
|
116
457
|
if (!isLocalhost(req)) {
|
|
117
458
|
sendJson(res, { error: "localhost-only endpoint" }, 403, req);
|
|
@@ -128,16 +469,81 @@ export function registerInstallSkillRoutes(router) {
|
|
|
128
469
|
sendJson(res, { error: "invalid scope (must be project|user|global)" }, 400, req);
|
|
129
470
|
return;
|
|
130
471
|
}
|
|
131
|
-
|
|
472
|
+
// 0845 T-017 (AC-US2-06): multi-agent in-process path.
|
|
473
|
+
if (body.agentIds !== undefined) {
|
|
474
|
+
const validation = validateAgentIds(body.agentIds);
|
|
475
|
+
if (!validation.ok) {
|
|
476
|
+
sendJson(res, { error: validation.error }, 400, req);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
480
|
+
// 0845 closure fix: server-side fallback resolves parsedSkill from
|
|
481
|
+
// the local skill registry when callers omit it. Keeps the frontend
|
|
482
|
+
// payload minimal — the browser doesn't need to fetch + parse
|
|
483
|
+
// SKILL.md itself. Explicit `parsedSkill` in the body still wins so
|
|
484
|
+
// out-of-tree skills (e.g. authoring flows) keep working.
|
|
485
|
+
let parsedSkill;
|
|
486
|
+
if (isValidParsedSkill(body.parsedSkill)) {
|
|
487
|
+
parsedSkill = {
|
|
488
|
+
...body.parsedSkill,
|
|
489
|
+
files: sanitizeSkillBundleFiles(body.parsedSkill.files),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
else if (body.parsedSkill !== undefined) {
|
|
493
|
+
sendJson(res, {
|
|
494
|
+
error: "parsedSkill, when provided, must have shape { name, description, body }",
|
|
495
|
+
}, 400, req);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const resolved = await resolveParsedSkillFromIdentifier(skill, { cwd: root });
|
|
500
|
+
if (!resolved) {
|
|
501
|
+
sendJson(res, {
|
|
502
|
+
error: `parsedSkill omitted and could not locate skill "${skill}" in project/personal/plugin-cache; ` +
|
|
503
|
+
"either install the skill locally first or include parsedSkill: { name, description, body } in the request body",
|
|
504
|
+
}, 404, req);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
parsedSkill = resolved;
|
|
508
|
+
}
|
|
509
|
+
const projectRoot = typeof body.projectRoot === "string" && body.projectRoot
|
|
510
|
+
? body.projectRoot
|
|
511
|
+
: root;
|
|
512
|
+
const job = await runMultiInstallJob({
|
|
513
|
+
identifier: skill,
|
|
514
|
+
skill: parsedSkill,
|
|
515
|
+
agentIds: validation.ids,
|
|
516
|
+
scope: normalizedScope,
|
|
517
|
+
projectRoot,
|
|
518
|
+
});
|
|
519
|
+
sendJson(res, {
|
|
520
|
+
jobId: job.id,
|
|
521
|
+
mode: "multi-agent",
|
|
522
|
+
streamPath: `/api/studio/install-skill/multi/${encodeURIComponent(job.id)}/stream`,
|
|
523
|
+
}, 202, req);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// 0845 T-017 (AC-US2-09): legacy single-agent CLI spawn — backward compat.
|
|
527
|
+
const job = startSpawnJob({
|
|
528
|
+
command: "vskill",
|
|
529
|
+
args: buildArgs(skill, scope),
|
|
530
|
+
meta: { skill, scope: scope },
|
|
531
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
532
|
+
jobs: JOBS,
|
|
533
|
+
});
|
|
132
534
|
sendJson(res, { jobId: job.id }, 202, req);
|
|
133
535
|
});
|
|
134
|
-
// GET /api/studio/install-skill/:jobId/stream —
|
|
135
|
-
router
|
|
536
|
+
// GET /api/studio/install-skill/:jobId/stream — legacy spawn-job stream.
|
|
537
|
+
mountSpawnStreamRoute(router, "/api/studio/install-skill", JOBS);
|
|
538
|
+
// 0845 T-017 — distinct stream for multi-install jobs (in-process).
|
|
539
|
+
// Same SSE shape (event: progress | done) so the frontend can consume
|
|
540
|
+
// both flavors with one EventSource subscription pattern.
|
|
541
|
+
router.get("/api/studio/install-skill/multi/:jobId/stream", async (req, res, params) => {
|
|
136
542
|
if (!isLocalhost(req)) {
|
|
137
543
|
sendJson(res, { error: "localhost-only endpoint" }, 403, req);
|
|
138
544
|
return;
|
|
139
545
|
}
|
|
140
|
-
const job =
|
|
546
|
+
const job = MULTI_JOBS.get(params.jobId);
|
|
141
547
|
if (!job) {
|
|
142
548
|
sendJson(res, { error: "unknown jobId" }, 404, req);
|
|
143
549
|
return;
|
|
@@ -145,7 +551,10 @@ export function registerInstallSkillRoutes(router) {
|
|
|
145
551
|
initSSE(res, req);
|
|
146
552
|
for (const ev of job.pastEvents) {
|
|
147
553
|
try {
|
|
148
|
-
|
|
554
|
+
if (ev.event === "done")
|
|
555
|
+
sendSSEDone(res, ev.data);
|
|
556
|
+
else
|
|
557
|
+
sendSSE(res, ev.event, ev.data);
|
|
149
558
|
}
|
|
150
559
|
catch { /* stream closed */ }
|
|
151
560
|
}
|
|
@@ -163,7 +572,9 @@ export function registerInstallSkillRoutes(router) {
|
|
|
163
572
|
else
|
|
164
573
|
sendSSE(res, event, data);
|
|
165
574
|
}
|
|
166
|
-
catch {
|
|
575
|
+
catch {
|
|
576
|
+
job.subscribers.delete(subscriber);
|
|
577
|
+
}
|
|
167
578
|
};
|
|
168
579
|
job.subscribers.add(subscriber);
|
|
169
580
|
const cleanup = () => { job.subscribers.delete(subscriber); };
|
|
@@ -171,5 +582,21 @@ export function registerInstallSkillRoutes(router) {
|
|
|
171
582
|
req.on("aborted", cleanup);
|
|
172
583
|
});
|
|
173
584
|
}
|
|
174
|
-
export const __test__ = {
|
|
585
|
+
export const __test__ = {
|
|
586
|
+
JOBS,
|
|
587
|
+
MULTI_JOBS,
|
|
588
|
+
SAFE_NAME,
|
|
589
|
+
SAFE_AGENT_ID,
|
|
590
|
+
VALID_SCOPES,
|
|
591
|
+
INSTALL_TIMEOUT_MS,
|
|
592
|
+
buildArgs,
|
|
593
|
+
normalizeInstallScope,
|
|
594
|
+
validateAgentIds,
|
|
595
|
+
isValidParsedSkill,
|
|
596
|
+
resolveParsedSkillFromIdentifier,
|
|
597
|
+
resolveParsedSkillFromPlatform,
|
|
598
|
+
fetchSkillMdFromGitHub,
|
|
599
|
+
runMultiInstallJob,
|
|
600
|
+
writeMultiInstallLockfile,
|
|
601
|
+
};
|
|
175
602
|
//# sourceMappingURL=install-skill-routes.js.map
|