vskill 1.0.13 → 1.0.15
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 +63 -2
- package/agents.json +1 -1
- package/dist/bin.js +0 -0
- package/dist/clone/github-scaffold.d.ts +38 -0
- package/dist/clone/github-scaffold.js +108 -0
- package/dist/clone/github-scaffold.js.map +1 -0
- package/dist/clone/provenance-fork.d.ts +34 -0
- package/dist/clone/provenance-fork.js +97 -0
- package/dist/clone/provenance-fork.js.map +1 -0
- package/dist/clone/reference-scanner.d.ts +19 -0
- package/dist/clone/reference-scanner.js +144 -0
- package/dist/clone/reference-scanner.js.map +1 -0
- package/dist/clone/skill-locator.d.ts +26 -0
- package/dist/clone/skill-locator.js +248 -0
- package/dist/clone/skill-locator.js.map +1 -0
- package/dist/clone/target-router.d.ts +73 -0
- package/dist/clone/target-router.js +200 -0
- package/dist/clone/target-router.js.map +1 -0
- package/dist/clone/types.d.ts +82 -0
- package/dist/clone/types.js +11 -0
- package/dist/clone/types.js.map +1 -0
- package/dist/commands/add.js +96 -32
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/auth.d.ts +23 -0
- package/dist/commands/auth.js +273 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/check.d.ts +55 -0
- package/dist/commands/check.js +279 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/clone-prompts.d.ts +13 -0
- package/dist/commands/clone-prompts.js +67 -0
- package/dist/commands/clone-prompts.js.map +1 -0
- package/dist/commands/clone.d.ts +70 -0
- package/dist/commands/clone.js +649 -0
- package/dist/commands/clone.js.map +1 -0
- package/dist/commands/eval/serve.js +8 -1
- package/dist/commands/eval/serve.js.map +1 -1
- package/dist/commands/keys.js +54 -2
- package/dist/commands/keys.js.map +1 -1
- package/dist/core/agent-prompts.d.ts +35 -0
- package/dist/core/agent-prompts.js +201 -0
- package/dist/core/agent-prompts.js.map +1 -0
- package/dist/core/skill-generator.d.ts +25 -3
- package/dist/core/skill-generator.js +131 -0
- package/dist/core/skill-generator.js.map +1 -1
- package/dist/eval/skill-scanner.d.ts +2 -12
- package/dist/eval/skill-scanner.js +27 -5
- package/dist/eval/skill-scanner.js.map +1 -1
- package/dist/eval-server/api-routes.d.ts +14 -0
- package/dist/eval-server/api-routes.js +376 -31
- package/dist/eval-server/api-routes.js.map +1 -1
- package/dist/eval-server/data-events.d.ts +1 -1
- package/dist/eval-server/data-events.js.map +1 -1
- package/dist/eval-server/install-engine-routes-helpers.d.ts +1 -3
- package/dist/eval-server/install-engine-routes-helpers.js +6 -14
- package/dist/eval-server/install-engine-routes-helpers.js.map +1 -1
- package/dist/eval-server/origin-resolver.d.ts +42 -0
- package/dist/eval-server/origin-resolver.js +168 -0
- package/dist/eval-server/origin-resolver.js.map +1 -0
- package/dist/eval-server/platform-proxy.d.ts +10 -0
- package/dist/eval-server/platform-proxy.js +58 -2
- package/dist/eval-server/platform-proxy.js.map +1 -1
- package/dist/eval-server/skill-create-routes.d.ts +8 -0
- package/dist/eval-server/skill-create-routes.js +96 -0
- package/dist/eval-server/skill-create-routes.js.map +1 -1
- package/dist/eval-server/skill-resolver.js +40 -0
- package/dist/eval-server/skill-resolver.js.map +1 -1
- package/dist/eval-server/utils/resolve-editor.d.ts +18 -0
- package/dist/eval-server/utils/resolve-editor.js +77 -0
- package/dist/eval-server/utils/resolve-editor.js.map +1 -0
- package/dist/eval-server/utils/scan-install-locations.d.ts +7 -0
- package/dist/eval-server/utils/scan-install-locations.js +20 -0
- package/dist/eval-server/utils/scan-install-locations.js.map +1 -1
- package/dist/eval-server/utils/which.d.ts +15 -0
- package/dist/eval-server/utils/which.js +76 -0
- package/dist/eval-server/utils/which.js.map +1 -0
- package/dist/eval-ui/assets/{CreateSkillPage-T0YWZWw-.js → CreateSkillPage-BmbvQEzE.js} +1 -1
- package/dist/eval-ui/assets/{FindSkillsPalette-KcFM32hZ.js → FindSkillsPalette-D0Zjhm31.js} +2 -2
- package/dist/eval-ui/assets/{SearchPaletteCore-EhBtr4Xx.js → SearchPaletteCore-EhcN1xEa.js} +1 -1
- package/dist/eval-ui/assets/SkillDetailPanel-B5J60ffv.js +1 -0
- package/dist/eval-ui/assets/{UpdateDropdown-pjFhHTi6.js → UpdateDropdown-Celf0_Cr.js} +1 -1
- package/dist/eval-ui/assets/index-BV7k6fdk.js +124 -0
- package/dist/eval-ui/assets/{index-BKAvJDDF.css → index-CKLqBL52.css} +1 -1
- package/dist/eval-ui/index.html +2 -2
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -1
- package/dist/installer/frontmatter.d.ts +26 -0
- package/dist/installer/frontmatter.js +90 -0
- package/dist/installer/frontmatter.js.map +1 -1
- package/dist/lib/github-fetch.d.ts +22 -0
- package/dist/lib/github-fetch.js +152 -0
- package/dist/lib/github-fetch.js.map +1 -0
- package/dist/lib/keychain.d.ts +41 -0
- package/dist/lib/keychain.js +232 -0
- package/dist/lib/keychain.js.map +1 -0
- package/dist/studio/types.d.ts +13 -0
- package/dist/utils/claude-plugin.d.ts +26 -0
- package/dist/utils/claude-plugin.js +60 -0
- package/dist/utils/claude-plugin.js.map +1 -1
- package/package.json +2 -1
- package/dist/eval-ui/assets/SkillDetailPanel-cyzLsLcK.js +0 -1
- package/dist/eval-ui/assets/index-C3S9iHnq.js +0 -122
|
@@ -2,24 +2,16 @@
|
|
|
2
2
|
// install-engine-routes-helpers.ts — extracted prerequisite-CLI lookup so it
|
|
3
3
|
// can be mocked independently of node:child_process in unit tests.
|
|
4
4
|
// Ref: 0734 AC-US5-02
|
|
5
|
+
//
|
|
6
|
+
// Now a thin re-export of the shared `whichSync` helper; the previous
|
|
7
|
+
// inline `spawnSync`-based implementation lives in utils/which.ts.
|
|
5
8
|
// ---------------------------------------------------------------------------
|
|
6
|
-
import {
|
|
9
|
+
import { whichSync } from "./utils/which.js";
|
|
7
10
|
/**
|
|
8
11
|
* Returns true when `cmd` resolves to an executable on PATH.
|
|
9
|
-
*
|
|
10
|
-
* Uses `which` (POSIX) / `where` (Windows). Pure best-effort: any failure to
|
|
11
|
-
* spawn the lookup process is treated as "not available".
|
|
12
|
+
* Uses `which` / `where` under the hood with metacharacter guard + cache.
|
|
12
13
|
*/
|
|
13
14
|
export function isCliAvailable(cmd) {
|
|
14
|
-
|
|
15
|
-
return false; // hard guard against shell metacharacters
|
|
16
|
-
const lookup = process.platform === "win32" ? "where" : "which";
|
|
17
|
-
try {
|
|
18
|
-
const res = spawnSync(lookup, [cmd], { stdio: "ignore" });
|
|
19
|
-
return res.status === 0;
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
15
|
+
return whichSync(cmd);
|
|
24
16
|
}
|
|
25
17
|
//# sourceMappingURL=install-engine-routes-helpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-engine-routes-helpers.js","sourceRoot":"","sources":["../../src/eval-server/install-engine-routes-helpers.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,mEAAmE;AACnE,sBAAsB;AACtB,8EAA8E;AAE9E,OAAO,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"install-engine-routes-helpers.js","sourceRoot":"","sources":["../../src/eval-server/install-engine-routes-helpers.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,mEAAmE;AACnE,sBAAsB;AACtB,EAAE;AACF,sEAAsE;AACtE,mEAAmE;AACnE,8EAA8E;AAE9E,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type OriginSource = "platform" | "anthropic-registry" | "local";
|
|
2
|
+
export type OriginProvider = "vskill" | "anthropic" | "local";
|
|
3
|
+
export interface OriginEnvelope {
|
|
4
|
+
source: OriginSource;
|
|
5
|
+
owner: string | null;
|
|
6
|
+
repo: string | null;
|
|
7
|
+
provider: OriginProvider;
|
|
8
|
+
trackedForUpdates: boolean;
|
|
9
|
+
/** Lockfile dir that produced the hit (for debugging). */
|
|
10
|
+
lockfilePath?: string;
|
|
11
|
+
/** Frontmatter source string when Tier 3 hits (debug). */
|
|
12
|
+
frontmatterSource?: string;
|
|
13
|
+
/** Registry key when Tier 4 hits (debug). */
|
|
14
|
+
registryMatch?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Well-known Anthropic-shipped skills. Mapped to their canonical
|
|
18
|
+
* `anthropics/skills/<name>` upstream (the verified-skill.com URL pattern
|
|
19
|
+
* for skills published from the `anthropics/skills` GitHub repo) so the
|
|
20
|
+
* Versions tab can fetch upstream history even when the skill arrived
|
|
21
|
+
* without a vskill lockfile entry.
|
|
22
|
+
*
|
|
23
|
+
* Verified live (2026-05-01): `https://verified-skill.com/api/v1/skills/anthropics/skills/pptx/versions`
|
|
24
|
+
* returns `{ versions, count, unversioned, currentVersion }` JSON. The
|
|
25
|
+
* `anthropic-skills/<name>` pattern (with hyphen) returns 404.
|
|
26
|
+
*
|
|
27
|
+
* Maintained as a hand-curated list. Add new names as Anthropic ships them.
|
|
28
|
+
*/
|
|
29
|
+
export declare const ANTHROPIC_SKILL_REGISTRY: Record<string, {
|
|
30
|
+
owner: string;
|
|
31
|
+
repo: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** Test-only: clear the in-process cache between cases. */
|
|
34
|
+
export declare function resetOriginResolverCache(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a skill's full provenance envelope.
|
|
37
|
+
*
|
|
38
|
+
* @param skill — bare skill slug (e.g. "slack-messaging")
|
|
39
|
+
* @param plugin — the plugin/agent dir the skill is mounted under (e.g. ".claude")
|
|
40
|
+
* @param root — the studio process cwd (project root)
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveSkillOrigin(skill: string, plugin: string | null, root: string): Promise<OriginEnvelope>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// 0823 — Comprehensive Origin Resolver for Skill Provenance.
|
|
2
|
+
//
|
|
3
|
+
// Determines where a skill came from so the Versions tab + rescan can fetch
|
|
4
|
+
// upstream version history. Walks five tiers; first hit wins.
|
|
5
|
+
//
|
|
6
|
+
// (1) project lockfile `<root>/vskill.lock`
|
|
7
|
+
// (2) user-global lockfile `~/.agents/vskill.lock`
|
|
8
|
+
// (3) frontmatter `source:` field in SKILL.md (deferred — present in interface,
|
|
9
|
+
// wired in a follow-up; today this tier is reserved and falls through.)
|
|
10
|
+
// (4) Anthropic-skill registry (well-known names → anthropics/skills/<name>)
|
|
11
|
+
// — see ANTHROPIC_SKILL_REGISTRY below. Verified-live URL pattern.
|
|
12
|
+
// (5) bare-name fallback / unknown (provider="local", no upstream)
|
|
13
|
+
//
|
|
14
|
+
// IMPORTANT: cache resolved envelopes to avoid repeated lockfile reads, but
|
|
15
|
+
// DO NOT cache the "local" fallback — a future install would otherwise be
|
|
16
|
+
// invisible until studio restart.
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { readLockfile } from "../lockfile/lockfile.js";
|
|
20
|
+
import { parseSource } from "../resolvers/source-resolver.js";
|
|
21
|
+
/**
|
|
22
|
+
* Well-known Anthropic-shipped skills. Mapped to their canonical
|
|
23
|
+
* `anthropics/skills/<name>` upstream (the verified-skill.com URL pattern
|
|
24
|
+
* for skills published from the `anthropics/skills` GitHub repo) so the
|
|
25
|
+
* Versions tab can fetch upstream history even when the skill arrived
|
|
26
|
+
* without a vskill lockfile entry.
|
|
27
|
+
*
|
|
28
|
+
* Verified live (2026-05-01): `https://verified-skill.com/api/v1/skills/anthropics/skills/pptx/versions`
|
|
29
|
+
* returns `{ versions, count, unversioned, currentVersion }` JSON. The
|
|
30
|
+
* `anthropic-skills/<name>` pattern (with hyphen) returns 404.
|
|
31
|
+
*
|
|
32
|
+
* Maintained as a hand-curated list. Add new names as Anthropic ships them.
|
|
33
|
+
*/
|
|
34
|
+
export const ANTHROPIC_SKILL_REGISTRY = {
|
|
35
|
+
"slack-messaging": { owner: "anthropics", repo: "skills" },
|
|
36
|
+
pptx: { owner: "anthropics", repo: "skills" },
|
|
37
|
+
"excalidraw-diagram-generator": { owner: "anthropics", repo: "skills" },
|
|
38
|
+
"excalidraw-skill": { owner: "anthropics", repo: "skills" },
|
|
39
|
+
"frontend-design": { owner: "anthropics", repo: "skills" },
|
|
40
|
+
gws: { owner: "anthropics", repo: "skills" },
|
|
41
|
+
"remotion-best-practices": { owner: "anthropics", repo: "skills" },
|
|
42
|
+
"social-media-posting": { owner: "anthropics", repo: "skills" },
|
|
43
|
+
"webapp-testing": { owner: "anthropics", repo: "skills" },
|
|
44
|
+
"obsidian-brain": { owner: "anthropics", repo: "skills" },
|
|
45
|
+
nanobanana: { owner: "anthropics", repo: "skills" },
|
|
46
|
+
pdf: { owner: "anthropics", repo: "skills" },
|
|
47
|
+
docx: { owner: "anthropics", repo: "skills" },
|
|
48
|
+
xlsx: { owner: "anthropics", repo: "skills" },
|
|
49
|
+
"skill-creator": { owner: "anthropics", repo: "skills" },
|
|
50
|
+
};
|
|
51
|
+
// 0823 simplify: cap the cache so a long-lived studio process polling many
|
|
52
|
+
// distinct (plugin, skill) combinations doesn't grow unboundedly. The Map's
|
|
53
|
+
// insertion-order semantics give us cheap LRU-ish eviction: when we exceed
|
|
54
|
+
// MAX_CACHE_ENTRIES, drop the oldest entries until under the soft limit.
|
|
55
|
+
const MAX_CACHE_ENTRIES = 1000;
|
|
56
|
+
const PRUNE_TARGET = 500;
|
|
57
|
+
const cache = new Map();
|
|
58
|
+
function setCacheEntry(key, value) {
|
|
59
|
+
cache.set(key, value);
|
|
60
|
+
if (cache.size > MAX_CACHE_ENTRIES) {
|
|
61
|
+
const dropCount = cache.size - PRUNE_TARGET;
|
|
62
|
+
let i = 0;
|
|
63
|
+
for (const k of cache.keys()) {
|
|
64
|
+
if (i++ >= dropCount)
|
|
65
|
+
break;
|
|
66
|
+
cache.delete(k);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Test-only: clear the in-process cache between cases. */
|
|
71
|
+
export function resetOriginResolverCache() {
|
|
72
|
+
cache.clear();
|
|
73
|
+
}
|
|
74
|
+
function tryLockfileTier(skill, dir) {
|
|
75
|
+
let lock;
|
|
76
|
+
try {
|
|
77
|
+
lock = readLockfile(dir);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const entry = lock?.skills?.[skill];
|
|
83
|
+
if (!entry?.source)
|
|
84
|
+
return null;
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = parseSource(entry.source);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if ((parsed.type === "github" ||
|
|
93
|
+
parsed.type === "github-plugin" ||
|
|
94
|
+
parsed.type === "marketplace") &&
|
|
95
|
+
parsed.owner &&
|
|
96
|
+
parsed.repo) {
|
|
97
|
+
return { owner: parsed.owner, repo: parsed.repo };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a skill's full provenance envelope.
|
|
103
|
+
*
|
|
104
|
+
* @param skill — bare skill slug (e.g. "slack-messaging")
|
|
105
|
+
* @param plugin — the plugin/agent dir the skill is mounted under (e.g. ".claude")
|
|
106
|
+
* @param root — the studio process cwd (project root)
|
|
107
|
+
*/
|
|
108
|
+
export async function resolveSkillOrigin(skill, plugin, root) {
|
|
109
|
+
const cacheKey = `${plugin ?? ""}::${skill}`;
|
|
110
|
+
const cached = cache.get(cacheKey);
|
|
111
|
+
if (cached)
|
|
112
|
+
return cached;
|
|
113
|
+
// Tier 1 — project lockfile
|
|
114
|
+
const projectHit = tryLockfileTier(skill, root);
|
|
115
|
+
if (projectHit) {
|
|
116
|
+
const env = {
|
|
117
|
+
source: "platform",
|
|
118
|
+
owner: projectHit.owner,
|
|
119
|
+
repo: projectHit.repo,
|
|
120
|
+
provider: "vskill",
|
|
121
|
+
trackedForUpdates: true,
|
|
122
|
+
lockfilePath: root,
|
|
123
|
+
};
|
|
124
|
+
setCacheEntry(cacheKey, env);
|
|
125
|
+
return env;
|
|
126
|
+
}
|
|
127
|
+
// Tier 2 — user-global lockfile (~/.agents/vskill.lock)
|
|
128
|
+
const globalDir = join(homedir(), ".agents");
|
|
129
|
+
const globalHit = tryLockfileTier(skill, globalDir);
|
|
130
|
+
if (globalHit) {
|
|
131
|
+
const env = {
|
|
132
|
+
source: "platform",
|
|
133
|
+
owner: globalHit.owner,
|
|
134
|
+
repo: globalHit.repo,
|
|
135
|
+
provider: "vskill",
|
|
136
|
+
trackedForUpdates: true,
|
|
137
|
+
lockfilePath: globalDir,
|
|
138
|
+
};
|
|
139
|
+
setCacheEntry(cacheKey, env);
|
|
140
|
+
return env;
|
|
141
|
+
}
|
|
142
|
+
// Tier 3 — frontmatter `source:` field. Reserved; not yet wired (no
|
|
143
|
+
// installed-skill writes a frontmatter origin today). Tier-skipped silently.
|
|
144
|
+
// Tier 4 — Anthropic-skill registry
|
|
145
|
+
const registryHit = ANTHROPIC_SKILL_REGISTRY[skill];
|
|
146
|
+
if (registryHit) {
|
|
147
|
+
const env = {
|
|
148
|
+
source: "anthropic-registry",
|
|
149
|
+
owner: registryHit.owner,
|
|
150
|
+
repo: registryHit.repo,
|
|
151
|
+
provider: "anthropic",
|
|
152
|
+
trackedForUpdates: true,
|
|
153
|
+
registryMatch: skill,
|
|
154
|
+
};
|
|
155
|
+
setCacheEntry(cacheKey, env);
|
|
156
|
+
return env;
|
|
157
|
+
}
|
|
158
|
+
// Tier 5 — local / unknown. NOT cached so a future install becomes visible
|
|
159
|
+
// without a studio restart.
|
|
160
|
+
return {
|
|
161
|
+
source: "local",
|
|
162
|
+
owner: null,
|
|
163
|
+
repo: null,
|
|
164
|
+
provider: "local",
|
|
165
|
+
trackedForUpdates: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=origin-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origin-resolver.js","sourceRoot":"","sources":["../../src/eval-server/origin-resolver.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,4EAA4E;AAC5E,8DAA8D;AAC9D,EAAE;AACF,kDAAkD;AAClD,qDAAqD;AACrD,kFAAkF;AAClF,8EAA8E;AAC9E,+EAA+E;AAC/E,yEAAyE;AACzE,qEAAqE;AACrE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,kCAAkC;AAElC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAmB9D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAoD;IACvF,iBAAiB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC1D,IAAI,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC7C,8BAA8B,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IACvE,kBAAkB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC3D,iBAAiB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC1D,GAAG,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC5C,yBAAyB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAClE,sBAAsB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC/D,gBAAgB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IACzD,gBAAgB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IACzD,UAAU,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IACnD,GAAG,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC5C,IAAI,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC7C,IAAI,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC7C,eAAe,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;CACzD,CAAC;AAEF,2EAA2E;AAC3E,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;AAEhD,SAAS,aAAa,CAAC,GAAW,EAAE,KAAqB;IACvD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtB,IAAI,KAAK,CAAC,IAAI,GAAG,iBAAiB,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,GAAG,YAAY,CAAC;QAC5C,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,EAAE,IAAI,SAAS;gBAAE,MAAM;YAC5B,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;AACH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,wBAAwB;IACtC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAED,SAAS,eAAe,CACtB,KAAa,EACb,GAAW;IAEX,IAAI,IAAI,CAAC;IACT,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IACE,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ;QACvB,MAAM,CAAC,IAAI,KAAK,eAAe;QAC/B,MAAM,CAAC,IAAI,KAAK,aAAa,CAAC;QAChC,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,IAAI,EACX,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAa,EACb,MAAqB,EACrB,IAAY;IAEZ,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;IAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,4BAA4B;IAC5B,MAAM,UAAU,GAAG,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChD,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,GAAG,GAAmB;YAC1B,MAAM,EAAE,UAAU;YAClB,KAAK,EAAE,UAAU,CAAC,KAAK;YACvB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,QAAQ,EAAE,QAAQ;YAClB,iBAAiB,EAAE,IAAI;YACvB,YAAY,EAAE,IAAI;SACnB,CAAC;QACF,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,wDAAwD;IACxD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IACpD,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,GAAG,GAAmB;YAC1B,MAAM,EAAE,UAAU;YAClB,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,IAAI,EAAE,SAAS,CAAC,IAAI;YACpB,QAAQ,EAAE,QAAQ;YAClB,iBAAiB,EAAE,IAAI;YACvB,YAAY,EAAE,SAAS;SACxB,CAAC;QACF,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,oEAAoE;IACpE,6EAA6E;IAE7E,oCAAoC;IACpC,MAAM,WAAW,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,GAAG,GAAmB;YAC1B,MAAM,EAAE,oBAAoB;YAC5B,KAAK,EAAE,WAAW,CAAC,KAAK;YACxB,IAAI,EAAE,WAAW,CAAC,IAAI;YACtB,QAAQ,EAAE,WAAW;YACrB,iBAAiB,EAAE,IAAI;YACvB,aAAa,EAAE,KAAK;SACrB,CAAC;QACF,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,2EAA2E;IAC3E,4BAA4B;IAC5B,OAAO;QACL,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,IAAI;QACX,IAAI,EAAE,IAAI;QACV,QAAQ,EAAE,OAAO;QACjB,iBAAiB,EAAE,KAAK;KACzB,CAAC;AACJ,CAAC"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
export declare function getPlatformBaseUrl(): string;
|
|
3
3
|
export declare function shouldProxyToPlatform(url: string | undefined): boolean;
|
|
4
|
+
export declare function shouldInjectAuth(url: string | undefined): boolean;
|
|
5
|
+
/** Test-only reset hook. */
|
|
6
|
+
export declare function _resetPlatformProxyAuthCacheForTests(): void;
|
|
7
|
+
export interface PickHeadersOptions {
|
|
8
|
+
/** Request path (e.g. "/api/v1/tenants/abc/skills"). */
|
|
9
|
+
path?: string;
|
|
10
|
+
/** Token provider override; defaults to in-process cached keychain read. */
|
|
11
|
+
tokenProvider?: () => string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare function pickHeadersForUpstream(src: http.IncomingHttpHeaders, opts?: PickHeadersOptions): Record<string, string>;
|
|
4
14
|
/**
|
|
5
15
|
* Proxy a single HTTP request from the studio to the platform.
|
|
6
16
|
* SSE-safe: streams the upstream body chunks directly to the response so
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
import * as http from "node:http";
|
|
51
51
|
import * as https from "node:https";
|
|
52
52
|
import { randomUUID } from "node:crypto";
|
|
53
|
+
import { getDefaultKeychain } from "../lib/keychain.js";
|
|
53
54
|
const DEFAULT_PLATFORM_URL = "https://verified-skill.com";
|
|
54
55
|
// Hop-by-hop headers per RFC 2616 §13.5.1 — never forward these on a proxy.
|
|
55
56
|
const HOP_BY_HOP = new Set([
|
|
@@ -88,24 +89,79 @@ const PROXY_PREFIXES = [
|
|
|
88
89
|
"/api/v1/studio/search",
|
|
89
90
|
"/api/v1/studio/telemetry/",
|
|
90
91
|
"/api/v1/stats",
|
|
92
|
+
// Private (org-scoped) routes that must carry the user's GitHub bearer token
|
|
93
|
+
// to the platform. The browser never sees this token; injection happens here.
|
|
94
|
+
"/api/v1/private/",
|
|
95
|
+
"/api/v1/tenants/",
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* Path prefixes that require an `Authorization: Bearer <github-token>` header
|
|
99
|
+
* to be injected at the proxy boundary. Public skill routes are intentionally
|
|
100
|
+
* excluded — they must remain anonymous so unauthenticated tabs continue to
|
|
101
|
+
* function in the public skill catalog.
|
|
102
|
+
*/
|
|
103
|
+
const AUTH_REQUIRED_PREFIXES = [
|
|
104
|
+
"/api/v1/private/",
|
|
105
|
+
"/api/v1/tenants/",
|
|
91
106
|
];
|
|
92
107
|
export function shouldProxyToPlatform(url) {
|
|
93
108
|
if (!url)
|
|
94
109
|
return false;
|
|
95
110
|
return PROXY_PREFIXES.some((p) => url.startsWith(p));
|
|
96
111
|
}
|
|
97
|
-
function
|
|
112
|
+
export function shouldInjectAuth(url) {
|
|
113
|
+
if (!url)
|
|
114
|
+
return false;
|
|
115
|
+
return AUTH_REQUIRED_PREFIXES.some((p) => url.startsWith(p));
|
|
116
|
+
}
|
|
117
|
+
// In-process token cache so a burst of proxy requests doesn't repeatedly hit
|
|
118
|
+
// the OS keychain. The keychain itself is fast on macOS but can be slower on
|
|
119
|
+
// Linux libsecret. 60s aligns with how stale a manual `vskill auth login`
|
|
120
|
+
// → `vskill studio refresh` loop would feel.
|
|
121
|
+
let _cachedToken = null;
|
|
122
|
+
const TOKEN_CACHE_MS = 60_000;
|
|
123
|
+
function readTokenForProxy(now = Date.now()) {
|
|
124
|
+
if (_cachedToken && _cachedToken.expiresAt > now)
|
|
125
|
+
return _cachedToken.value;
|
|
126
|
+
let token = null;
|
|
127
|
+
try {
|
|
128
|
+
token = getDefaultKeychain().getGitHubToken();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
token = null;
|
|
132
|
+
}
|
|
133
|
+
_cachedToken = { value: token, expiresAt: now + TOKEN_CACHE_MS };
|
|
134
|
+
return token;
|
|
135
|
+
}
|
|
136
|
+
/** Test-only reset hook. */
|
|
137
|
+
export function _resetPlatformProxyAuthCacheForTests() {
|
|
138
|
+
_cachedToken = null;
|
|
139
|
+
}
|
|
140
|
+
export function pickHeadersForUpstream(src, opts = {}) {
|
|
98
141
|
const out = {};
|
|
99
142
|
for (const [k, v] of Object.entries(src)) {
|
|
100
143
|
if (typeof v === "undefined")
|
|
101
144
|
continue;
|
|
102
145
|
if (HOP_BY_HOP.has(k.toLowerCase()))
|
|
103
146
|
continue;
|
|
147
|
+
// Strip any client-supplied Authorization on private/tenant paths — we
|
|
148
|
+
// mint our own from the keychain. On other paths we pass through (the
|
|
149
|
+
// existing public proxy never carried Authorization, so this is a no-op).
|
|
150
|
+
if (k.toLowerCase() === "authorization" && opts.path && shouldInjectAuth(opts.path)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
104
153
|
out[k] = Array.isArray(v) ? v.join(", ") : String(v);
|
|
105
154
|
}
|
|
106
155
|
// Preserve client-IP intent for any future platform-side observability.
|
|
107
156
|
const xff = out["x-forwarded-for"];
|
|
108
157
|
out["x-forwarded-for"] = xff ? `${xff}, 127.0.0.1` : "127.0.0.1";
|
|
158
|
+
if (opts.path && shouldInjectAuth(opts.path)) {
|
|
159
|
+
const tokenProvider = opts.tokenProvider ?? (() => readTokenForProxy());
|
|
160
|
+
const token = tokenProvider();
|
|
161
|
+
if (token) {
|
|
162
|
+
out["authorization"] = `Bearer ${token}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
109
165
|
return out;
|
|
110
166
|
}
|
|
111
167
|
function pickHeadersForDownstream(src) {
|
|
@@ -138,7 +194,7 @@ export function proxyToPlatform(req, res, baseUrl = getPlatformBaseUrl()) {
|
|
|
138
194
|
port: target.port || (target.protocol === "https:" ? 443 : 80),
|
|
139
195
|
path: `${target.pathname}${target.search}`,
|
|
140
196
|
method: req.method,
|
|
141
|
-
headers: pickHeadersForUpstream(req.headers),
|
|
197
|
+
headers: pickHeadersForUpstream(req.headers, { path: target.pathname }),
|
|
142
198
|
}, (upstreamRes) => {
|
|
143
199
|
const status = upstreamRes.statusCode ?? 502;
|
|
144
200
|
const headers = pickHeadersForDownstream(upstreamRes.headers);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-proxy.js","sourceRoot":"","sources":["../../src/eval-server/platform-proxy.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,2EAA2E;AAC3E,gDAAgD;AAChD,EAAE;AACF,iFAAiF;AACjF,wEAAwE;AACxE,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,yEAAyE;AACzE,6EAA6E;AAC7E,yEAAyE;AACzE,4EAA4E;AAC5E,4BAA4B;AAC5B,EAAE;AACF,gBAAgB;AAChB,wEAAwE;AACxE,2EAA2E;AAC3E,kFAAkF;AAClF,4DAA4D;AAC5D,yEAAyE;AACzE,wEAAwE;AACxE,0EAA0E;AAC1E,oCAAoC;AACpC,oEAAoE;AACpE,4CAA4C;AAC5C,mEAAmE;AACnE,qEAAqE;AACrE,0CAA0C;AAC1C,EAAE;AACF,yDAAyD;AACzD,0DAA0D;AAC1D,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,qEAAqE;AACrE,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,yEAAyE;AACzE,4EAA4E;AAC5E,EAAE;AACF,0EAA0E;AAC1E,sEAAsE;AACtE,yEAAyE;AACzE,8EAA8E;AAE9E,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"platform-proxy.js","sourceRoot":"","sources":["../../src/eval-server/platform-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -126,6 +126,14 @@ export declare function interpretValidatorResult(skillPath: string, res: SpawnRe
|
|
|
126
126
|
* helper trivially testable.
|
|
127
127
|
*/
|
|
128
128
|
export declare function formatValidatorReport(outcome: ValidatorOutcome): string;
|
|
129
|
+
/** Compute target directory for a new skill based on layout */
|
|
130
|
+
export declare function resolveSafe(base: string, relPath: string): string;
|
|
131
|
+
/**
|
|
132
|
+
* Build a `.env.example` body from a list of secret env-var names. Each line
|
|
133
|
+
* is `NAME=` with no value — the file ships placeholders only, never real
|
|
134
|
+
* secrets. Caller is responsible for including a leading comment if desired.
|
|
135
|
+
*/
|
|
136
|
+
export declare function buildEnvExample(secrets: string[]): string;
|
|
129
137
|
/**
|
|
130
138
|
* Check if a resolved path is strictly within a root directory.
|
|
131
139
|
* Uses trailing separator to prevent prefix collision (e.g., /project vs /project-evil).
|
|
@@ -412,6 +412,55 @@ export function formatValidatorReport(outcome) {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
/** Compute target directory for a new skill based on layout */
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// 0815: path safety + .env.example helpers for multi-file skill creation.
|
|
417
|
+
//
|
|
418
|
+
// resolveSafe rejects any path that would escape the skill directory: absolute
|
|
419
|
+
// paths, drive letters, '..' segments, and relative paths whose resolved form
|
|
420
|
+
// lands outside `base`. Used by POST /api/skills/create when looping the
|
|
421
|
+
// optional `files` map. Throws so the route returns 400.
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
export function resolveSafe(base, relPath) {
|
|
424
|
+
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
425
|
+
throw new Error(`resolveSafe: empty path`);
|
|
426
|
+
}
|
|
427
|
+
// Reject absolute paths (POSIX) and Windows drive letters.
|
|
428
|
+
if (relPath.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(relPath)) {
|
|
429
|
+
throw new Error(`resolveSafe: refused absolute path '${relPath}'`);
|
|
430
|
+
}
|
|
431
|
+
// Reject any '..' segment — easier to reason about than relying on resolve()
|
|
432
|
+
// alone, since a benign-looking resolve result can still hide a traversal
|
|
433
|
+
// depending on the realpath of `base`.
|
|
434
|
+
const segments = relPath.split(/[/\\]+/);
|
|
435
|
+
if (segments.some((s) => s === "..")) {
|
|
436
|
+
throw new Error(`resolveSafe: refused traversal in '${relPath}'`);
|
|
437
|
+
}
|
|
438
|
+
const resolved = resolve(base, relPath);
|
|
439
|
+
const baseResolved = resolve(base);
|
|
440
|
+
// Defense in depth: ensure the resolved path is still inside base.
|
|
441
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
|
|
442
|
+
throw new Error(`resolveSafe: path '${relPath}' escapes base`);
|
|
443
|
+
}
|
|
444
|
+
return resolved;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Build a `.env.example` body from a list of secret env-var names. Each line
|
|
448
|
+
* is `NAME=` with no value — the file ships placeholders only, never real
|
|
449
|
+
* secrets. Caller is responsible for including a leading comment if desired.
|
|
450
|
+
*/
|
|
451
|
+
export function buildEnvExample(secrets) {
|
|
452
|
+
const lines = [
|
|
453
|
+
"# .env.example — populated by the skill author, never committed.",
|
|
454
|
+
"# Copy to .env.local in this directory and fill in real values.",
|
|
455
|
+
"",
|
|
456
|
+
];
|
|
457
|
+
for (const name of secrets) {
|
|
458
|
+
if (typeof name !== "string" || name.length === 0)
|
|
459
|
+
continue;
|
|
460
|
+
lines.push(`${name}=`);
|
|
461
|
+
}
|
|
462
|
+
return lines.join("\n") + "\n";
|
|
463
|
+
}
|
|
415
464
|
function computeSkillDir(root, layout, plugin, name) {
|
|
416
465
|
switch (layout) {
|
|
417
466
|
case 1: return join(root, plugin, "skills", name);
|
|
@@ -1018,6 +1067,53 @@ export function registerSkillCreateRoutes(router, root) {
|
|
|
1018
1067
|
catch { /* history write failure should not break the main response */ }
|
|
1019
1068
|
}
|
|
1020
1069
|
}
|
|
1070
|
+
// -------------------------------------------------------------------
|
|
1071
|
+
// 0815: multi-file payload — write auxiliary files atomically.
|
|
1072
|
+
//
|
|
1073
|
+
// Each `files` entry is validated through resolveSafe() before any I/O.
|
|
1074
|
+
// On any failure mid-write, rollback by removing the skill directory
|
|
1075
|
+
// (unless this is an update — preserve existing user content). The
|
|
1076
|
+
// rollback is intentionally NOT inside a finally — the existing outer
|
|
1077
|
+
// try/catch (a few lines below) handles top-level failures and we want
|
|
1078
|
+
// the path-traversal case to surface as a 400 with the dir cleaned up.
|
|
1079
|
+
// -------------------------------------------------------------------
|
|
1080
|
+
if (body.files && typeof body.files === "object") {
|
|
1081
|
+
const written = [];
|
|
1082
|
+
try {
|
|
1083
|
+
for (const [relPath, contents] of Object.entries(body.files)) {
|
|
1084
|
+
if (typeof contents !== "string") {
|
|
1085
|
+
throw new Error(`files['${relPath}'] is not a string`);
|
|
1086
|
+
}
|
|
1087
|
+
const safePath = resolveSafe(targetDir, relPath);
|
|
1088
|
+
const safeDir = safePath.slice(0, safePath.lastIndexOf(sep));
|
|
1089
|
+
mkdirSync(safeDir, { recursive: true });
|
|
1090
|
+
writeFileSync(safePath, contents, "utf-8");
|
|
1091
|
+
written.push(safePath);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
catch (err) {
|
|
1095
|
+
// Rollback: clean up the partial directory unless we're updating.
|
|
1096
|
+
if (!isUpdateMode) {
|
|
1097
|
+
try {
|
|
1098
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1099
|
+
}
|
|
1100
|
+
catch { /* best-effort */ }
|
|
1101
|
+
}
|
|
1102
|
+
sendJson(res, {
|
|
1103
|
+
error: `Failed to write multi-file payload: ${err.message}`,
|
|
1104
|
+
code: "multi-file-write-failed",
|
|
1105
|
+
}, 400, req);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// 0815: emit .env.example from declared secrets (placeholders only).
|
|
1110
|
+
if (Array.isArray(body.secrets) && body.secrets.length > 0) {
|
|
1111
|
+
try {
|
|
1112
|
+
const envExamplePath = join(targetDir, ".env.example");
|
|
1113
|
+
writeFileSync(envExamplePath, buildEnvExample(body.secrets), "utf-8");
|
|
1114
|
+
}
|
|
1115
|
+
catch { /* non-blocking — secrets are advisory */ }
|
|
1116
|
+
}
|
|
1021
1117
|
// Finalize: remove draft.json if it exists (draft → final)
|
|
1022
1118
|
const draftPath = join(targetDir, "draft.json");
|
|
1023
1119
|
if (existsSync(draftPath)) {
|