runcastle 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-PRGLHAVL.js → add-DD7G73HH.js} +27 -10
- package/dist/add-DD7G73HH.js.map +1 -0
- package/dist/{chunk-UCYVC3H6.js → chunk-7CHGGMP7.js} +3 -38
- package/dist/chunk-7CHGGMP7.js.map +1 -0
- package/dist/chunk-GU4LRYYH.js +41 -0
- package/dist/chunk-GU4LRYYH.js.map +1 -0
- package/dist/{chunk-Y5P5BQWF.js → chunk-QTC5Y3LU.js} +25 -15
- package/dist/chunk-QTC5Y3LU.js.map +1 -0
- package/dist/chunk-UG25BZUN.js +59 -0
- package/dist/chunk-UG25BZUN.js.map +1 -0
- package/dist/{chunk-ZQ3BK4ZD.js → chunk-VWAIXSB6.js} +1 -57
- package/dist/chunk-VWAIXSB6.js.map +1 -0
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/{info-U6AZZ2TV.js → info-3PW6A6JA.js} +7 -5
- package/dist/{info-U6AZZ2TV.js.map → info-3PW6A6JA.js.map} +1 -1
- package/dist/run-M4XXNPGD.js +291 -0
- package/dist/run-M4XXNPGD.js.map +1 -0
- package/dist/{update-NIIUPL2E.js → update-UCT4K6IQ.js} +6 -4
- package/dist/{update-NIIUPL2E.js.map → update-UCT4K6IQ.js.map} +1 -1
- package/package.json +1 -1
- package/dist/add-PRGLHAVL.js.map +0 -1
- package/dist/chunk-UCYVC3H6.js.map +0 -1
- package/dist/chunk-Y5P5BQWF.js.map +0 -1
- package/dist/chunk-ZQ3BK4ZD.js.map +0 -1
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
renderPreview
|
|
5
|
-
} from "./chunk-ZQ3BK4ZD.js";
|
|
2
|
+
downloadFromRegistry
|
|
3
|
+
} from "./chunk-7CHGGMP7.js";
|
|
6
4
|
import {
|
|
7
|
-
downloadFromRegistry,
|
|
8
5
|
writeLockEntry
|
|
9
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-GU4LRYYH.js";
|
|
7
|
+
import {
|
|
8
|
+
renderPreview
|
|
9
|
+
} from "./chunk-VWAIXSB6.js";
|
|
10
10
|
import {
|
|
11
|
+
ensurePayloadGitignore,
|
|
11
12
|
hasConflicts,
|
|
13
|
+
mapTarget,
|
|
12
14
|
planInstall,
|
|
13
15
|
postInstallNotes,
|
|
14
16
|
renderDiff,
|
|
15
17
|
sha256Hex,
|
|
16
18
|
toPreviewPlan,
|
|
17
19
|
writePlan
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-QTC5Y3LU.js";
|
|
21
|
+
import {
|
|
22
|
+
compatibilityNote,
|
|
23
|
+
detectSandcastle
|
|
24
|
+
} from "./chunk-UG25BZUN.js";
|
|
19
25
|
import {
|
|
20
26
|
RealGitHubClient
|
|
21
27
|
} from "./chunk-OJ356IMJ.js";
|
|
@@ -233,6 +239,13 @@ async function addCommand(spec, opts) {
|
|
|
233
239
|
if (!isJsonMode()) log(pc.dim("\n(dry run \u2014 nothing written)"));
|
|
234
240
|
return;
|
|
235
241
|
}
|
|
242
|
+
if (!interactive && !opts.yes && !isJsonMode()) {
|
|
243
|
+
throw new CliError("confirmation required \u2014 non-interactive installs must pass --yes", {
|
|
244
|
+
exitCode: 1,
|
|
245
|
+
code: "E_CONFIRM_REQUIRED",
|
|
246
|
+
details: [`re-run: runcastle add ${spec} --yes`]
|
|
247
|
+
});
|
|
248
|
+
}
|
|
236
249
|
const conflicts = plan.filter((p) => p.action === "overwrite");
|
|
237
250
|
if (interactive && conflicts.length > 0) {
|
|
238
251
|
const clack = await import("@clack/prompts");
|
|
@@ -255,6 +268,7 @@ async function addCommand(spec, opts) {
|
|
|
255
268
|
}
|
|
256
269
|
}
|
|
257
270
|
await writePlan(cwd, plan);
|
|
271
|
+
await ensurePayloadGitignore(cwd, installDir);
|
|
258
272
|
const filesRecord = {};
|
|
259
273
|
for (const p of plan) filesRecord[p.target] = p.sha256;
|
|
260
274
|
await writeLockEntry(cwd, {
|
|
@@ -266,7 +280,10 @@ async function addCommand(spec, opts) {
|
|
|
266
280
|
...resolved.registry ? { registry: resolved.registry.baseUrl } : {},
|
|
267
281
|
dir: installDir,
|
|
268
282
|
files: filesRecord,
|
|
269
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
283
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
284
|
+
entrypoint: mapTarget(resolved.manifest.sandcastle.entrypoint, installDir),
|
|
285
|
+
requiredEnv: resolved.manifest.env.filter((e) => e.required).map((e) => e.name),
|
|
286
|
+
sandboxProviders: resolved.manifest.sandboxProviders
|
|
270
287
|
});
|
|
271
288
|
if (!isJsonMode()) {
|
|
272
289
|
const written = plan.filter((p) => p.action !== "identical").length;
|
|
@@ -274,7 +291,7 @@ async function addCommand(spec, opts) {
|
|
|
274
291
|
log(pc.green(`\u2713 installed ${resolved.slug}@${resolved.version} (${written} file(s) into ${installDir}/)`));
|
|
275
292
|
log("");
|
|
276
293
|
log(pc.bold("Next steps:"));
|
|
277
|
-
for (const line of postInstallNotes(resolved.manifest, installDir)) log(` ${line}`);
|
|
294
|
+
for (const line of postInstallNotes(resolved.manifest, installDir, detected)) log(` ${line}`);
|
|
278
295
|
} else {
|
|
279
296
|
emitJson({
|
|
280
297
|
ok: true,
|
|
@@ -295,4 +312,4 @@ export {
|
|
|
295
312
|
addCommand,
|
|
296
313
|
parseAddSpec
|
|
297
314
|
};
|
|
298
|
-
//# sourceMappingURL=add-
|
|
315
|
+
//# sourceMappingURL=add-DD7G73HH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/add.ts","../src/lib/install-id.ts"],"sourcesContent":["// `runcastle add <author/slug>[@version]` and `add github:owner/repo[/dir][@sha]`\n// (spec §5.4). The critical path. Order is exact: resolve → download → verify\n// checksums → preview → confirm → write → post-install → lockfile → ping.\n\nimport path from \"node:path\";\nimport {\n parseManifest,\n PAYLOAD_DIR,\n type Manifest,\n type WorkflowRecord,\n} from \"@castellan/schema\";\nimport { CliError } from \"../lib/errors.js\";\nimport { emitJson, isJsonMode, log, pc } from \"../lib/output.js\";\nimport { resolveRegistry } from \"../lib/config.js\";\nimport { RegistryClient } from \"../lib/registry.js\";\nimport { RealGitHubClient, type GitHubClient } from \"../lib/github.js\";\nimport { downloadFromRegistry } from \"../lib/download.js\";\nimport { sha256Hex } from \"../lib/checksum.js\";\nimport { getFetch, type FetchLike } from \"../lib/http.js\";\nimport { detectSandcastle, compatibilityNote } from \"../lib/sandcastle-version.js\";\nimport { renderPreview } from \"../lib/preview.js\";\nimport {\n planInstall,\n hasConflicts,\n toPreviewPlan,\n renderDiff,\n writePlan,\n mapTarget,\n ensurePayloadGitignore,\n postInstallNotes,\n type PlannedFile,\n} from \"../lib/installer.js\";\nimport { writeLockEntry } from \"../lib/lockfile.js\";\nimport { getInstallId } from \"../lib/install-id.js\";\n\nexport interface AddOptions {\n dir?: string;\n dryRun?: boolean;\n yes?: boolean;\n json?: boolean;\n registry?: string;\n /** Test seams. */\n fetchImpl?: FetchLike;\n github?: GitHubClient;\n cwd?: string;\n}\n\ninterface ResolvedInstall {\n manifest: Manifest;\n author: string;\n slug: string;\n version: string;\n commitSha: string;\n files: { path: string; bytes: Uint8Array; sha256: string }[];\n githubMode: boolean;\n source: string;\n registry?: RegistryClient;\n}\n\n// --- Spec parsing ------------------------------------------------------------\n\ninterface ParsedRef {\n githubMode: boolean;\n author?: string;\n slug?: string;\n version?: string;\n owner?: string;\n repo?: string;\n directory?: string;\n ref?: string;\n}\n\nexport function parseAddSpec(spec: string): ParsedRef {\n if (spec.startsWith(\"github:\")) {\n const rest = spec.slice(\"github:\".length);\n const atIdx = rest.indexOf(\"@\");\n const pathPart = atIdx === -1 ? rest : rest.slice(0, atIdx);\n const ref = atIdx === -1 ? \"\" : rest.slice(atIdx + 1);\n const segs = pathPart.split(\"/\").filter(Boolean);\n if (segs.length < 2) {\n throw new CliError(`invalid github: spec \"${spec}\" — expected github:owner/repo[/dir][@sha]`, {\n code: \"E_BAD_SPEC\",\n });\n }\n const owner = segs[0]!;\n const repo = segs[1]!;\n const dirSegs = segs.slice(2);\n return {\n githubMode: true,\n owner,\n repo,\n ...(dirSegs.length ? { directory: dirSegs.join(\"/\") } : {}),\n ...(ref ? { ref } : {}),\n };\n }\n const atIdx = spec.indexOf(\"@\");\n const namepart = atIdx === -1 ? spec : spec.slice(0, atIdx);\n const version = atIdx === -1 ? \"\" : spec.slice(atIdx + 1);\n const segs = namepart.split(\"/\").filter(Boolean);\n if (segs.length !== 2) {\n throw new CliError(`invalid spec \"${spec}\" — expected author/slug[@version] or github:owner/repo[/dir][@sha]`, {\n code: \"E_BAD_SPEC\",\n });\n }\n return {\n githubMode: false,\n author: segs[0]!,\n slug: segs[1]!,\n ...(version ? { version } : {}),\n };\n}\n\n// --- Resolution --------------------------------------------------------------\n\nasync function resolveFromRegistry(\n parsed: ParsedRef,\n registry: RegistryClient,\n github: GitHubClient,\n fetchImpl: FetchLike,\n): Promise<ResolvedInstall> {\n const record: WorkflowRecord = await registry.getWorkflow(parsed.author!, parsed.slug!, parsed.version);\n const downloaded = await downloadFromRegistry(record, registry, github, fetchImpl);\n return {\n manifest: record.manifest,\n author: record.author,\n slug: record.slug,\n version: record.version,\n commitSha: record.commitSha,\n files: downloaded.map((d) => ({ path: d.path, bytes: d.bytes, sha256: d.sha256 })),\n githubMode: false,\n source: `${record.author}/${record.slug}`,\n registry,\n };\n}\n\nasync function resolveFromGitHub(\n parsed: ParsedRef,\n github: GitHubClient,\n): Promise<ResolvedInstall> {\n const { owner, repo } = parsed;\n const dirPrefix = parsed.directory ? `${parsed.directory.replace(/\\/+$/, \"\")}/` : \"\";\n const sha = await github.resolveRef(owner!, repo!, parsed.ref ?? \"HEAD\");\n\n // Fetch and parse the manifest at the pinned SHA.\n const manifestBytes = await github.fetchRawFile(owner!, repo!, sha, `${dirPrefix}workflow.json`);\n let manifest: Manifest;\n try {\n const parsedManifest = parseManifest(JSON.parse(new TextDecoder().decode(manifestBytes)));\n if (!parsedManifest.ok) {\n throw new CliError(`workflow.json at ${owner}/${repo}@${sha.slice(0, 7)} is invalid`, {\n code: \"E_SCHEMA_INVALID\",\n details: parsedManifest.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`),\n });\n }\n manifest = parsedManifest.manifest;\n } catch (e) {\n if (e instanceof CliError) throw e;\n throw new CliError(`could not parse workflow.json from ${owner}/${repo}`, {\n code: \"E_SCHEMA_INVALID\",\n details: [(e as Error).message],\n });\n }\n\n // Fetch every listed file and compute checksums CLIENT-SIDE (M1: no tamper\n // evidence — surfaced in the preview).\n const files: ResolvedInstall[\"files\"] = [];\n for (const rel of manifest.files) {\n const bytes = await github.fetchRawFile(owner!, repo!, sha, `${dirPrefix}${rel}`);\n files.push({ path: rel, bytes, sha256: sha256Hex(bytes) });\n }\n\n return {\n manifest,\n author: `github:${owner}/${repo}`,\n slug: manifest.slug,\n version: manifest.version,\n commitSha: sha,\n files,\n githubMode: true,\n source: `github:${owner}/${repo}${parsed.directory ? `/${parsed.directory}` : \"\"}`,\n };\n}\n\n// --- Install-dir decision ----------------------------------------------------\n\nasync function decideInstallDir(\n projectRoot: string,\n slug: string,\n files: ResolvedInstall[\"files\"],\n opts: AddOptions,\n): Promise<{ installDir: string; namespaced: boolean }> {\n if (opts.dir) {\n return { installDir: `${PAYLOAD_DIR}/${opts.dir.replace(/^[./]+/, \"\")}`, namespaced: true };\n }\n // Default target: .sandcastle. Detect conflicts against existing files.\n const flatPlan = planInstall(projectRoot, files, PAYLOAD_DIR);\n if (!hasConflicts(flatPlan)) return { installDir: PAYLOAD_DIR, namespaced: false };\n\n // Conflict: default to a namespaced install (spec §11.4). Offer the choice\n // interactively; non-interactive falls back to the safe (namespaced) default.\n const namespacedDir = `${PAYLOAD_DIR}/${slug}`;\n const interactive = !opts.yes && !isJsonMode() && process.stdin.isTTY;\n if (!interactive) return { installDir: namespacedDir, namespaced: true };\n\n const clack = await import(\"@clack/prompts\");\n const choice = await clack.select({\n message: `Existing .sandcastle/ files would be overwritten. Install where?`,\n options: [\n { value: \"namespaced\", label: `Namespaced — ${namespacedDir}/ (recommended)` },\n { value: \"overwrite\", label: `Overwrite files in ${PAYLOAD_DIR}/` },\n ],\n initialValue: \"namespaced\",\n });\n if (clack.isCancel(choice)) throw new CliError(\"\", { exitCode: 1, code: \"E_CANCELLED\" });\n return choice === \"overwrite\"\n ? { installDir: PAYLOAD_DIR, namespaced: false }\n : { installDir: namespacedDir, namespaced: true };\n}\n\n// --- Command -----------------------------------------------------------------\n\nexport async function addCommand(spec: string, opts: AddOptions): Promise<void> {\n const cwd = opts.cwd ?? process.cwd();\n const fetchImpl = getFetch(opts.fetchImpl);\n const github = opts.github ?? new RealGitHubClient({ fetchImpl });\n const parsed = parseAddSpec(spec);\n\n // 1–2. Resolve + download + verify.\n let resolved: ResolvedInstall;\n if (parsed.githubMode) {\n resolved = await resolveFromGitHub(parsed, github);\n } else {\n const registry = new RegistryClient({ baseUrl: resolveRegistry(opts.registry), fetchImpl });\n resolved = await resolveFromRegistry(parsed, registry, github, fetchImpl);\n }\n\n // Install-dir decision (conflict → namespaced default).\n const { installDir } = await decideInstallDir(cwd, resolved.slug, resolved.files, opts);\n const plan = planInstall(cwd, resolved.files, installDir);\n\n // 3. Preview.\n const detected = await detectSandcastle(cwd);\n const note = compatibilityNote(resolved.manifest.sandcastle.versionRange, detected);\n const previewText = renderPreview({\n manifest: resolved.manifest,\n author: resolved.author,\n version: resolved.version,\n compatibilityNote: note,\n filePlan: toPreviewPlan(plan),\n installDir,\n githubMode: resolved.githubMode,\n colorize: pc.isColorSupported && !isJsonMode(),\n });\n\n if (isJsonMode()) {\n // In --json mode we only write when explicitly told to (--yes) and not\n // dry-run. When we're not writing, emit the preview plan and stop; when we\n // are, stay silent here — a single install result is emitted after writing.\n if (opts.dryRun || !opts.yes) {\n emitJson({\n ok: true,\n action: \"preview\",\n author: resolved.author,\n slug: resolved.slug,\n version: resolved.version,\n commitSha: resolved.commitSha,\n installDir,\n githubMode: resolved.githubMode,\n compatibilityNote: note ?? null,\n disclosures: resolved.manifest.disclosures,\n env: resolved.manifest.env,\n files: plan.map((p) => ({ target: p.target, action: p.action, sha256: p.sha256 })),\n });\n return;\n }\n } else {\n log(previewText);\n }\n\n // 3e. Diffs on conflict (interactive, non-json).\n const interactive = !opts.yes && !isJsonMode() && process.stdin.isTTY;\n if (opts.dryRun) {\n if (!isJsonMode()) log(pc.dim(\"\\n(dry run — nothing written)\"));\n return;\n }\n\n // Non-interactive without an explicit --yes writes nothing: the preview's\n // confirm step is a documented security gate, and a piped/scripted invocation\n // must opt in rather than silently inherit consent (spec §5.4.4).\n if (!interactive && !opts.yes && !isJsonMode()) {\n throw new CliError(\"confirmation required — non-interactive installs must pass --yes\", {\n exitCode: 1,\n code: \"E_CONFIRM_REQUIRED\",\n details: [`re-run: runcastle add ${spec} --yes`],\n });\n }\n\n const conflicts = plan.filter((p) => p.action === \"overwrite\");\n if (interactive && conflicts.length > 0) {\n const clack = await import(\"@clack/prompts\");\n const showDiff = await clack.confirm({\n message: `${conflicts.length} file(s) will be overwritten. Show diffs?`,\n initialValue: false,\n });\n if (!clack.isCancel(showDiff) && showDiff) {\n for (const p of conflicts) {\n log(\"\");\n log(renderDiff(cwd, p));\n }\n }\n }\n\n // 3/4. Confirm + write.\n if (interactive) {\n const clack = await import(\"@clack/prompts\");\n const ok = await clack.confirm({ message: `Write ${plan.filter((p) => p.action !== \"identical\").length} file(s)?`, initialValue: true });\n if (clack.isCancel(ok) || !ok) {\n throw new CliError(\"aborted — nothing written\", { exitCode: 1, code: \"E_ABORTED\" });\n }\n }\n\n await writePlan(cwd, plan);\n await ensurePayloadGitignore(cwd, installDir);\n\n // 6. Lockfile.\n const filesRecord: Record<string, string> = {};\n for (const p of plan) filesRecord[p.target] = p.sha256;\n await writeLockEntry(cwd, {\n source: resolved.source,\n slug: resolved.slug,\n ...(resolved.githubMode ? {} : { author: resolved.author }),\n version: resolved.version,\n sha: resolved.commitSha,\n ...(resolved.registry ? { registry: resolved.registry.baseUrl } : {}),\n dir: installDir,\n files: filesRecord,\n installedAt: new Date().toISOString(),\n entrypoint: mapTarget(resolved.manifest.sandcastle.entrypoint, installDir),\n requiredEnv: resolved.manifest.env.filter((e) => e.required).map((e) => e.name),\n sandboxProviders: resolved.manifest.sandboxProviders,\n });\n\n // 5. Post-install output.\n if (!isJsonMode()) {\n const written = plan.filter((p) => p.action !== \"identical\").length;\n log(\"\");\n log(pc.green(`✓ installed ${resolved.slug}@${resolved.version} (${written} file(s) into ${installDir}/)`));\n log(\"\");\n log(pc.bold(\"Next steps:\"));\n for (const line of postInstallNotes(resolved.manifest, installDir, detected)) log(` ${line}`);\n } else {\n emitJson({\n ok: true,\n action: \"install\",\n slug: resolved.slug,\n version: resolved.version,\n installDir,\n written: plan.filter((p) => p.action !== \"identical\").map((p) => p.target),\n });\n }\n\n // 6. Download ping (registry only, fire-and-forget).\n if (resolved.registry) {\n const anonId = await getInstallId();\n void resolved.registry.ping(resolved.author, resolved.slug, anonId).catch(() => {});\n }\n}\n","// A persistent random anonymous install id for download pings (spec §5.4.6 —\n// \"no PII beyond a random anonymous install id\"; dedupe by id per day). Stored\n// alongside the auth token; generated once on first use.\n\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { configDir } from \"./config.js\";\n\nfunction idPath(): string {\n return path.join(configDir(), \"install-id\");\n}\n\nexport async function getInstallId(): Promise<string> {\n const file = idPath();\n try {\n const existing = (await fsp.readFile(file, \"utf8\")).trim();\n if (existing) return existing;\n } catch {\n /* generate below */\n }\n const id = randomUUID();\n try {\n await fsp.mkdir(configDir(), { recursive: true });\n await fsp.writeFile(file, id, \"utf8\");\n } catch {\n /* non-fatal — a transient id is fine for a ping */\n }\n return id;\n}\n"],"mappings":"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","names":["atIdx","segs"]}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sha256Hex
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QTC5Y3LU.js";
|
|
4
4
|
import {
|
|
5
5
|
doFetch
|
|
6
6
|
} from "./chunk-5VPKMXSV.js";
|
|
7
7
|
import {
|
|
8
|
-
LOCKFILE_NAME,
|
|
9
8
|
USER_AGENT
|
|
10
9
|
} from "./chunk-LP5MV76P.js";
|
|
11
|
-
import {
|
|
12
|
-
PAYLOAD_DIR
|
|
13
|
-
} from "./chunk-AHRTPV2C.js";
|
|
14
10
|
import {
|
|
15
11
|
CliError
|
|
16
12
|
} from "./chunk-6CWCR5SO.js";
|
|
@@ -71,38 +67,7 @@ async function downloadFromRegistry(record, registry, github, fetchImpl) {
|
|
|
71
67
|
return out;
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
// src/lib/lockfile.ts
|
|
75
|
-
import fsp from "fs/promises";
|
|
76
|
-
import path from "path";
|
|
77
|
-
function lockPath(projectRoot) {
|
|
78
|
-
return path.join(projectRoot, PAYLOAD_DIR, LOCKFILE_NAME);
|
|
79
|
-
}
|
|
80
|
-
async function readLockfile(projectRoot) {
|
|
81
|
-
try {
|
|
82
|
-
const raw = await fsp.readFile(lockPath(projectRoot), "utf8");
|
|
83
|
-
const parsed = JSON.parse(raw);
|
|
84
|
-
if (parsed && Array.isArray(parsed.installs)) {
|
|
85
|
-
return { lockfileVersion: 1, installs: parsed.installs };
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
}
|
|
89
|
-
return { lockfileVersion: 1, installs: [] };
|
|
90
|
-
}
|
|
91
|
-
async function writeLockEntry(projectRoot, entry) {
|
|
92
|
-
const lock = await readLockfile(projectRoot);
|
|
93
|
-
const idx = lock.installs.findIndex((i) => i.dir === entry.dir);
|
|
94
|
-
if (idx >= 0) lock.installs[idx] = entry;
|
|
95
|
-
else lock.installs.push(entry);
|
|
96
|
-
lock.installs.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
97
|
-
const file = lockPath(projectRoot);
|
|
98
|
-
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
99
|
-
await fsp.writeFile(file, `${JSON.stringify(lock, null, 2)}
|
|
100
|
-
`, "utf8");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
70
|
export {
|
|
104
|
-
downloadFromRegistry
|
|
105
|
-
readLockfile,
|
|
106
|
-
writeLockEntry
|
|
71
|
+
downloadFromRegistry
|
|
107
72
|
};
|
|
108
|
-
//# sourceMappingURL=chunk-
|
|
73
|
+
//# sourceMappingURL=chunk-7CHGGMP7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/download.ts"],"sourcesContent":["// Download + verify (spec §5.4.2). Registry cache endpoint first, then\n// raw.githubusercontent.com fallback. EVERY file's SHA-256 is verified against\n// the registry's publish-time checksum; a mismatch is a security-flavored abort.\n\nimport {\n type FileChecksum,\n type WorkflowRecord,\n} from \"@castellan/schema\";\nimport { CliError } from \"./errors.js\";\nimport { sha256Hex } from \"./checksum.js\";\nimport { doFetch, type FetchLike } from \"./http.js\";\nimport { USER_AGENT } from \"./config.js\";\nimport type { RegistryClient } from \"./registry.js\";\nimport type { GitHubClient } from \"./github.js\";\n\n/** A downloaded file: raw bytes keyed later by its package-relative path. */\nexport interface DownloadedFile {\n path: string;\n bytes: Uint8Array;\n sha256: string;\n /** Where the bytes came from, for transparency. */\n source: \"registry\" | \"github\";\n}\n\n/**\n * Fetch bytes from the registry cache endpoint. A `410 Gone` is a MODERATION\n * signal (blocked/removed workflow, §8.7): it hard-aborts and must NOT fall\n * back to GitHub. Any other failure (404/5xx/network) returns null so the\n * caller can try the GitHub raw mirror.\n */\nasync function fetchFromRegistry(\n fetchImpl: FetchLike,\n url: string,\n filePath: string,\n): Promise<Uint8Array | null> {\n const res = await doFetch(fetchImpl, url, { headers: { \"user-agent\": USER_AGENT } });\n if (res.status === 410) {\n throw new CliError(\n `${filePath} has been withdrawn by the registry (410) — this workflow was blocked or removed for policy reasons; refusing to fall back to GitHub`,\n { code: \"E_GONE\", details: [url] },\n );\n }\n if (!res.ok) return null;\n return new Uint8Array(await res.arrayBuffer());\n}\n\n/**\n * Download and verify every file in a resolved registry record. Owner/repo are\n * parsed from `record.repo`. On any checksum mismatch, aborts immediately with a\n * security-flavored error naming the file and both digests.\n */\nexport async function downloadFromRegistry(\n record: WorkflowRecord,\n registry: RegistryClient,\n github: GitHubClient,\n fetchImpl: FetchLike,\n): Promise<DownloadedFile[]> {\n const [owner, repo] = record.repo.split(\"/\");\n const dirPrefix = record.directory ? `${record.directory.replace(/\\/+$/, \"\")}/` : \"\";\n const out: DownloadedFile[] = [];\n\n for (const file of record.files as FileChecksum[]) {\n let bytes: Uint8Array | null = null;\n let source: DownloadedFile[\"source\"] = \"registry\";\n const urlsTried: string[] = [];\n\n // 1) Registry cache endpoint (downloadUrl if provided, else constructed).\n const cacheUrl = file.downloadUrl ?? registry.fileUrl(record.author, record.slug, record.version, file.path);\n urlsTried.push(cacheUrl);\n bytes = await fetchFromRegistry(fetchImpl, cacheUrl, file.path);\n\n // 2) raw.githubusercontent.com fallback at the pinned SHA.\n if (!bytes && owner && repo) {\n source = \"github\";\n try {\n bytes = await github.fetchRawFile(owner, repo, record.commitSha, `${dirPrefix}${file.path}`);\n } catch (e) {\n urlsTried.push((e as CliError).message);\n }\n }\n\n if (!bytes) {\n throw new CliError(`could not download ${file.path} from any source`, {\n code: \"E_DOWNLOAD\",\n details: urlsTried,\n });\n }\n\n const digest = sha256Hex(bytes);\n if (digest !== file.sha256) {\n throw new CliError(\n `checksum mismatch for ${file.path} — refusing to write (possible tampering)`,\n {\n code: \"E_CHECKSUM\",\n details: [\n `expected sha256: ${file.sha256}`,\n `actual sha256: ${digest}`,\n `source: ${source}`,\n ],\n },\n );\n }\n\n out.push({ path: file.path, bytes, sha256: digest, source });\n }\n\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;AA8BA,eAAe,kBACb,WACA,KACA,UAC4B;AAC5B,QAAM,MAAM,MAAM,QAAQ,WAAW,KAAK,EAAE,SAAS,EAAE,cAAc,WAAW,EAAE,CAAC;AACnF,MAAI,IAAI,WAAW,KAAK;AACtB,UAAM,IAAI;AAAA,MACR,GAAG,QAAQ;AAAA,MACX,EAAE,MAAM,UAAU,SAAS,CAAC,GAAG,EAAE;AAAA,IACnC;AAAA,EACF;AACA,MAAI,CAAC,IAAI,GAAI,QAAO;AACpB,SAAO,IAAI,WAAW,MAAM,IAAI,YAAY,CAAC;AAC/C;AAOA,eAAsB,qBACpB,QACA,UACA,QACA,WAC2B;AAC3B,QAAM,CAAC,OAAO,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG;AAC3C,QAAM,YAAY,OAAO,YAAY,GAAG,OAAO,UAAU,QAAQ,QAAQ,EAAE,CAAC,MAAM;AAClF,QAAM,MAAwB,CAAC;AAE/B,aAAW,QAAQ,OAAO,OAAyB;AACjD,QAAI,QAA2B;AAC/B,QAAI,SAAmC;AACvC,UAAM,YAAsB,CAAC;AAG7B,UAAM,WAAW,KAAK,eAAe,SAAS,QAAQ,OAAO,QAAQ,OAAO,MAAM,OAAO,SAAS,KAAK,IAAI;AAC3G,cAAU,KAAK,QAAQ;AACvB,YAAQ,MAAM,kBAAkB,WAAW,UAAU,KAAK,IAAI;AAG9D,QAAI,CAAC,SAAS,SAAS,MAAM;AAC3B,eAAS;AACT,UAAI;AACF,gBAAQ,MAAM,OAAO,aAAa,OAAO,MAAM,OAAO,WAAW,GAAG,SAAS,GAAG,KAAK,IAAI,EAAE;AAAA,MAC7F,SAAS,GAAG;AACV,kBAAU,KAAM,EAAe,OAAO;AAAA,MACxC;AAAA,IACF;AAEA,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,sBAAsB,KAAK,IAAI,oBAAoB;AAAA,QACpE,MAAM;AAAA,QACN,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAEA,UAAM,SAAS,UAAU,KAAK;AAC9B,QAAI,WAAW,KAAK,QAAQ;AAC1B,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,QAClC;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,YACP,oBAAoB,KAAK,MAAM;AAAA,YAC/B,oBAAoB,MAAM;AAAA,YAC1B,WAAW,MAAM;AAAA,UACnB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,KAAK,EAAE,MAAM,KAAK,MAAM,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAAA,EAC7D;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOCKFILE_NAME
|
|
3
|
+
} from "./chunk-LP5MV76P.js";
|
|
4
|
+
import {
|
|
5
|
+
PAYLOAD_DIR
|
|
6
|
+
} from "./chunk-AHRTPV2C.js";
|
|
7
|
+
|
|
8
|
+
// src/lib/lockfile.ts
|
|
9
|
+
import fsp from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
function lockPath(projectRoot) {
|
|
12
|
+
return path.join(projectRoot, PAYLOAD_DIR, LOCKFILE_NAME);
|
|
13
|
+
}
|
|
14
|
+
async function readLockfile(projectRoot) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await fsp.readFile(lockPath(projectRoot), "utf8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (parsed && Array.isArray(parsed.installs)) {
|
|
19
|
+
return { lockfileVersion: 1, installs: parsed.installs };
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return { lockfileVersion: 1, installs: [] };
|
|
24
|
+
}
|
|
25
|
+
async function writeLockEntry(projectRoot, entry) {
|
|
26
|
+
const lock = await readLockfile(projectRoot);
|
|
27
|
+
const idx = lock.installs.findIndex((i) => i.dir === entry.dir);
|
|
28
|
+
if (idx >= 0) lock.installs[idx] = entry;
|
|
29
|
+
else lock.installs.push(entry);
|
|
30
|
+
lock.installs.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
31
|
+
const file = lockPath(projectRoot);
|
|
32
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
33
|
+
await fsp.writeFile(file, `${JSON.stringify(lock, null, 2)}
|
|
34
|
+
`, "utf8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
readLockfile,
|
|
39
|
+
writeLockEntry
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=chunk-GU4LRYYH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/lockfile.ts"],"sourcesContent":["// The install lockfile (spec §5.4.6): `.sandcastle/runcastle.lock.json`. Records\n// what was installed so `runcastle update` can diff cleanly. Supports multiple\n// installs (namespaced) keyed by their install directory.\n\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { PAYLOAD_DIR, LOCKFILE_NAME } from \"./config.js\";\n\nexport interface LockEntry {\n /** \"author/slug\" for registry installs, or \"github:owner/repo[/dir]\" for github: installs. */\n source: string;\n slug: string;\n author?: string;\n version: string;\n sha: string;\n registry?: string;\n /** Install directory relative to project root (e.g. \".sandcastle\" or \".sandcastle/<slug>\"). */\n dir: string;\n /** Installed target path (relative to project root) → sha256 of the written bytes. */\n files: Record<string, string>;\n installedAt: string;\n /** Entrypoint target path (relative to project root) — lets `run` skip guessing. */\n entrypoint?: string;\n /** Names of env vars the manifest declares as required. */\n requiredEnv?: string[];\n /** Sandbox providers the manifest declares (e.g. [\"docker\", \"podman\"]). */\n sandboxProviders?: string[];\n}\n\nexport interface Lockfile {\n lockfileVersion: 1;\n installs: LockEntry[];\n}\n\nfunction lockPath(projectRoot: string): string {\n return path.join(projectRoot, PAYLOAD_DIR, LOCKFILE_NAME);\n}\n\nexport async function readLockfile(projectRoot: string): Promise<Lockfile> {\n try {\n const raw = await fsp.readFile(lockPath(projectRoot), \"utf8\");\n const parsed = JSON.parse(raw) as Partial<Lockfile>;\n if (parsed && Array.isArray(parsed.installs)) {\n return { lockfileVersion: 1, installs: parsed.installs };\n }\n } catch {\n /* absent or unreadable → empty */\n }\n return { lockfileVersion: 1, installs: [] };\n}\n\n/** Upsert an install entry (keyed by install `dir`) and persist. */\nexport async function writeLockEntry(projectRoot: string, entry: LockEntry): Promise<void> {\n const lock = await readLockfile(projectRoot);\n const idx = lock.installs.findIndex((i) => i.dir === entry.dir);\n if (idx >= 0) lock.installs[idx] = entry;\n else lock.installs.push(entry);\n lock.installs.sort((a, b) => a.dir.localeCompare(b.dir));\n const file = lockPath(projectRoot);\n await fsp.mkdir(path.dirname(file), { recursive: true });\n await fsp.writeFile(file, `${JSON.stringify(lock, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":";;;;;;;;AAIA,OAAO,SAAS;AAChB,OAAO,UAAU;AA6BjB,SAAS,SAAS,aAA6B;AAC7C,SAAO,KAAK,KAAK,aAAa,aAAa,aAAa;AAC1D;AAEA,eAAsB,aAAa,aAAwC;AACzE,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,SAAS,SAAS,WAAW,GAAG,MAAM;AAC5D,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,MAAM,QAAQ,OAAO,QAAQ,GAAG;AAC5C,aAAO,EAAE,iBAAiB,GAAG,UAAU,OAAO,SAAS;AAAA,IACzD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,EAAE,iBAAiB,GAAG,UAAU,CAAC,EAAE;AAC5C;AAGA,eAAsB,eAAe,aAAqB,OAAiC;AACzF,QAAM,OAAO,MAAM,aAAa,WAAW;AAC3C,QAAM,MAAM,KAAK,SAAS,UAAU,CAAC,MAAM,EAAE,QAAQ,MAAM,GAAG;AAC9D,MAAI,OAAO,EAAG,MAAK,SAAS,GAAG,IAAI;AAAA,MAC9B,MAAK,SAAS,KAAK,KAAK;AAC7B,OAAK,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;AACvD,QAAM,OAAO,SAAS,WAAW;AACjC,QAAM,IAAI,MAAM,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACvD,QAAM,IAAI,UAAU,MAAM,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACxE;","names":[]}
|
|
@@ -93,28 +93,37 @@ async function writePlan(projectRoot, plan) {
|
|
|
93
93
|
await fsp.writeFile(abs, p.bytes);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
function
|
|
96
|
+
async function ensurePayloadGitignore(projectRoot, installDir) {
|
|
97
|
+
const abs = resolveInsideRoot(projectRoot, `${installDir}/.gitignore`);
|
|
98
|
+
if (fs.existsSync(abs)) return;
|
|
99
|
+
await fsp.mkdir(path.dirname(abs), { recursive: true });
|
|
100
|
+
await fsp.writeFile(abs, "# added by runcastle: never commit secrets or run artifacts\n.env\nlogs/\nworktrees/\n", "utf8");
|
|
101
|
+
}
|
|
102
|
+
function postInstallNotes(manifest, installDir, detected) {
|
|
97
103
|
const out = [];
|
|
98
|
-
const entryRel = manifest.sandcastle.entrypoint.replace(new RegExp(`^${PAYLOAD_DIR}/`), "");
|
|
99
|
-
const entryTarget = `${installDir}/${entryRel}`;
|
|
100
104
|
const envTarget = `${installDir}/.env`;
|
|
105
|
+
const runArg = installDir === PAYLOAD_DIR ? "" : ` ${installDir}`;
|
|
106
|
+
let step = 1;
|
|
107
|
+
if (detected && !detected.version && !detected.range) {
|
|
108
|
+
out.push(`${step++}. Install the runtime: npm install -D @ai-hero/sandcastle tsx`);
|
|
109
|
+
out.push(` ${"(runcastle run offers to do this for you)"}`);
|
|
110
|
+
}
|
|
101
111
|
const requiredEnv = manifest.env.filter((e) => e.required).map((e) => e.name);
|
|
102
112
|
if (requiredEnv.length > 0) {
|
|
103
|
-
out.push(
|
|
113
|
+
out.push(`${step++}. Copy ${envTarget}.example to ${envTarget} and set: ${requiredEnv.join(", ")}`);
|
|
114
|
+
out.push(` (runcastle run creates the copy and tells you exactly what's missing)`);
|
|
104
115
|
} else {
|
|
105
|
-
out.push(
|
|
116
|
+
out.push(`${step++}. Review ${envTarget}.example for any env vars you need to set.`);
|
|
106
117
|
}
|
|
107
118
|
const providers = manifest.sandboxProviders.length > 0 ? manifest.sandboxProviders : ["docker"];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
119
|
+
if (providers.includes("docker") || providers.includes("podman")) {
|
|
120
|
+
out.push(`${step++}. Run it: npx runcastle run${runArg}`);
|
|
121
|
+
out.push(` (checks your setup, builds the sandbox image on first run, then executes)`);
|
|
122
|
+
} else if (providers.includes("vercel")) {
|
|
123
|
+
out.push(`${step++}. This workflow targets the Vercel sandbox \u2014 see the Sandcastle docs for setup.`);
|
|
124
|
+
} else {
|
|
125
|
+
out.push(`${step++}. This workflow uses a custom sandbox \u2014 see its README.`);
|
|
116
126
|
}
|
|
117
|
-
out.push(`3. Run it: npx tsx ${entryTarget}`);
|
|
118
127
|
if (manifest.postInstallNotes) {
|
|
119
128
|
out.push("");
|
|
120
129
|
out.push(`Author notes: ${manifest.postInstallNotes}`);
|
|
@@ -131,6 +140,7 @@ export {
|
|
|
131
140
|
toPreviewPlan,
|
|
132
141
|
renderDiff,
|
|
133
142
|
writePlan,
|
|
143
|
+
ensurePayloadGitignore,
|
|
134
144
|
postInstallNotes
|
|
135
145
|
};
|
|
136
|
-
//# sourceMappingURL=chunk-
|
|
146
|
+
//# sourceMappingURL=chunk-QTC5Y3LU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/checksum.ts","../src/lib/installer.ts"],"sourcesContent":["// SHA-256 checksums. The tamper-evidence anchor (spec §8.2): the CLI verifies\n// every downloaded file against the registry's publish-time checksum, regardless\n// of where the bytes came from.\n\nimport { createHash } from \"node:crypto\";\n\n/** Lowercase hex SHA-256 of raw bytes. */\nexport function sha256Hex(bytes: Uint8Array): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n","// Shared install mechanics for `add` (registry and github: modes): map package\n// paths to install targets (with namespacing), compute the create/overwrite plan,\n// render per-file diffs, write bytes byte-for-byte, and generate post-install\n// notes. Never executes anything (spec §8.1).\n\nimport fsp from \"node:fs/promises\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createTwoFilesPatch } from \"diff\";\nimport type { Manifest } from \"@castellan/schema\";\nimport { PAYLOAD_DIR } from \"./config.js\";\nimport { CliError } from \"./errors.js\";\nimport { sha256Hex } from \"./checksum.js\";\nimport type { FileAction, PreviewFilePlanEntry } from \"./preview.js\";\n\nexport interface PlannedFile {\n /** Package-relative source path (e.g. \".sandcastle/main.ts\"). */\n packagePath: string;\n /** Target path relative to project root (post-namespacing). */\n target: string;\n bytes: Uint8Array;\n sha256: string;\n action: FileAction;\n}\n\n/**\n * Resolve a project-relative target to an absolute path and assert it stays\n * inside `projectRoot` (defense-in-depth against a malicious manifest whose\n * `files`/entrypoint escape the payload dir — §8 / C1). Aborts the whole\n * install if the resolved path would land outside the project.\n */\nexport function resolveInsideRoot(projectRoot: string, target: string): string {\n const root = path.resolve(projectRoot);\n const abs = path.resolve(root, ...target.split(\"/\"));\n const prefix = root.endsWith(path.sep) ? root : root + path.sep;\n if (abs !== root && !abs.startsWith(prefix)) {\n throw new CliError(\n `refusing to write outside the project: \"${target}\" resolves to ${abs} (possible path-traversal in the workflow manifest)`,\n { code: \"E_UNSAFE_PATH\" },\n );\n }\n return abs;\n}\n\n/** Map a package-relative path into the chosen install directory. */\nexport function mapTarget(packagePath: string, installDir: string): string {\n const payloadPrefix = `${PAYLOAD_DIR}/`;\n if (installDir === PAYLOAD_DIR) return packagePath;\n if (packagePath === PAYLOAD_DIR) return installDir;\n if (packagePath.startsWith(payloadPrefix)) {\n return `${installDir}/${packagePath.slice(payloadPrefix.length)}`;\n }\n return packagePath;\n}\n\nfunction readExisting(projectRoot: string, target: string): Uint8Array | null {\n const abs = path.join(projectRoot, ...target.split(\"/\"));\n try {\n if (!fs.existsSync(abs)) return null;\n return new Uint8Array(fs.readFileSync(abs));\n } catch {\n return null;\n }\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/** Build the full install plan, resolving each file's action against disk. */\nexport function planInstall(\n projectRoot: string,\n files: { path: string; bytes: Uint8Array; sha256?: string }[],\n installDir: string,\n): PlannedFile[] {\n return files.map((f) => {\n const target = mapTarget(f.path, installDir);\n const existing = readExisting(projectRoot, target);\n let action: FileAction = \"create\";\n if (existing) action = bytesEqual(existing, f.bytes) ? \"identical\" : \"overwrite\";\n return {\n packagePath: f.path,\n target,\n bytes: f.bytes,\n sha256: f.sha256 ?? sha256Hex(f.bytes),\n action,\n };\n });\n}\n\n/** True when any planned file would overwrite a differing existing file. */\nexport function hasConflicts(plan: PlannedFile[]): boolean {\n return plan.some((p) => p.action === \"overwrite\");\n}\n\nexport function toPreviewPlan(plan: PlannedFile[]): PreviewFilePlanEntry[] {\n return plan.map((p) => ({ target: p.target, action: p.action }));\n}\n\nconst decoder = new TextDecoder(\"utf-8\", { fatal: false });\n\n/** A unified diff between the existing file and the incoming bytes (text only). */\nexport function renderDiff(projectRoot: string, planned: PlannedFile): string {\n const existing = readExisting(projectRoot, planned.target);\n const oldText = existing ? decoder.decode(existing) : \"\";\n const newText = decoder.decode(planned.bytes);\n return createTwoFilesPatch(\n `a/${planned.target}`,\n `b/${planned.target}`,\n oldText,\n newText,\n \"current\",\n \"incoming\",\n );\n}\n\n/** Write every planned file byte-for-byte, creating parent directories. */\nexport async function writePlan(projectRoot: string, plan: PlannedFile[]): Promise<void> {\n // Assert containment for EVERY target before writing a single byte, so a\n // traversal entry aborts the whole install atomically (§8 / C1).\n const targets = plan\n .filter((p) => p.action !== \"identical\")\n .map((p) => ({ p, abs: resolveInsideRoot(projectRoot, p.target) }));\n for (const { p, abs } of targets) {\n await fsp.mkdir(path.dirname(abs), { recursive: true });\n await fsp.writeFile(abs, p.bytes);\n }\n}\n\n/**\n * Ensure the install dir has a .gitignore covering secrets and run artifacts.\n * `sandcastle init` writes one; installed payloads usually don't ship one, and\n * without it the user's `.env` (holding a real token) is one `git add -A` away\n * from being committed. Never overwrites an author-provided file.\n */\nexport async function ensurePayloadGitignore(projectRoot: string, installDir: string): Promise<void> {\n const abs = resolveInsideRoot(projectRoot, `${installDir}/.gitignore`);\n if (fs.existsSync(abs)) return;\n await fsp.mkdir(path.dirname(abs), { recursive: true });\n await fsp.writeFile(abs, \"# added by runcastle: never commit secrets or run artifacts\\n.env\\nlogs/\\nworktrees/\\n\", \"utf8\");\n}\n\n/**\n * Generate the post-install output (spec §5.4.5). The happy path is exactly two\n * steps — fill in .env, then `runcastle run` (which installs the runtime,\n * builds the image, and executes). The runtime-install hint appears only when\n * sandcastle isn't already a dependency of the project.\n */\nexport function postInstallNotes(\n manifest: Manifest,\n installDir: string,\n detected?: { version?: string; range?: string },\n): string[] {\n const out: string[] = [];\n const envTarget = `${installDir}/.env`;\n const runArg = installDir === PAYLOAD_DIR ? \"\" : ` ${installDir}`;\n let step = 1;\n\n if (detected && !detected.version && !detected.range) {\n out.push(`${step++}. Install the runtime: npm install -D @ai-hero/sandcastle tsx`);\n out.push(` ${\"(runcastle run offers to do this for you)\"}`);\n }\n\n const requiredEnv = manifest.env.filter((e) => e.required).map((e) => e.name);\n if (requiredEnv.length > 0) {\n out.push(`${step++}. Copy ${envTarget}.example to ${envTarget} and set: ${requiredEnv.join(\", \")}`);\n out.push(` (runcastle run creates the copy and tells you exactly what's missing)`);\n } else {\n out.push(`${step++}. Review ${envTarget}.example for any env vars you need to set.`);\n }\n\n const providers = manifest.sandboxProviders.length > 0 ? manifest.sandboxProviders : [\"docker\"];\n if (providers.includes(\"docker\") || providers.includes(\"podman\")) {\n out.push(`${step++}. Run it: npx runcastle run${runArg}`);\n out.push(` (checks your setup, builds the sandbox image on first run, then executes)`);\n } else if (providers.includes(\"vercel\")) {\n out.push(`${step++}. This workflow targets the Vercel sandbox — see the Sandcastle docs for setup.`);\n } else {\n out.push(`${step++}. This workflow uses a custom sandbox — see its README.`);\n }\n\n if (manifest.postInstallNotes) {\n out.push(\"\");\n out.push(`Author notes: ${manifest.postInstallNotes}`);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SANDCASTLE_PACKAGE_NAME
|
|
3
|
+
} from "./chunk-AHRTPV2C.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/sandcastle-version.ts
|
|
6
|
+
import fsp from "fs/promises";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
async function installedVersion(cwd) {
|
|
10
|
+
const pkgJson = path.join(cwd, "node_modules", ...SANDCASTLE_PACKAGE_NAME.split("/"), "package.json");
|
|
11
|
+
try {
|
|
12
|
+
const raw = await fsp.readFile(pkgJson, "utf8");
|
|
13
|
+
const v = JSON.parse(raw).version;
|
|
14
|
+
if (v && semver.valid(v)) return v;
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
async function declaredRange(cwd) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fsp.readFile(path.join(cwd, "package.json"), "utf8");
|
|
22
|
+
const pkg = JSON.parse(raw);
|
|
23
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
24
|
+
const range = pkg[field]?.[SANDCASTLE_PACKAGE_NAME];
|
|
25
|
+
if (range) return range;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
async function detectSandcastle(cwd) {
|
|
32
|
+
const [version, range] = await Promise.all([installedVersion(cwd), declaredRange(cwd)]);
|
|
33
|
+
return {
|
|
34
|
+
...version ? { version } : {},
|
|
35
|
+
...range ? { range } : {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function compatibilityNote(workflowRange, detected) {
|
|
39
|
+
const { version, range } = detected;
|
|
40
|
+
if (version && semver.valid(version) && semver.validRange(workflowRange)) {
|
|
41
|
+
if (!semver.satisfies(version, workflowRange)) {
|
|
42
|
+
return `Your installed ${SANDCASTLE_PACKAGE_NAME} is ${version}, outside this workflow's tested range ${workflowRange}.`;
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
if (range && semver.validRange(range) && semver.validRange(workflowRange)) {
|
|
47
|
+
const min = semver.minVersion(range);
|
|
48
|
+
if (min && !semver.satisfies(min, workflowRange)) {
|
|
49
|
+
return `Your ${SANDCASTLE_PACKAGE_NAME} dependency (${range}) may fall outside this workflow's tested range ${workflowRange}.`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
detectSandcastle,
|
|
57
|
+
compatibilityNote
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=chunk-UG25BZUN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/sandcastle-version.ts"],"sourcesContent":["// Best-effort detection of the user's installed @ai-hero/sandcastle version so\n// `add` can warn when it falls outside the workflow's tested versionRange\n// (spec §5.4.3a). Reads (in order) the installed package's package.json, then\n// the consumer's own package.json dependency ranges.\n\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport semver from \"semver\";\nimport { SANDCASTLE_PACKAGE_NAME } from \"@castellan/schema\";\n\n/** Return the concrete installed version, if resolvable. */\nasync function installedVersion(cwd: string): Promise<string | undefined> {\n const pkgJson = path.join(cwd, \"node_modules\", ...SANDCASTLE_PACKAGE_NAME.split(\"/\"), \"package.json\");\n try {\n const raw = await fsp.readFile(pkgJson, \"utf8\");\n const v = (JSON.parse(raw) as { version?: string }).version;\n if (v && semver.valid(v)) return v;\n } catch {\n /* not installed */\n }\n return undefined;\n}\n\n/** Return the declared dependency range from the consumer's package.json. */\nasync function declaredRange(cwd: string): Promise<string | undefined> {\n try {\n const raw = await fsp.readFile(path.join(cwd, \"package.json\"), \"utf8\");\n const pkg = JSON.parse(raw) as Record<string, Record<string, string> | undefined>;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"] as const) {\n const range = pkg[field]?.[SANDCASTLE_PACKAGE_NAME];\n if (range) return range;\n }\n } catch {\n /* no package.json */\n }\n return undefined;\n}\n\nexport interface SandcastleDetection {\n /** Concrete installed version, if found in node_modules. */\n version?: string;\n /** Declared dependency range from package.json, if present. */\n range?: string;\n}\n\nexport async function detectSandcastle(cwd: string): Promise<SandcastleDetection> {\n const [version, range] = await Promise.all([installedVersion(cwd), declaredRange(cwd)]);\n return {\n ...(version ? { version } : {}),\n ...(range ? { range } : {}),\n };\n}\n\n/**\n * Decide whether the user's Sandcastle is compatible with a workflow's\n * versionRange. Returns a human note when there is a mismatch, else undefined.\n */\nexport function compatibilityNote(\n workflowRange: string,\n detected: SandcastleDetection,\n): string | undefined {\n const { version, range } = detected;\n if (version && semver.valid(version) && semver.validRange(workflowRange)) {\n if (!semver.satisfies(version, workflowRange)) {\n return `Your installed ${SANDCASTLE_PACKAGE_NAME} is ${version}, outside this workflow's tested range ${workflowRange}.`;\n }\n return undefined;\n }\n if (range && semver.validRange(range) && semver.validRange(workflowRange)) {\n // Compare ranges via their lowest satisfying versions — coarse but useful.\n const min = semver.minVersion(range);\n if (min && !semver.satisfies(min, workflowRange)) {\n return `Your ${SANDCASTLE_PACKAGE_NAME} dependency (${range}) may fall outside this workflow's tested range ${workflowRange}.`;\n }\n }\n return undefined;\n}\n"],"mappings":";;;;;AAKA,OAAO,SAAS;AAChB,OAAO,UAAU;AACjB,OAAO,YAAY;AAInB,eAAe,iBAAiB,KAA0C;AACxE,QAAM,UAAU,KAAK,KAAK,KAAK,gBAAgB,GAAG,wBAAwB,MAAM,GAAG,GAAG,cAAc;AACpG,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,SAAS,SAAS,MAAM;AAC9C,UAAM,IAAK,KAAK,MAAM,GAAG,EAA2B;AACpD,QAAI,KAAK,OAAO,MAAM,CAAC,EAAG,QAAO;AAAA,EACnC,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,eAAe,cAAc,KAA0C;AACrE,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,SAAS,KAAK,KAAK,KAAK,cAAc,GAAG,MAAM;AACrE,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,eAAW,SAAS,CAAC,gBAAgB,mBAAmB,kBAAkB,GAAY;AACpF,YAAM,QAAQ,IAAI,KAAK,IAAI,uBAAuB;AAClD,UAAI,MAAO,QAAO;AAAA,IACpB;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AASA,eAAsB,iBAAiB,KAA2C;AAChF,QAAM,CAAC,SAAS,KAAK,IAAI,MAAM,QAAQ,IAAI,CAAC,iBAAiB,GAAG,GAAG,cAAc,GAAG,CAAC,CAAC;AACtF,SAAO;AAAA,IACL,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,EAC3B;AACF;AAMO,SAAS,kBACd,eACA,UACoB;AACpB,QAAM,EAAE,SAAS,MAAM,IAAI;AAC3B,MAAI,WAAW,OAAO,MAAM,OAAO,KAAK,OAAO,WAAW,aAAa,GAAG;AACxE,QAAI,CAAC,OAAO,UAAU,SAAS,aAAa,GAAG;AAC7C,aAAO,kBAAkB,uBAAuB,OAAO,OAAO,0CAA0C,aAAa;AAAA,IACvH;AACA,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAO,WAAW,KAAK,KAAK,OAAO,WAAW,aAAa,GAAG;AAEzE,UAAM,MAAM,OAAO,WAAW,KAAK;AACnC,QAAI,OAAO,CAAC,OAAO,UAAU,KAAK,aAAa,GAAG;AAChD,aAAO,QAAQ,uBAAuB,gBAAgB,KAAK,mDAAmD,aAAa;AAAA,IAC7H;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1,57 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SANDCASTLE_PACKAGE_NAME
|
|
3
|
-
} from "./chunk-AHRTPV2C.js";
|
|
4
|
-
|
|
5
|
-
// src/lib/sandcastle-version.ts
|
|
6
|
-
import fsp from "fs/promises";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import semver from "semver";
|
|
9
|
-
async function installedVersion(cwd) {
|
|
10
|
-
const pkgJson = path.join(cwd, "node_modules", ...SANDCASTLE_PACKAGE_NAME.split("/"), "package.json");
|
|
11
|
-
try {
|
|
12
|
-
const raw = await fsp.readFile(pkgJson, "utf8");
|
|
13
|
-
const v = JSON.parse(raw).version;
|
|
14
|
-
if (v && semver.valid(v)) return v;
|
|
15
|
-
} catch {
|
|
16
|
-
}
|
|
17
|
-
return void 0;
|
|
18
|
-
}
|
|
19
|
-
async function declaredRange(cwd) {
|
|
20
|
-
try {
|
|
21
|
-
const raw = await fsp.readFile(path.join(cwd, "package.json"), "utf8");
|
|
22
|
-
const pkg = JSON.parse(raw);
|
|
23
|
-
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
24
|
-
const range = pkg[field]?.[SANDCASTLE_PACKAGE_NAME];
|
|
25
|
-
if (range) return range;
|
|
26
|
-
}
|
|
27
|
-
} catch {
|
|
28
|
-
}
|
|
29
|
-
return void 0;
|
|
30
|
-
}
|
|
31
|
-
async function detectSandcastle(cwd) {
|
|
32
|
-
const [version, range] = await Promise.all([installedVersion(cwd), declaredRange(cwd)]);
|
|
33
|
-
return {
|
|
34
|
-
...version ? { version } : {},
|
|
35
|
-
...range ? { range } : {}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function compatibilityNote(workflowRange, detected) {
|
|
39
|
-
const { version, range } = detected;
|
|
40
|
-
if (version && semver.valid(version) && semver.validRange(workflowRange)) {
|
|
41
|
-
if (!semver.satisfies(version, workflowRange)) {
|
|
42
|
-
return `Your installed ${SANDCASTLE_PACKAGE_NAME} is ${version}, outside this workflow's tested range ${workflowRange}.`;
|
|
43
|
-
}
|
|
44
|
-
return void 0;
|
|
45
|
-
}
|
|
46
|
-
if (range && semver.validRange(range) && semver.validRange(workflowRange)) {
|
|
47
|
-
const min = semver.minVersion(range);
|
|
48
|
-
if (min && !semver.satisfies(min, workflowRange)) {
|
|
49
|
-
return `Your ${SANDCASTLE_PACKAGE_NAME} dependency (${range}) may fall outside this workflow's tested range ${workflowRange}.`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return void 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
1
|
// src/lib/preview.ts
|
|
56
2
|
import pc from "picocolors";
|
|
57
3
|
function pad(s, width) {
|
|
@@ -127,8 +73,6 @@ function renderPreview(input) {
|
|
|
127
73
|
}
|
|
128
74
|
|
|
129
75
|
export {
|
|
130
|
-
detectSandcastle,
|
|
131
|
-
compatibilityNote,
|
|
132
76
|
renderPreview
|
|
133
77
|
};
|
|
134
|
-
//# sourceMappingURL=chunk-
|
|
78
|
+
//# sourceMappingURL=chunk-VWAIXSB6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/preview.ts"],"sourcesContent":["// The install preview (spec §5.4.3) — a SECURITY SURFACE. Snapshot-tested;\n// changes require review. Pure and deterministic: given a manifest and a file\n// plan, it returns the exact text shown before any bytes are written. Color is\n// applied only when `colorize` is true so snapshots stay stable.\n\nimport type { Manifest } from \"@castellan/schema\";\nimport pc from \"picocolors\";\n\nexport type FileAction = \"create\" | \"overwrite\" | \"identical\";\n\nexport interface PreviewFilePlanEntry {\n /** Target path relative to the project root (post-namespacing). */\n target: string;\n action: FileAction;\n}\n\nexport interface PreviewInput {\n manifest: Manifest;\n author: string;\n version: string;\n /** Compatibility warning about the user's installed Sandcastle, if any. */\n compatibilityNote?: string | undefined;\n filePlan: PreviewFilePlanEntry[];\n /** Install destination dir relative to project root (e.g. \".sandcastle/<slug>\"). */\n installDir: string;\n /** True for `github:` installs — checksums are client-side, no tamper evidence (M1). */\n githubMode?: boolean;\n colorize?: boolean;\n}\n\nfunction pad(s: string, width: number): string {\n return s.length >= width ? s : s + \" \".repeat(width - s.length);\n}\n\nexport function renderPreview(input: PreviewInput): string {\n const { manifest, author, version } = input;\n const c = input.colorize\n ? pc\n : ({\n bold: (s: string) => s,\n dim: (s: string) => s,\n yellow: (s: string) => s,\n green: (s: string) => s,\n red: (s: string) => s,\n cyan: (s: string) => s,\n } as unknown as typeof pc);\n\n const lines: string[] = [];\n const rule = \"─\".repeat(60);\n\n // --- a. Summary -----------------------------------------------------------\n lines.push(c.bold(`${manifest.name}`));\n lines.push(`${c.dim(\"by\")} ${author} ${c.dim(\"·\")} v${version} ${c.dim(\"·\")} ${manifest.license}`);\n if (manifest.description) lines.push(c.dim(manifest.description));\n lines.push(\"\");\n lines.push(`${c.dim(\"Sandcastle range:\")} ${manifest.sandcastle.versionRange}`);\n if (input.compatibilityNote) {\n lines.push(c.yellow(`⚠ ${input.compatibilityNote}`));\n }\n if (manifest.agents.length > 0) {\n const agents = manifest.agents\n .map((a) => `${a.role} (${a.provider} ${a.model}${a.parallel ? \", parallel\" : \"\"})`)\n .join(\", \");\n lines.push(`${c.dim(\"Agents:\")} ${agents}`);\n }\n if (manifest.sandboxProviders.length > 0) {\n lines.push(`${c.dim(\"Sandbox:\")} ${manifest.sandboxProviders.join(\", \")}`);\n }\n\n // --- b. Required env vars -------------------------------------------------\n if (manifest.env.length > 0) {\n lines.push(\"\");\n lines.push(c.bold(\"Environment variables\"));\n const nameW = Math.max(4, ...manifest.env.map((e) => e.name.length));\n for (const e of manifest.env) {\n const req = e.required ? c.red(\"required\") : c.dim(\"optional\");\n const desc = e.description ? ` ${c.dim(e.description)}` : \"\";\n lines.push(` ${pad(e.name, nameW)} ${req}${desc}`);\n }\n }\n\n // --- c. Disclosures block (prominent / yellow) ----------------------------\n lines.push(\"\");\n lines.push(c.yellow(c.bold(\"⚠ Disclosures — what this workflow can do\")));\n lines.push(c.yellow(rule));\n const d = manifest.disclosures;\n lines.push(\n ` ${c.yellow(\"Host hooks:\")} ${d.hostHooks.length ? d.hostHooks.map((h) => `\\`${h}\\``).join(\", \") : c.dim(\"none\")}`,\n );\n lines.push(\n ` ${c.yellow(\"Sandbox hooks:\")} ${d.sandboxHooks.length ? d.sandboxHooks.map((h) => `\\`${h}\\``).join(\", \") : c.dim(\"none\")}`,\n );\n lines.push(` ${c.yellow(\"Network access:\")} ${d.networkAccess ?? c.dim(\"none declared\")}`);\n lines.push(\n ` ${c.yellow(\"Shell expansion:\")} ${d.usesShellExpansion ? c.red(\"yes — prompt files run shell commands at load time\") : c.dim(\"no\")}`,\n );\n lines.push(c.yellow(rule));\n\n // --- d. File plan ---------------------------------------------------------\n lines.push(\"\");\n lines.push(c.bold(`Files (into ${input.installDir}/)`));\n for (const f of input.filePlan) {\n const tag =\n f.action === \"create\"\n ? c.green(\"create \")\n : f.action === \"overwrite\"\n ? c.yellow(\"overwrite\")\n : c.dim(\"identical\");\n lines.push(` ${tag} ${f.target}`);\n }\n\n if (input.githubMode) {\n lines.push(\"\");\n lines.push(\n c.dim(\n \"Note: installing from github: — checksums are computed client-side from the fetched files (no registry tamper-evidence).\",\n ),\n );\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ function ctx(command) {
|
|
|
27
27
|
}
|
|
28
28
|
async function main() {
|
|
29
29
|
const program = new Command();
|
|
30
|
-
program.name("runcastle").description("Install and publish Sandcastle workflow packages").version("0.
|
|
30
|
+
program.name("runcastle").description("Install and publish Sandcastle workflow packages").version("0.2.0").option("--json", "machine-readable JSON output").option("--registry <url>", "registry base URL").option("-y, --yes", "skip confirmation prompts");
|
|
31
31
|
program.command("init").description("Scaffold a workflow.json from an existing .sandcastle/ directory").action(async (_o, cmd) => {
|
|
32
32
|
const g = ctx(cmd);
|
|
33
33
|
const { initCommand } = await import("./init-MOJNLWXK.js");
|
|
@@ -40,7 +40,7 @@ async function main() {
|
|
|
40
40
|
});
|
|
41
41
|
program.command("add").description("Install a workflow (author/slug[@version] or github:owner/repo[/dir][@sha])").argument("<spec>", "workflow reference").option("--dir <subdir>", "install into .sandcastle/<subdir>/").option("--dry-run", "resolve + preview only; write nothing").action(async (spec, o, cmd) => {
|
|
42
42
|
const g = ctx(cmd);
|
|
43
|
-
const { addCommand } = await import("./add-
|
|
43
|
+
const { addCommand } = await import("./add-DD7G73HH.js");
|
|
44
44
|
await addCommand(spec, {
|
|
45
45
|
...o.dir ? { dir: o.dir } : {},
|
|
46
46
|
...o.dryRun ? { dryRun: true } : {},
|
|
@@ -49,9 +49,14 @@ async function main() {
|
|
|
49
49
|
...g.registry ? { registry: g.registry } : {}
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
program.command("run").description("Preflight-check and run an installed workflow (builds the sandbox image on first run)").argument("[dir]", "install directory when multiple workflows are installed (e.g. .sandcastle/<name>)").action(async (dirArg, _o, cmd) => {
|
|
53
|
+
const g = ctx(cmd);
|
|
54
|
+
const { runCommand } = await import("./run-M4XXNPGD.js");
|
|
55
|
+
await runCommand(dirArg, { ...g.yes ? { yes: true } : {} });
|
|
56
|
+
});
|
|
52
57
|
program.command("info").description("Show a workflow's preview without installing").argument("<spec>", "author/slug[@version]").action(async (spec, _o, cmd) => {
|
|
53
58
|
const g = ctx(cmd);
|
|
54
|
-
const { infoCommand } = await import("./info-
|
|
59
|
+
const { infoCommand } = await import("./info-3PW6A6JA.js");
|
|
55
60
|
await infoCommand(spec, {
|
|
56
61
|
...g.json ? { json: true } : {},
|
|
57
62
|
...g.registry ? { registry: g.registry } : {}
|
|
@@ -77,7 +82,7 @@ async function main() {
|
|
|
77
82
|
});
|
|
78
83
|
program.command("update").description("Update installed workflows to newer versions (unmodified files only)").action(async (_o, cmd) => {
|
|
79
84
|
const g = ctx(cmd);
|
|
80
|
-
const { updateCommand } = await import("./update-
|
|
85
|
+
const { updateCommand } = await import("./update-UCT4K6IQ.js");
|
|
81
86
|
await updateCommand({
|
|
82
87
|
...g.json ? { json: true } : {},
|
|
83
88
|
...g.registry ? { registry: g.registry } : {}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n// runcastle CLI entrypoint. Kept intentionally light: commander + the output\n// helpers only. Every command module (and the heavy schema/analyzer code it may\n// pull) is loaded LAZILY via dynamic import so cold `npx` start stays < 3s\n// (spec §5.7) — the hot path `add` never touches ts-morph.\n\nimport { Command } from \"commander\";\nimport { isCliError } from \"./lib/errors.js\";\nimport { setJsonMode, logErr, pc } from \"./lib/output.js\";\n\ninterface GlobalOpts {\n json?: boolean;\n registry?: string;\n yes?: boolean;\n}\n\nfunction fail(e: unknown): void {\n // Set process.exitCode and let the event loop drain rather than calling\n // process.exit() synchronously. On Windows, forcing exit while undici's\n // keep-alive sockets (opened by `add`'s multi-file download) are still\n // closing trips a libuv assertion (`UV_HANDLE_CLOSING`) and yields exit 127\n // instead of the intended code. The success path already exits naturally, so\n // the error path does the same. Commands throw CliError; nothing runs after.\n if (isCliError(e)) {\n if (e.message) logErr(`${pc.red(\"error\")} ${e.message}`);\n for (const d of e.details) if (d) logErr(` ${pc.dim(d)}`);\n process.exitCode = e.exitCode;\n return;\n }\n logErr(`${pc.red(\"error\")} ${(e as Error)?.message ?? String(e)}`);\n process.exitCode = 1;\n}\n\n/** Merge global + command options and set json mode before running. */\nfunction ctx(command: Command): GlobalOpts {\n const opts = command.optsWithGlobals() as GlobalOpts;\n setJsonMode(Boolean(opts.json));\n return opts;\n}\n\nasync function main(): Promise<void> {\n const program = new Command();\n program\n .name(\"runcastle\")\n .description(\"Install and publish Sandcastle workflow packages\")\n .version(\"0.
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n// runcastle CLI entrypoint. Kept intentionally light: commander + the output\n// helpers only. Every command module (and the heavy schema/analyzer code it may\n// pull) is loaded LAZILY via dynamic import so cold `npx` start stays < 3s\n// (spec §5.7) — the hot path `add` never touches ts-morph.\n\nimport { Command } from \"commander\";\nimport { isCliError } from \"./lib/errors.js\";\nimport { setJsonMode, logErr, pc } from \"./lib/output.js\";\n\ninterface GlobalOpts {\n json?: boolean;\n registry?: string;\n yes?: boolean;\n}\n\nfunction fail(e: unknown): void {\n // Set process.exitCode and let the event loop drain rather than calling\n // process.exit() synchronously. On Windows, forcing exit while undici's\n // keep-alive sockets (opened by `add`'s multi-file download) are still\n // closing trips a libuv assertion (`UV_HANDLE_CLOSING`) and yields exit 127\n // instead of the intended code. The success path already exits naturally, so\n // the error path does the same. Commands throw CliError; nothing runs after.\n if (isCliError(e)) {\n if (e.message) logErr(`${pc.red(\"error\")} ${e.message}`);\n for (const d of e.details) if (d) logErr(` ${pc.dim(d)}`);\n process.exitCode = e.exitCode;\n return;\n }\n logErr(`${pc.red(\"error\")} ${(e as Error)?.message ?? String(e)}`);\n process.exitCode = 1;\n}\n\n/** Merge global + command options and set json mode before running. */\nfunction ctx(command: Command): GlobalOpts {\n const opts = command.optsWithGlobals() as GlobalOpts;\n setJsonMode(Boolean(opts.json));\n return opts;\n}\n\nasync function main(): Promise<void> {\n const program = new Command();\n program\n .name(\"runcastle\")\n .description(\"Install and publish Sandcastle workflow packages\")\n .version(\"0.2.0\")\n .option(\"--json\", \"machine-readable JSON output\")\n .option(\"--registry <url>\", \"registry base URL\")\n .option(\"-y, --yes\", \"skip confirmation prompts\");\n\n program\n .command(\"init\")\n .description(\"Scaffold a workflow.json from an existing .sandcastle/ directory\")\n .action(async (_o, cmd: Command) => {\n const g = ctx(cmd);\n const { initCommand } = await import(\"./commands/init.js\");\n await initCommand({ ...(g.yes ? { yes: true } : {}) });\n });\n\n program\n .command(\"validate\")\n .description(\"Validate a workflow package (exit 0/1)\")\n .argument(\"[path]\", \"package directory\", \".\")\n .action(async (pathArg: string, _o, cmd: Command) => {\n ctx(cmd);\n const { validateCommand } = await import(\"./commands/validate.js\");\n await validateCommand({ path: pathArg });\n });\n\n program\n .command(\"add\")\n .description(\"Install a workflow (author/slug[@version] or github:owner/repo[/dir][@sha])\")\n .argument(\"<spec>\", \"workflow reference\")\n .option(\"--dir <subdir>\", \"install into .sandcastle/<subdir>/\")\n .option(\"--dry-run\", \"resolve + preview only; write nothing\")\n .action(async (spec: string, o: { dir?: string; dryRun?: boolean }, cmd: Command) => {\n const g = ctx(cmd);\n const { addCommand } = await import(\"./commands/add.js\");\n await addCommand(spec, {\n ...(o.dir ? { dir: o.dir } : {}),\n ...(o.dryRun ? { dryRun: true } : {}),\n ...(g.yes ? { yes: true } : {}),\n ...(g.json ? { json: true } : {}),\n ...(g.registry ? { registry: g.registry } : {}),\n });\n });\n\n program\n .command(\"run\")\n .description(\"Preflight-check and run an installed workflow (builds the sandbox image on first run)\")\n .argument(\"[dir]\", \"install directory when multiple workflows are installed (e.g. .sandcastle/<name>)\")\n .action(async (dirArg: string | undefined, _o, cmd: Command) => {\n const g = ctx(cmd);\n const { runCommand } = await import(\"./commands/run.js\");\n await runCommand(dirArg, { ...(g.yes ? { yes: true } : {}) });\n });\n\n program\n .command(\"info\")\n .description(\"Show a workflow's preview without installing\")\n .argument(\"<spec>\", \"author/slug[@version]\")\n .action(async (spec: string, _o, cmd: Command) => {\n const g = ctx(cmd);\n const { infoCommand } = await import(\"./commands/info.js\");\n await infoCommand(spec, {\n ...(g.json ? { json: true } : {}),\n ...(g.registry ? { registry: g.registry } : {}),\n });\n });\n\n program\n .command(\"search\")\n .description(\"Search the registry\")\n .argument(\"[query]\", \"search text\", \"\")\n .option(\"--sort <sort>\", \"downloads | newest | name\")\n .action(async (query: string, o: { sort?: string }, cmd: Command) => {\n const g = ctx(cmd);\n const { searchCommand } = await import(\"./commands/search.js\");\n await searchCommand(query, {\n ...(o.sort ? { sort: o.sort } : {}),\n ...(g.json ? { json: true } : {}),\n ...(g.registry ? { registry: g.registry } : {}),\n });\n });\n\n program\n .command(\"publish\")\n .description(\"Validate, authenticate, and register the workflow at the current commit\")\n .action(async (_o, cmd: Command) => {\n const g = ctx(cmd);\n const { publishCommand } = await import(\"./commands/publish.js\");\n await publishCommand({\n ...(g.yes ? { yes: true } : {}),\n ...(g.json ? { json: true } : {}),\n ...(g.registry ? { registry: g.registry } : {}),\n });\n });\n\n program\n .command(\"update\")\n .description(\"Update installed workflows to newer versions (unmodified files only)\")\n .action(async (_o, cmd: Command) => {\n const g = ctx(cmd);\n const { updateCommand } = await import(\"./commands/update.js\");\n await updateCommand({\n ...(g.json ? { json: true } : {}),\n ...(g.registry ? { registry: g.registry } : {}),\n });\n });\n\n program\n .command(\"logout\")\n .description(\"Remove the cached GitHub token\")\n .action(async (_o, cmd: Command) => {\n ctx(cmd);\n const { logoutCommand } = await import(\"./commands/logout.js\");\n await logoutCommand();\n });\n\n await program.parseAsync(process.argv);\n}\n\nmain().catch(fail);\n"],"mappings":"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","names":[]}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
compatibilityNote,
|
|
3
|
-
detectSandcastle,
|
|
4
2
|
renderPreview
|
|
5
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VWAIXSB6.js";
|
|
6
4
|
import {
|
|
7
5
|
mapTarget
|
|
8
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QTC5Y3LU.js";
|
|
7
|
+
import {
|
|
8
|
+
compatibilityNote,
|
|
9
|
+
detectSandcastle
|
|
10
|
+
} from "./chunk-UG25BZUN.js";
|
|
9
11
|
import {
|
|
10
12
|
RegistryClient
|
|
11
13
|
} from "./chunk-5VPKMXSV.js";
|
|
@@ -75,4 +77,4 @@ async function infoCommand(spec, opts) {
|
|
|
75
77
|
export {
|
|
76
78
|
infoCommand
|
|
77
79
|
};
|
|
78
|
-
//# sourceMappingURL=info-
|
|
80
|
+
//# sourceMappingURL=info-3PW6A6JA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/info.ts"],"sourcesContent":["// `runcastle info <author/slug>` (spec §5.5). Prints the §5.4.3 preview WITHOUT\n// installing. Resolves the record from the registry and renders the same preview\n// surface `add` uses; the file plan reflects what an install into `.sandcastle/`\n// would do (create vs overwrite) without downloading file contents.\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { emitJson, isJsonMode, log, pc } from \"../lib/output.js\";\nimport { resolveRegistry, PAYLOAD_DIR } from \"../lib/config.js\";\nimport { RegistryClient } from \"../lib/registry.js\";\nimport { renderPreview, type PreviewFilePlanEntry } from \"../lib/preview.js\";\nimport { detectSandcastle, compatibilityNote } from \"../lib/sandcastle-version.js\";\nimport { mapTarget } from \"../lib/installer.js\";\nimport { CliError } from \"../lib/errors.js\";\nimport type { FetchLike } from \"../lib/http.js\";\n\nexport interface InfoOptions {\n registry?: string;\n json?: boolean;\n version?: string;\n fetchImpl?: FetchLike;\n}\n\nexport async function infoCommand(spec: string, opts: InfoOptions): Promise<void> {\n const atIdx = spec.indexOf(\"@\");\n const namepart = atIdx === -1 ? spec : spec.slice(0, atIdx);\n const inlineVersion = atIdx === -1 ? \"\" : spec.slice(atIdx + 1);\n const segs = namepart.split(\"/\").filter(Boolean);\n if (segs.length !== 2) {\n throw new CliError(`invalid spec \"${spec}\" — expected author/slug[@version]`, { code: \"E_BAD_SPEC\" });\n }\n const [author, slug] = segs as [string, string];\n const version = opts.version ?? inlineVersion;\n\n const registry = new RegistryClient({\n baseUrl: resolveRegistry(opts.registry),\n ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),\n });\n const record = await registry.getWorkflow(author, slug, version);\n\n if (isJsonMode()) {\n emitJson(record);\n return;\n }\n\n const cwd = process.cwd();\n const filePlan: PreviewFilePlanEntry[] = record.files.map((f) => {\n const target = mapTarget(f.path, PAYLOAD_DIR);\n const exists = fs.existsSync(path.join(cwd, ...target.split(\"/\")));\n return { target, action: exists ? \"overwrite\" : \"create\" };\n });\n\n const detected = await detectSandcastle(cwd);\n const note = compatibilityNote(record.manifest.sandcastle.versionRange, detected);\n\n log(\n renderPreview({\n manifest: record.manifest,\n author: record.author,\n version: record.version,\n compatibilityNote: note,\n filePlan,\n installDir: PAYLOAD_DIR,\n colorize: pc.isColorSupported,\n }),\n );\n log(\"\");\n log(pc.dim(`Install: npx runcastle add ${record.author}/${record.slug}${version ? `@${record.version}` : \"\"}`));\n if (record.versions.length > 1) {\n log(pc.dim(`Versions: ${record.versions.join(\", \")}`));\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/commands/info.ts"],"sourcesContent":["// `runcastle info <author/slug>` (spec §5.5). Prints the §5.4.3 preview WITHOUT\n// installing. Resolves the record from the registry and renders the same preview\n// surface `add` uses; the file plan reflects what an install into `.sandcastle/`\n// would do (create vs overwrite) without downloading file contents.\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { emitJson, isJsonMode, log, pc } from \"../lib/output.js\";\nimport { resolveRegistry, PAYLOAD_DIR } from \"../lib/config.js\";\nimport { RegistryClient } from \"../lib/registry.js\";\nimport { renderPreview, type PreviewFilePlanEntry } from \"../lib/preview.js\";\nimport { detectSandcastle, compatibilityNote } from \"../lib/sandcastle-version.js\";\nimport { mapTarget } from \"../lib/installer.js\";\nimport { CliError } from \"../lib/errors.js\";\nimport type { FetchLike } from \"../lib/http.js\";\n\nexport interface InfoOptions {\n registry?: string;\n json?: boolean;\n version?: string;\n fetchImpl?: FetchLike;\n}\n\nexport async function infoCommand(spec: string, opts: InfoOptions): Promise<void> {\n const atIdx = spec.indexOf(\"@\");\n const namepart = atIdx === -1 ? spec : spec.slice(0, atIdx);\n const inlineVersion = atIdx === -1 ? \"\" : spec.slice(atIdx + 1);\n const segs = namepart.split(\"/\").filter(Boolean);\n if (segs.length !== 2) {\n throw new CliError(`invalid spec \"${spec}\" — expected author/slug[@version]`, { code: \"E_BAD_SPEC\" });\n }\n const [author, slug] = segs as [string, string];\n const version = opts.version ?? inlineVersion;\n\n const registry = new RegistryClient({\n baseUrl: resolveRegistry(opts.registry),\n ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),\n });\n const record = await registry.getWorkflow(author, slug, version);\n\n if (isJsonMode()) {\n emitJson(record);\n return;\n }\n\n const cwd = process.cwd();\n const filePlan: PreviewFilePlanEntry[] = record.files.map((f) => {\n const target = mapTarget(f.path, PAYLOAD_DIR);\n const exists = fs.existsSync(path.join(cwd, ...target.split(\"/\")));\n return { target, action: exists ? \"overwrite\" : \"create\" };\n });\n\n const detected = await detectSandcastle(cwd);\n const note = compatibilityNote(record.manifest.sandcastle.versionRange, detected);\n\n log(\n renderPreview({\n manifest: record.manifest,\n author: record.author,\n version: record.version,\n compatibilityNote: note,\n filePlan,\n installDir: PAYLOAD_DIR,\n colorize: pc.isColorSupported,\n }),\n );\n log(\"\");\n log(pc.dim(`Install: npx runcastle add ${record.author}/${record.slug}${version ? `@${record.version}` : \"\"}`));\n if (record.versions.length > 1) {\n log(pc.dim(`Versions: ${record.versions.join(\", \")}`));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,OAAO,QAAQ;AACf,OAAO,UAAU;AAiBjB,eAAsB,YAAY,MAAc,MAAkC;AAChF,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,QAAM,WAAW,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;AAC1D,QAAM,gBAAgB,UAAU,KAAK,KAAK,KAAK,MAAM,QAAQ,CAAC;AAC9D,QAAM,OAAO,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,IAAI,SAAS,iBAAiB,IAAI,2CAAsC,EAAE,MAAM,aAAa,CAAC;AAAA,EACtG;AACA,QAAM,CAAC,QAAQ,IAAI,IAAI;AACvB,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,WAAW,IAAI,eAAe;AAAA,IAClC,SAAS,gBAAgB,KAAK,QAAQ;AAAA,IACtC,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,EACxD,CAAC;AACD,QAAM,SAAS,MAAM,SAAS,YAAY,QAAQ,MAAM,OAAO;AAE/D,MAAI,WAAW,GAAG;AAChB,aAAS,MAAM;AACf;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,WAAmC,OAAO,MAAM,IAAI,CAAC,MAAM;AAC/D,UAAM,SAAS,UAAU,EAAE,MAAM,WAAW;AAC5C,UAAM,SAAS,GAAG,WAAW,KAAK,KAAK,KAAK,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC;AACjE,WAAO,EAAE,QAAQ,QAAQ,SAAS,cAAc,SAAS;AAAA,EAC3D,CAAC;AAED,QAAM,WAAW,MAAM,iBAAiB,GAAG;AAC3C,QAAM,OAAO,kBAAkB,OAAO,SAAS,WAAW,cAAc,QAAQ;AAEhF;AAAA,IACE,cAAc;AAAA,MACZ,UAAU,OAAO;AAAA,MACjB,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,mBAAmB;AAAA,MACnB;AAAA,MACA,YAAY;AAAA,MACZ,UAAU,GAAG;AAAA,IACf,CAAC;AAAA,EACH;AACA,MAAI,EAAE;AACN,MAAI,GAAG,IAAI,+BAA+B,OAAO,MAAM,IAAI,OAAO,IAAI,GAAG,UAAU,IAAI,OAAO,OAAO,KAAK,EAAE,EAAE,CAAC;AAC/G,MAAI,OAAO,SAAS,SAAS,GAAG;AAC9B,QAAI,GAAG,IAAI,aAAa,OAAO,SAAS,KAAK,IAAI,CAAC,EAAE,CAAC;AAAA,EACvD;AACF;","names":[]}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readLockfile
|
|
3
|
+
} from "./chunk-GU4LRYYH.js";
|
|
4
|
+
import {
|
|
5
|
+
detectSandcastle
|
|
6
|
+
} from "./chunk-UG25BZUN.js";
|
|
7
|
+
import "./chunk-LP5MV76P.js";
|
|
8
|
+
import {
|
|
9
|
+
PAYLOAD_DIR,
|
|
10
|
+
SANDCASTLE_PACKAGE_NAME
|
|
11
|
+
} from "./chunk-AHRTPV2C.js";
|
|
12
|
+
import {
|
|
13
|
+
CliError
|
|
14
|
+
} from "./chunk-6CWCR5SO.js";
|
|
15
|
+
import {
|
|
16
|
+
isJsonMode,
|
|
17
|
+
log,
|
|
18
|
+
pc
|
|
19
|
+
} from "./chunk-PRNBOQHW.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/run.ts
|
|
22
|
+
import fs from "fs";
|
|
23
|
+
import fsp from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { spawn } from "child_process";
|
|
26
|
+
function winQuote(token) {
|
|
27
|
+
return /[ \t"^&|<>()%!]/.test(token) ? `"${token.replace(/"/g, '""')}"` : token;
|
|
28
|
+
}
|
|
29
|
+
var defaultExec = (cmd, args, opts) => new Promise((resolve, reject) => {
|
|
30
|
+
const child = process.platform === "win32" ? spawn([cmd, ...args].map(winQuote).join(" "), {
|
|
31
|
+
cwd: opts.cwd,
|
|
32
|
+
stdio: opts.inherit ? "inherit" : "ignore",
|
|
33
|
+
shell: true
|
|
34
|
+
}) : spawn(cmd, args, {
|
|
35
|
+
cwd: opts.cwd,
|
|
36
|
+
stdio: opts.inherit ? "inherit" : "ignore"
|
|
37
|
+
});
|
|
38
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill(), opts.timeoutMs) : void 0;
|
|
39
|
+
child.on("error", (e) => {
|
|
40
|
+
if (timer) clearTimeout(timer);
|
|
41
|
+
reject(e);
|
|
42
|
+
});
|
|
43
|
+
child.on("close", (code) => {
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
resolve({ code: code ?? 1 });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
function defaultImageName(repoDir) {
|
|
49
|
+
const dirName = repoDir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "local";
|
|
50
|
+
const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
51
|
+
return `sandcastle:${sanitized || "local"}`;
|
|
52
|
+
}
|
|
53
|
+
function detectImageName(entrySource, repoDir) {
|
|
54
|
+
const m = /imageName\s*:\s*["'`]([^"'`]+)["'`]/.exec(entrySource);
|
|
55
|
+
if (m?.[1] && !m[1].includes("${")) return m[1];
|
|
56
|
+
return defaultImageName(repoDir);
|
|
57
|
+
}
|
|
58
|
+
function normalizeDirArg(dirArg) {
|
|
59
|
+
const clean = dirArg.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\.\//, "");
|
|
60
|
+
if (clean === PAYLOAD_DIR || clean.startsWith(`${PAYLOAD_DIR}/`)) return clean;
|
|
61
|
+
return `${PAYLOAD_DIR}/${clean}`;
|
|
62
|
+
}
|
|
63
|
+
async function findEntrypoint(cwd, dir, lock) {
|
|
64
|
+
const candidates = lock?.entrypoint ? [lock.entrypoint] : [`${dir}/main.ts`, `${dir}/main.mts`];
|
|
65
|
+
for (const rel of candidates) {
|
|
66
|
+
if (fs.existsSync(path.join(cwd, ...rel.split("/")))) return rel;
|
|
67
|
+
}
|
|
68
|
+
throw new CliError(`no entrypoint found in ${dir}/ (looked for ${candidates.join(", ")})`, {
|
|
69
|
+
code: "E_NO_ENTRYPOINT",
|
|
70
|
+
details: [`install a workflow first: npx runcastle add <author/slug>`]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function resolveRunTarget(cwd, dirArg, interactive) {
|
|
74
|
+
const lockfile = await readLockfile(cwd);
|
|
75
|
+
if (dirArg) {
|
|
76
|
+
const dir2 = normalizeDirArg(dirArg);
|
|
77
|
+
const lock2 = lockfile.installs.find((i) => i.dir === dir2);
|
|
78
|
+
return { dir: dir2, entrypoint: await findEntrypoint(cwd, dir2, lock2), ...lock2 ? { lock: lock2 } : {} };
|
|
79
|
+
}
|
|
80
|
+
if (lockfile.installs.length > 1) {
|
|
81
|
+
if (!interactive) {
|
|
82
|
+
throw new CliError(`multiple workflows installed \u2014 pass one: runcastle run <dir>`, {
|
|
83
|
+
code: "E_AMBIGUOUS_TARGET",
|
|
84
|
+
details: lockfile.installs.map((i) => `runcastle run ${i.dir} ${pc.dim(`(${i.source})`)}`)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const clack = await import("@clack/prompts");
|
|
88
|
+
const choice = await clack.select({
|
|
89
|
+
message: "Which workflow?",
|
|
90
|
+
options: lockfile.installs.map((i) => ({ value: i.dir, label: `${i.source} ${pc.dim(i.dir)}` }))
|
|
91
|
+
});
|
|
92
|
+
if (clack.isCancel(choice)) throw new CliError("", { exitCode: 1, code: "E_CANCELLED" });
|
|
93
|
+
const lock2 = lockfile.installs.find((i) => i.dir === choice);
|
|
94
|
+
return { dir: lock2.dir, entrypoint: await findEntrypoint(cwd, lock2.dir, lock2), lock: lock2 };
|
|
95
|
+
}
|
|
96
|
+
const lock = lockfile.installs[0];
|
|
97
|
+
const dir = lock?.dir ?? PAYLOAD_DIR;
|
|
98
|
+
return { dir, entrypoint: await findEntrypoint(cwd, dir, lock), ...lock ? { lock } : {} };
|
|
99
|
+
}
|
|
100
|
+
function detectPackageManager(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
103
|
+
const pm = pkg.packageManager?.split("@")[0];
|
|
104
|
+
if (pm === "pnpm" || pm === "yarn" || pm === "bun" || pm === "npm") return pm;
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
108
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
109
|
+
if (fs.existsSync(path.join(cwd, "bun.lock")) || fs.existsSync(path.join(cwd, "bun.lockb"))) return "bun";
|
|
110
|
+
return "npm";
|
|
111
|
+
}
|
|
112
|
+
function installDevCommand(pm, pkgs) {
|
|
113
|
+
switch (pm) {
|
|
114
|
+
case "pnpm":
|
|
115
|
+
return { cmd: "pnpm", args: ["add", "-D", ...pkgs] };
|
|
116
|
+
case "yarn":
|
|
117
|
+
return { cmd: "yarn", args: ["add", "-D", ...pkgs] };
|
|
118
|
+
case "bun":
|
|
119
|
+
return { cmd: "bun", args: ["add", "-d", ...pkgs] };
|
|
120
|
+
default:
|
|
121
|
+
return { cmd: "npm", args: ["install", "-D", ...pkgs] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function ensureSandcastle(cwd, interactive, yes, exec) {
|
|
125
|
+
const detected = await detectSandcastle(cwd);
|
|
126
|
+
if (detected.version) {
|
|
127
|
+
log(`${pc.green("\u2713")} ${SANDCASTLE_PACKAGE_NAME} ${detected.version}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const missing = [SANDCASTLE_PACKAGE_NAME];
|
|
131
|
+
if (!fs.existsSync(path.join(cwd, "node_modules", "tsx"))) missing.push("tsx");
|
|
132
|
+
const pm = detectPackageManager(cwd);
|
|
133
|
+
const { cmd, args } = installDevCommand(pm, missing);
|
|
134
|
+
const cmdline = `${cmd} ${args.join(" ")}`;
|
|
135
|
+
let proceed = yes;
|
|
136
|
+
if (!proceed && interactive) {
|
|
137
|
+
const clack = await import("@clack/prompts");
|
|
138
|
+
const ok = await clack.confirm({
|
|
139
|
+
message: `${SANDCASTLE_PACKAGE_NAME} is not installed. Install it now? (${cmdline})`,
|
|
140
|
+
initialValue: true
|
|
141
|
+
});
|
|
142
|
+
proceed = !clack.isCancel(ok) && ok === true;
|
|
143
|
+
}
|
|
144
|
+
if (!proceed) {
|
|
145
|
+
throw new CliError(`${SANDCASTLE_PACKAGE_NAME} is not installed in this project`, {
|
|
146
|
+
code: "E_MISSING_RUNTIME",
|
|
147
|
+
details: [`install it, then re-run: ${cmdline}`]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
log(`${pc.cyan("\u2192")} ${cmdline}`);
|
|
151
|
+
const res = await exec(cmd, args, { cwd, inherit: true });
|
|
152
|
+
if (res.code !== 0) {
|
|
153
|
+
throw new CliError(`"${cmdline}" failed (exit ${res.code})`, { code: "E_MISSING_RUNTIME" });
|
|
154
|
+
}
|
|
155
|
+
log(`${pc.green("\u2713")} ${SANDCASTLE_PACKAGE_NAME} installed`);
|
|
156
|
+
}
|
|
157
|
+
function detectProvider(cwd, dir, lock) {
|
|
158
|
+
const fromLock = lock?.sandboxProviders?.find((p) => p === "docker" || p === "podman");
|
|
159
|
+
if (fromLock) return fromLock;
|
|
160
|
+
if (fs.existsSync(path.join(cwd, ...dir.split("/"), "Containerfile"))) return "podman";
|
|
161
|
+
return "docker";
|
|
162
|
+
}
|
|
163
|
+
function localSandcastleBin(cwd) {
|
|
164
|
+
const bin = path.join(
|
|
165
|
+
cwd,
|
|
166
|
+
"node_modules",
|
|
167
|
+
".bin",
|
|
168
|
+
process.platform === "win32" ? "sandcastle.cmd" : "sandcastle"
|
|
169
|
+
);
|
|
170
|
+
return fs.existsSync(bin) ? bin : void 0;
|
|
171
|
+
}
|
|
172
|
+
async function ensureImage(cwd, entrypoint, provider, exec) {
|
|
173
|
+
try {
|
|
174
|
+
const info = await exec(provider, ["info"], { cwd, inherit: false, timeoutMs: 15e3 });
|
|
175
|
+
if (info.code !== 0) {
|
|
176
|
+
throw new CliError(`the ${provider} daemon is not responding`, {
|
|
177
|
+
code: "E_SANDBOX_DOWN",
|
|
178
|
+
details: [provider === "docker" ? "start Docker Desktop (or the docker service) and re-run" : "start the podman machine and re-run"]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
if (e instanceof CliError) throw e;
|
|
183
|
+
throw new CliError(`${provider} is not installed or not on PATH`, {
|
|
184
|
+
code: "E_SANDBOX_MISSING",
|
|
185
|
+
details: [provider === "docker" ? "install Docker Desktop: https://www.docker.com/" : "install Podman: https://podman.io/"]
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
log(`${pc.green("\u2713")} ${provider} daemon`);
|
|
189
|
+
const entrySource = await fsp.readFile(path.join(cwd, ...entrypoint.split("/")), "utf8");
|
|
190
|
+
const image = detectImageName(entrySource, cwd);
|
|
191
|
+
const inspect = await exec(provider, ["image", "inspect", image], { cwd, inherit: false, timeoutMs: 15e3 });
|
|
192
|
+
if (inspect.code === 0) {
|
|
193
|
+
log(`${pc.green("\u2713")} image ${image}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
log(`${pc.cyan("\u2192")} building image ${image} (first run)\u2026`);
|
|
197
|
+
const bin = localSandcastleBin(cwd);
|
|
198
|
+
const buildArgs = [provider, "build-image", "--image-name", image];
|
|
199
|
+
const res = bin ? await exec(bin, buildArgs, { cwd, inherit: true }) : await exec("npx", ["-y", SANDCASTLE_PACKAGE_NAME, ...buildArgs], { cwd, inherit: true });
|
|
200
|
+
if (res.code !== 0) {
|
|
201
|
+
throw new CliError(`image build failed (exit ${res.code})`, {
|
|
202
|
+
code: "E_IMAGE_BUILD",
|
|
203
|
+
details: [`retry manually: npx ${SANDCASTLE_PACKAGE_NAME} ${buildArgs.join(" ")}`]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
log(`${pc.green("\u2713")} image ${image}`);
|
|
207
|
+
}
|
|
208
|
+
function parseEnvFile(text) {
|
|
209
|
+
const out = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
211
|
+
const line = raw.trim();
|
|
212
|
+
if (!line || line.startsWith("#")) continue;
|
|
213
|
+
const eq = line.indexOf("=");
|
|
214
|
+
if (eq <= 0) continue;
|
|
215
|
+
const key = line.slice(0, eq).trim();
|
|
216
|
+
let val = line.slice(eq + 1).trim();
|
|
217
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
218
|
+
val = val.slice(1, -1);
|
|
219
|
+
}
|
|
220
|
+
out.set(key, val);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
async function ensureEnv(cwd, dir, lock) {
|
|
225
|
+
const envPath = path.join(cwd, ...dir.split("/"), ".env");
|
|
226
|
+
const examplePath = path.join(cwd, ...dir.split("/"), ".env.example");
|
|
227
|
+
const exampleText = fs.existsSync(examplePath) ? await fsp.readFile(examplePath, "utf8") : "";
|
|
228
|
+
if (!fs.existsSync(envPath) && exampleText) {
|
|
229
|
+
await fsp.writeFile(envPath, exampleText, "utf8");
|
|
230
|
+
log(`${pc.cyan("\u2192")} created ${dir}/.env from .env.example`);
|
|
231
|
+
}
|
|
232
|
+
const required = lock?.requiredEnv ?? [...parseEnvFile(exampleText).keys()];
|
|
233
|
+
if (required.length === 0) return;
|
|
234
|
+
const envText = fs.existsSync(envPath) ? await fsp.readFile(envPath, "utf8") : "";
|
|
235
|
+
const envMap = parseEnvFile(envText);
|
|
236
|
+
const missing = required.filter((k) => !envMap.get(k) && !process.env[k]);
|
|
237
|
+
if (missing.length > 0) {
|
|
238
|
+
throw new CliError(`missing required env var(s): ${missing.join(", ")}`, {
|
|
239
|
+
code: "E_MISSING_ENV",
|
|
240
|
+
details: [
|
|
241
|
+
`edit ${dir}/.env and fill in the value(s)`,
|
|
242
|
+
...missing.includes("CLAUDE_CODE_OAUTH_TOKEN") ? ["generate a token with: claude setup-token"] : []
|
|
243
|
+
]
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
log(`${pc.green("\u2713")} env: ${required.join(", ")}`);
|
|
247
|
+
}
|
|
248
|
+
async function runCommand(dirArg, opts) {
|
|
249
|
+
if (isJsonMode()) {
|
|
250
|
+
throw new CliError("run executes a workflow interactively \u2014 --json is not supported", {
|
|
251
|
+
code: "E_UNSUPPORTED"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
255
|
+
const exec = opts.execImpl ?? defaultExec;
|
|
256
|
+
const interactive = !opts.yes && process.stdin.isTTY === true;
|
|
257
|
+
const target = await resolveRunTarget(cwd, dirArg, interactive);
|
|
258
|
+
log(`${pc.bold("runcastle run")} ${pc.dim(target.lock ? target.lock.source : target.dir)}`);
|
|
259
|
+
await ensureSandcastle(cwd, interactive, Boolean(opts.yes), exec);
|
|
260
|
+
const provider = detectProvider(cwd, target.dir, target.lock);
|
|
261
|
+
await ensureImage(cwd, target.entrypoint, provider, exec);
|
|
262
|
+
await ensureEnv(cwd, target.dir, target.lock);
|
|
263
|
+
const tsxBin = path.join(
|
|
264
|
+
cwd,
|
|
265
|
+
"node_modules",
|
|
266
|
+
".bin",
|
|
267
|
+
process.platform === "win32" ? "tsx.cmd" : "tsx"
|
|
268
|
+
);
|
|
269
|
+
log("");
|
|
270
|
+
log(`${pc.cyan("\u25B6")} tsx ${target.entrypoint}`);
|
|
271
|
+
log("");
|
|
272
|
+
const res = fs.existsSync(tsxBin) ? await exec(tsxBin, [target.entrypoint], { cwd, inherit: true }) : await exec("npx", ["-y", "tsx", target.entrypoint], { cwd, inherit: true });
|
|
273
|
+
if (res.code !== 0) {
|
|
274
|
+
throw new CliError(`workflow exited with code ${res.code}`, {
|
|
275
|
+
exitCode: res.code,
|
|
276
|
+
code: "E_RUN_FAILED",
|
|
277
|
+
details: [`logs are under ${target.dir}/logs/`]
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export {
|
|
282
|
+
defaultImageName,
|
|
283
|
+
detectImageName,
|
|
284
|
+
detectPackageManager,
|
|
285
|
+
detectProvider,
|
|
286
|
+
installDevCommand,
|
|
287
|
+
parseEnvFile,
|
|
288
|
+
resolveRunTarget,
|
|
289
|
+
runCommand
|
|
290
|
+
};
|
|
291
|
+
//# sourceMappingURL=run-M4XXNPGD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/run.ts"],"sourcesContent":["// `runcastle run [dir]` — the one-command path from an installed workflow to a\n// running one. Runs an ordered preflight (sandcastle dep → sandbox daemon →\n// image → env), fixes what it safely can (build the image, copy .env.example),\n// prints ONE actionable line for what it can't, then execs the entrypoint via\n// tsx. Unlike `add` (which never executes anything, spec §8.1), `run` is the\n// user's explicit consent to execute the workflow they installed.\n\nimport fs from \"node:fs\";\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { spawn } from \"node:child_process\";\nimport { SANDCASTLE_PACKAGE_NAME } from \"@castellan/schema\";\nimport { PAYLOAD_DIR } from \"../lib/config.js\";\nimport { CliError } from \"../lib/errors.js\";\nimport { isJsonMode, log, pc } from \"../lib/output.js\";\nimport { detectSandcastle } from \"../lib/sandcastle-version.js\";\nimport { readLockfile, type LockEntry } from \"../lib/lockfile.js\";\n\nexport interface RunCommandOptions {\n dir?: string;\n yes?: boolean;\n /** Test seams. */\n cwd?: string;\n execImpl?: ExecImpl;\n}\n\n/** Spawn seam: returns the exit code (rejects only on spawn failure). */\nexport type ExecImpl = (\n cmd: string,\n args: string[],\n opts: { cwd: string; inherit: boolean; timeoutMs?: number },\n) => Promise<{ code: number }>;\n\n/** Quote a token for the Windows shell only when it needs it. */\nfunction winQuote(token: string): string {\n return /[ \\t\"^&|<>()%!]/.test(token) ? `\"${token.replace(/\"/g, '\"\"')}\"` : token;\n}\n\nconst defaultExec: ExecImpl = (cmd, args, opts) =>\n new Promise((resolve, reject) => {\n // .cmd shims (npx/npm/docker on some setups) need a shell on Windows, and\n // shell:true with an args ARRAY is deprecated (DEP0190) — so pass one\n // pre-quoted command string there, and a plain argv everywhere else.\n const child =\n process.platform === \"win32\"\n ? spawn([cmd, ...args].map(winQuote).join(\" \"), {\n cwd: opts.cwd,\n stdio: opts.inherit ? \"inherit\" : \"ignore\",\n shell: true,\n })\n : spawn(cmd, args, {\n cwd: opts.cwd,\n stdio: opts.inherit ? \"inherit\" : \"ignore\",\n });\n const timer = opts.timeoutMs\n ? setTimeout(() => child.kill(), opts.timeoutMs)\n : undefined;\n child.on(\"error\", (e) => {\n if (timer) clearTimeout(timer);\n reject(e);\n });\n child.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n resolve({ code: code ?? 1 });\n });\n });\n\n// --- Image name (mirrors @ai-hero/sandcastle's defaultImageName exactly) ------\n\nexport function defaultImageName(repoDir: string): string {\n const dirName = repoDir.replace(/[\\\\/]+$/, \"\").split(/[\\\\/]/).pop() ?? \"local\";\n const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, \"-\");\n return `sandcastle:${sanitized || \"local\"}`;\n}\n\n/** A hardcoded `imageName: \"…\"` in the entrypoint wins over the default. */\nexport function detectImageName(entrySource: string, repoDir: string): string {\n const m = /imageName\\s*:\\s*[\"'`]([^\"'`]+)[\"'`]/.exec(entrySource);\n if (m?.[1] && !m[1].includes(\"${\")) return m[1];\n return defaultImageName(repoDir);\n}\n\n// --- Target resolution ---------------------------------------------------------\n\ninterface RunTarget {\n /** Install dir relative to project root (e.g. \".sandcastle\"). */\n dir: string;\n /** Entrypoint path relative to project root. */\n entrypoint: string;\n lock?: LockEntry;\n}\n\nfunction normalizeDirArg(dirArg: string): string {\n const clean = dirArg.replace(/\\\\/g, \"/\").replace(/\\/+$/, \"\").replace(/^\\.\\//, \"\");\n if (clean === PAYLOAD_DIR || clean.startsWith(`${PAYLOAD_DIR}/`)) return clean;\n return `${PAYLOAD_DIR}/${clean}`;\n}\n\nasync function findEntrypoint(cwd: string, dir: string, lock?: LockEntry): Promise<string> {\n const candidates = lock?.entrypoint\n ? [lock.entrypoint]\n : [`${dir}/main.ts`, `${dir}/main.mts`];\n for (const rel of candidates) {\n if (fs.existsSync(path.join(cwd, ...rel.split(\"/\")))) return rel;\n }\n throw new CliError(`no entrypoint found in ${dir}/ (looked for ${candidates.join(\", \")})`, {\n code: \"E_NO_ENTRYPOINT\",\n details: [`install a workflow first: npx runcastle add <author/slug>`],\n });\n}\n\nexport async function resolveRunTarget(\n cwd: string,\n dirArg: string | undefined,\n interactive: boolean,\n): Promise<RunTarget> {\n const lockfile = await readLockfile(cwd);\n\n if (dirArg) {\n const dir = normalizeDirArg(dirArg);\n const lock = lockfile.installs.find((i) => i.dir === dir);\n return { dir, entrypoint: await findEntrypoint(cwd, dir, lock), ...(lock ? { lock } : {}) };\n }\n\n if (lockfile.installs.length > 1) {\n if (!interactive) {\n throw new CliError(`multiple workflows installed — pass one: runcastle run <dir>`, {\n code: \"E_AMBIGUOUS_TARGET\",\n details: lockfile.installs.map((i) => `runcastle run ${i.dir} ${pc.dim(`(${i.source})`)}`),\n });\n }\n const clack = await import(\"@clack/prompts\");\n const choice = await clack.select({\n message: \"Which workflow?\",\n options: lockfile.installs.map((i) => ({ value: i.dir, label: `${i.source} ${pc.dim(i.dir)}` })),\n });\n if (clack.isCancel(choice)) throw new CliError(\"\", { exitCode: 1, code: \"E_CANCELLED\" });\n const lock = lockfile.installs.find((i) => i.dir === choice)!;\n return { dir: lock.dir, entrypoint: await findEntrypoint(cwd, lock.dir, lock), lock };\n }\n\n const lock = lockfile.installs[0];\n const dir = lock?.dir ?? PAYLOAD_DIR;\n return { dir, entrypoint: await findEntrypoint(cwd, dir, lock), ...(lock ? { lock } : {}) };\n}\n\n// --- Preflight: sandcastle dep ---------------------------------------------------\n\ntype PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nexport function detectPackageManager(cwd: string): PackageManager {\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(cwd, \"package.json\"), \"utf8\")) as {\n packageManager?: string;\n };\n const pm = pkg.packageManager?.split(\"@\")[0];\n if (pm === \"pnpm\" || pm === \"yarn\" || pm === \"bun\" || pm === \"npm\") return pm;\n } catch {\n /* fall through to lockfile sniffing */\n }\n if (fs.existsSync(path.join(cwd, \"pnpm-lock.yaml\"))) return \"pnpm\";\n if (fs.existsSync(path.join(cwd, \"yarn.lock\"))) return \"yarn\";\n if (fs.existsSync(path.join(cwd, \"bun.lock\")) || fs.existsSync(path.join(cwd, \"bun.lockb\"))) return \"bun\";\n return \"npm\";\n}\n\nexport function installDevCommand(pm: PackageManager, pkgs: string[]): { cmd: string; args: string[] } {\n switch (pm) {\n case \"pnpm\":\n return { cmd: \"pnpm\", args: [\"add\", \"-D\", ...pkgs] };\n case \"yarn\":\n return { cmd: \"yarn\", args: [\"add\", \"-D\", ...pkgs] };\n case \"bun\":\n return { cmd: \"bun\", args: [\"add\", \"-d\", ...pkgs] };\n default:\n return { cmd: \"npm\", args: [\"install\", \"-D\", ...pkgs] };\n }\n}\n\nasync function ensureSandcastle(\n cwd: string,\n interactive: boolean,\n yes: boolean,\n exec: ExecImpl,\n): Promise<void> {\n const detected = await detectSandcastle(cwd);\n if (detected.version) {\n log(`${pc.green(\"✓\")} ${SANDCASTLE_PACKAGE_NAME} ${detected.version}`);\n return;\n }\n\n const missing = [SANDCASTLE_PACKAGE_NAME];\n if (!fs.existsSync(path.join(cwd, \"node_modules\", \"tsx\"))) missing.push(\"tsx\");\n const pm = detectPackageManager(cwd);\n const { cmd, args } = installDevCommand(pm, missing);\n const cmdline = `${cmd} ${args.join(\" \")}`;\n\n let proceed = yes;\n if (!proceed && interactive) {\n const clack = await import(\"@clack/prompts\");\n const ok = await clack.confirm({\n message: `${SANDCASTLE_PACKAGE_NAME} is not installed. Install it now? (${cmdline})`,\n initialValue: true,\n });\n proceed = !clack.isCancel(ok) && ok === true;\n }\n if (!proceed) {\n throw new CliError(`${SANDCASTLE_PACKAGE_NAME} is not installed in this project`, {\n code: \"E_MISSING_RUNTIME\",\n details: [`install it, then re-run: ${cmdline}`],\n });\n }\n\n log(`${pc.cyan(\"→\")} ${cmdline}`);\n const res = await exec(cmd, args, { cwd, inherit: true });\n if (res.code !== 0) {\n throw new CliError(`\"${cmdline}\" failed (exit ${res.code})`, { code: \"E_MISSING_RUNTIME\" });\n }\n log(`${pc.green(\"✓\")} ${SANDCASTLE_PACKAGE_NAME} installed`);\n}\n\n// --- Preflight: sandbox provider + image ---------------------------------------\n\ntype Provider = \"docker\" | \"podman\";\n\nexport function detectProvider(cwd: string, dir: string, lock?: LockEntry): Provider {\n const fromLock = lock?.sandboxProviders?.find((p) => p === \"docker\" || p === \"podman\");\n if (fromLock) return fromLock as Provider;\n if (fs.existsSync(path.join(cwd, ...dir.split(\"/\"), \"Containerfile\"))) return \"podman\";\n return \"docker\";\n}\n\n/** Path to the locally installed sandcastle bin, or undefined. */\nfunction localSandcastleBin(cwd: string): string | undefined {\n const bin = path.join(\n cwd,\n \"node_modules\",\n \".bin\",\n process.platform === \"win32\" ? \"sandcastle.cmd\" : \"sandcastle\",\n );\n return fs.existsSync(bin) ? bin : undefined;\n}\n\nasync function ensureImage(\n cwd: string,\n entrypoint: string,\n provider: Provider,\n exec: ExecImpl,\n): Promise<void> {\n // Daemon reachable?\n try {\n const info = await exec(provider, [\"info\"], { cwd, inherit: false, timeoutMs: 15_000 });\n if (info.code !== 0) {\n throw new CliError(`the ${provider} daemon is not responding`, {\n code: \"E_SANDBOX_DOWN\",\n details: [provider === \"docker\" ? \"start Docker Desktop (or the docker service) and re-run\" : \"start the podman machine and re-run\"],\n });\n }\n } catch (e) {\n if (e instanceof CliError) throw e;\n throw new CliError(`${provider} is not installed or not on PATH`, {\n code: \"E_SANDBOX_MISSING\",\n details: [provider === \"docker\" ? \"install Docker Desktop: https://www.docker.com/\" : \"install Podman: https://podman.io/\"],\n });\n }\n log(`${pc.green(\"✓\")} ${provider} daemon`);\n\n const entrySource = await fsp.readFile(path.join(cwd, ...entrypoint.split(\"/\")), \"utf8\");\n const image = detectImageName(entrySource, cwd);\n const inspect = await exec(provider, [\"image\", \"inspect\", image], { cwd, inherit: false, timeoutMs: 15_000 });\n if (inspect.code === 0) {\n log(`${pc.green(\"✓\")} image ${image}`);\n return;\n }\n\n log(`${pc.cyan(\"→\")} building image ${image} (first run)…`);\n const bin = localSandcastleBin(cwd);\n const buildArgs = [provider, \"build-image\", \"--image-name\", image];\n const res = bin\n ? await exec(bin, buildArgs, { cwd, inherit: true })\n : await exec(\"npx\", [\"-y\", SANDCASTLE_PACKAGE_NAME, ...buildArgs], { cwd, inherit: true });\n if (res.code !== 0) {\n throw new CliError(`image build failed (exit ${res.code})`, {\n code: \"E_IMAGE_BUILD\",\n details: [`retry manually: npx ${SANDCASTLE_PACKAGE_NAME} ${buildArgs.join(\" \")}`],\n });\n }\n log(`${pc.green(\"✓\")} image ${image}`);\n}\n\n// --- Preflight: env --------------------------------------------------------------\n\n/** Parse KEY=VALUE lines (dotenv subset — enough for our .env.example format). */\nexport function parseEnvFile(text: string): Map<string, string> {\n const out = new Map<string, string>();\n for (const raw of text.split(/\\r?\\n/)) {\n const line = raw.trim();\n if (!line || line.startsWith(\"#\")) continue;\n const eq = line.indexOf(\"=\");\n if (eq <= 0) continue;\n const key = line.slice(0, eq).trim();\n let val = line.slice(eq + 1).trim();\n if ((val.startsWith('\"') && val.endsWith('\"')) || (val.startsWith(\"'\") && val.endsWith(\"'\"))) {\n val = val.slice(1, -1);\n }\n out.set(key, val);\n }\n return out;\n}\n\nasync function ensureEnv(cwd: string, dir: string, lock?: LockEntry): Promise<void> {\n const envPath = path.join(cwd, ...dir.split(\"/\"), \".env\");\n const examplePath = path.join(cwd, ...dir.split(\"/\"), \".env.example\");\n\n const exampleText = fs.existsSync(examplePath) ? await fsp.readFile(examplePath, \"utf8\") : \"\";\n if (!fs.existsSync(envPath) && exampleText) {\n await fsp.writeFile(envPath, exampleText, \"utf8\");\n log(`${pc.cyan(\"→\")} created ${dir}/.env from .env.example`);\n }\n\n // Required keys: what the author declared at publish time, else every key in\n // .env.example. A key is satisfied by a non-empty .env value or process.env.\n const required = lock?.requiredEnv ?? [...parseEnvFile(exampleText).keys()];\n if (required.length === 0) return;\n\n const envText = fs.existsSync(envPath) ? await fsp.readFile(envPath, \"utf8\") : \"\";\n const envMap = parseEnvFile(envText);\n const missing = required.filter((k) => !envMap.get(k) && !process.env[k]);\n if (missing.length > 0) {\n throw new CliError(`missing required env var(s): ${missing.join(\", \")}`, {\n code: \"E_MISSING_ENV\",\n details: [\n `edit ${dir}/.env and fill in the value(s)`,\n ...(missing.includes(\"CLAUDE_CODE_OAUTH_TOKEN\")\n ? [\"generate a token with: claude setup-token\"]\n : []),\n ],\n });\n }\n log(`${pc.green(\"✓\")} env: ${required.join(\", \")}`);\n}\n\n// --- Command ---------------------------------------------------------------------\n\nexport async function runCommand(dirArg: string | undefined, opts: RunCommandOptions): Promise<void> {\n if (isJsonMode()) {\n throw new CliError(\"run executes a workflow interactively — --json is not supported\", {\n code: \"E_UNSUPPORTED\",\n });\n }\n const cwd = opts.cwd ?? process.cwd();\n const exec = opts.execImpl ?? defaultExec;\n const interactive = !opts.yes && process.stdin.isTTY === true;\n\n const target = await resolveRunTarget(cwd, dirArg, interactive);\n log(`${pc.bold(\"runcastle run\")} ${pc.dim(target.lock ? target.lock.source : target.dir)}`);\n\n await ensureSandcastle(cwd, interactive, Boolean(opts.yes), exec);\n const provider = detectProvider(cwd, target.dir, target.lock);\n await ensureImage(cwd, target.entrypoint, provider, exec);\n await ensureEnv(cwd, target.dir, target.lock);\n\n const tsxBin = path.join(\n cwd,\n \"node_modules\",\n \".bin\",\n process.platform === \"win32\" ? \"tsx.cmd\" : \"tsx\",\n );\n log(\"\");\n log(`${pc.cyan(\"▶\")} tsx ${target.entrypoint}`);\n log(\"\");\n const res = fs.existsSync(tsxBin)\n ? await exec(tsxBin, [target.entrypoint], { cwd, inherit: true })\n : await exec(\"npx\", [\"-y\", \"tsx\", target.entrypoint], { cwd, inherit: true });\n if (res.code !== 0) {\n throw new CliError(`workflow exited with code ${res.code}`, {\n exitCode: res.code,\n code: \"E_RUN_FAILED\",\n details: [`logs are under ${target.dir}/logs/`],\n });\n }\n}\n"],"mappings":"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","names":["dir","lock"]}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
downloadFromRegistry
|
|
2
|
+
downloadFromRegistry
|
|
3
|
+
} from "./chunk-7CHGGMP7.js";
|
|
4
|
+
import {
|
|
3
5
|
readLockfile,
|
|
4
6
|
writeLockEntry
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GU4LRYYH.js";
|
|
6
8
|
import {
|
|
7
9
|
mapTarget,
|
|
8
10
|
resolveInsideRoot,
|
|
9
11
|
sha256Hex
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QTC5Y3LU.js";
|
|
11
13
|
import {
|
|
12
14
|
RealGitHubClient
|
|
13
15
|
} from "./chunk-OJ356IMJ.js";
|
|
@@ -120,4 +122,4 @@ async function updateCommand(opts) {
|
|
|
120
122
|
export {
|
|
121
123
|
updateCommand
|
|
122
124
|
};
|
|
123
|
-
//# sourceMappingURL=update-
|
|
125
|
+
//# sourceMappingURL=update-UCT4K6IQ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/update.ts"],"sourcesContent":["// `runcastle update` (spec §5.6, minimal v1). Read the lockfile, check for newer\n// versions, auto-update files that are UNMODIFIED since install, and REFUSE to\n// touch locally-modified files (telling the user to resolve them manually).\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { emitJson, isJsonMode, log, pc } from \"../lib/output.js\";\nimport { resolveRegistry } from \"../lib/config.js\";\nimport { RegistryClient } from \"../lib/registry.js\";\nimport { RealGitHubClient } from \"../lib/github.js\";\nimport { downloadFromRegistry } from \"../lib/download.js\";\nimport { sha256Hex } from \"../lib/checksum.js\";\nimport { getFetch, type FetchLike } from \"../lib/http.js\";\nimport { readLockfile, writeLockEntry, type LockEntry } from \"../lib/lockfile.js\";\nimport { mapTarget, resolveInsideRoot } from \"../lib/installer.js\";\n\nexport interface UpdateOptions {\n registry?: string;\n json?: boolean;\n fetchImpl?: FetchLike;\n cwd?: string;\n}\n\ninterface UpdateReport {\n slug: string;\n from: string;\n to: string;\n updated: string[];\n skippedModified: string[];\n status: \"updated\" | \"up-to-date\" | \"skipped-github\" | \"no-op\";\n}\n\nfunction onDiskSha(projectRoot: string, target: string): string | null {\n const abs = path.join(projectRoot, ...target.split(\"/\"));\n if (!fs.existsSync(abs)) return null;\n return sha256Hex(new Uint8Array(fs.readFileSync(abs)));\n}\n\nexport async function updateCommand(opts: UpdateOptions): Promise<void> {\n const cwd = opts.cwd ?? process.cwd();\n const fetchImpl = getFetch(opts.fetchImpl);\n const github = new RealGitHubClient({ fetchImpl });\n const lock = await readLockfile(cwd);\n\n if (lock.installs.length === 0) {\n if (isJsonMode()) emitJson({ ok: true, reports: [] });\n else log(pc.dim(\"No installs recorded in .sandcastle/runcastle.lock.json — nothing to update.\"));\n return;\n }\n\n const reports: UpdateReport[] = [];\n\n for (const entry of lock.installs) {\n if (entry.source.startsWith(\"github:\") || !entry.author) {\n reports.push({ slug: entry.slug, from: entry.version, to: entry.version, updated: [], skippedModified: [], status: \"skipped-github\" });\n continue;\n }\n const registry = new RegistryClient({ baseUrl: resolveRegistry(opts.registry ?? entry.registry), fetchImpl });\n const record = await registry.getWorkflow(entry.author, entry.slug);\n if (record.version === entry.version) {\n reports.push({ slug: entry.slug, from: entry.version, to: record.version, updated: [], skippedModified: [], status: \"up-to-date\" });\n continue;\n }\n\n const downloaded = await downloadFromRegistry(record, registry, github, fetchImpl);\n const updated: string[] = [];\n const skippedModified: string[] = [];\n const newFiles: Record<string, string> = { ...entry.files };\n\n for (const d of downloaded) {\n const target = mapTarget(d.path, entry.dir);\n const recorded = entry.files[target];\n const disk = onDiskSha(cwd, target);\n // Locally modified: on-disk differs from what we installed last time.\n if (disk !== null && recorded !== undefined && disk !== recorded) {\n skippedModified.push(target);\n continue;\n }\n if (disk === d.sha256) {\n // already identical to the new bytes\n newFiles[target] = d.sha256;\n continue;\n }\n // Defense-in-depth: never write outside the project (§8 / C1).\n const abs = resolveInsideRoot(cwd, target);\n fs.mkdirSync(path.dirname(abs), { recursive: true });\n fs.writeFileSync(abs, d.bytes);\n updated.push(target);\n newFiles[target] = d.sha256;\n }\n\n // Persist the new version/sha; skipped files retain their recorded checksum.\n const next: LockEntry = {\n ...entry,\n version: record.version,\n sha: record.commitSha,\n files: newFiles,\n installedAt: new Date().toISOString(),\n };\n await writeLockEntry(cwd, next);\n\n reports.push({\n slug: entry.slug,\n from: entry.version,\n to: record.version,\n updated,\n skippedModified,\n status: \"updated\",\n });\n }\n\n if (isJsonMode()) {\n emitJson({ ok: true, reports });\n return;\n }\n\n for (const r of reports) {\n if (r.status === \"up-to-date\") {\n log(`${pc.dim(\"·\")} ${r.slug} ${pc.dim(`is up to date (${r.from})`)}`);\n } else if (r.status === \"skipped-github\") {\n log(`${pc.dim(\"·\")} ${r.slug} ${pc.dim(\"installed from github: — re-run `runcastle add github:…` to update\")}`);\n } else {\n log(`${pc.green(\"↑\")} ${r.slug} ${pc.dim(`${r.from} → ${r.to}`)}`);\n for (const f of r.updated) log(` ${pc.green(\"updated\")} ${f}`);\n for (const f of r.skippedModified) log(` ${pc.yellow(\"skipped\")} ${f} ${pc.dim(\"(locally modified — resolve manually)\")}`);\n }\n }\n const anyModified = reports.some((r) => r.skippedModified.length > 0);\n if (anyModified) {\n log(\"\");\n log(pc.yellow(\"Some files were locally modified and left untouched. Diff them against the new version and merge by hand.\"));\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/commands/update.ts"],"sourcesContent":["// `runcastle update` (spec §5.6, minimal v1). Read the lockfile, check for newer\n// versions, auto-update files that are UNMODIFIED since install, and REFUSE to\n// touch locally-modified files (telling the user to resolve them manually).\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { emitJson, isJsonMode, log, pc } from \"../lib/output.js\";\nimport { resolveRegistry } from \"../lib/config.js\";\nimport { RegistryClient } from \"../lib/registry.js\";\nimport { RealGitHubClient } from \"../lib/github.js\";\nimport { downloadFromRegistry } from \"../lib/download.js\";\nimport { sha256Hex } from \"../lib/checksum.js\";\nimport { getFetch, type FetchLike } from \"../lib/http.js\";\nimport { readLockfile, writeLockEntry, type LockEntry } from \"../lib/lockfile.js\";\nimport { mapTarget, resolveInsideRoot } from \"../lib/installer.js\";\n\nexport interface UpdateOptions {\n registry?: string;\n json?: boolean;\n fetchImpl?: FetchLike;\n cwd?: string;\n}\n\ninterface UpdateReport {\n slug: string;\n from: string;\n to: string;\n updated: string[];\n skippedModified: string[];\n status: \"updated\" | \"up-to-date\" | \"skipped-github\" | \"no-op\";\n}\n\nfunction onDiskSha(projectRoot: string, target: string): string | null {\n const abs = path.join(projectRoot, ...target.split(\"/\"));\n if (!fs.existsSync(abs)) return null;\n return sha256Hex(new Uint8Array(fs.readFileSync(abs)));\n}\n\nexport async function updateCommand(opts: UpdateOptions): Promise<void> {\n const cwd = opts.cwd ?? process.cwd();\n const fetchImpl = getFetch(opts.fetchImpl);\n const github = new RealGitHubClient({ fetchImpl });\n const lock = await readLockfile(cwd);\n\n if (lock.installs.length === 0) {\n if (isJsonMode()) emitJson({ ok: true, reports: [] });\n else log(pc.dim(\"No installs recorded in .sandcastle/runcastle.lock.json — nothing to update.\"));\n return;\n }\n\n const reports: UpdateReport[] = [];\n\n for (const entry of lock.installs) {\n if (entry.source.startsWith(\"github:\") || !entry.author) {\n reports.push({ slug: entry.slug, from: entry.version, to: entry.version, updated: [], skippedModified: [], status: \"skipped-github\" });\n continue;\n }\n const registry = new RegistryClient({ baseUrl: resolveRegistry(opts.registry ?? entry.registry), fetchImpl });\n const record = await registry.getWorkflow(entry.author, entry.slug);\n if (record.version === entry.version) {\n reports.push({ slug: entry.slug, from: entry.version, to: record.version, updated: [], skippedModified: [], status: \"up-to-date\" });\n continue;\n }\n\n const downloaded = await downloadFromRegistry(record, registry, github, fetchImpl);\n const updated: string[] = [];\n const skippedModified: string[] = [];\n const newFiles: Record<string, string> = { ...entry.files };\n\n for (const d of downloaded) {\n const target = mapTarget(d.path, entry.dir);\n const recorded = entry.files[target];\n const disk = onDiskSha(cwd, target);\n // Locally modified: on-disk differs from what we installed last time.\n if (disk !== null && recorded !== undefined && disk !== recorded) {\n skippedModified.push(target);\n continue;\n }\n if (disk === d.sha256) {\n // already identical to the new bytes\n newFiles[target] = d.sha256;\n continue;\n }\n // Defense-in-depth: never write outside the project (§8 / C1).\n const abs = resolveInsideRoot(cwd, target);\n fs.mkdirSync(path.dirname(abs), { recursive: true });\n fs.writeFileSync(abs, d.bytes);\n updated.push(target);\n newFiles[target] = d.sha256;\n }\n\n // Persist the new version/sha; skipped files retain their recorded checksum.\n const next: LockEntry = {\n ...entry,\n version: record.version,\n sha: record.commitSha,\n files: newFiles,\n installedAt: new Date().toISOString(),\n };\n await writeLockEntry(cwd, next);\n\n reports.push({\n slug: entry.slug,\n from: entry.version,\n to: record.version,\n updated,\n skippedModified,\n status: \"updated\",\n });\n }\n\n if (isJsonMode()) {\n emitJson({ ok: true, reports });\n return;\n }\n\n for (const r of reports) {\n if (r.status === \"up-to-date\") {\n log(`${pc.dim(\"·\")} ${r.slug} ${pc.dim(`is up to date (${r.from})`)}`);\n } else if (r.status === \"skipped-github\") {\n log(`${pc.dim(\"·\")} ${r.slug} ${pc.dim(\"installed from github: — re-run `runcastle add github:…` to update\")}`);\n } else {\n log(`${pc.green(\"↑\")} ${r.slug} ${pc.dim(`${r.from} → ${r.to}`)}`);\n for (const f of r.updated) log(` ${pc.green(\"updated\")} ${f}`);\n for (const f of r.skippedModified) log(` ${pc.yellow(\"skipped\")} ${f} ${pc.dim(\"(locally modified — resolve manually)\")}`);\n }\n }\n const anyModified = reports.some((r) => r.skippedModified.length > 0);\n if (anyModified) {\n log(\"\");\n log(pc.yellow(\"Some files were locally modified and left untouched. Diff them against the new version and merge by hand.\"));\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/dist/add-PRGLHAVL.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/add.ts","../src/lib/install-id.ts"],"sourcesContent":["// `runcastle add <author/slug>[@version]` and `add github:owner/repo[/dir][@sha]`\n// (spec §5.4). The critical path. Order is exact: resolve → download → verify\n// checksums → preview → confirm → write → post-install → lockfile → ping.\n\nimport path from \"node:path\";\nimport {\n parseManifest,\n PAYLOAD_DIR,\n type Manifest,\n type WorkflowRecord,\n} from \"@castellan/schema\";\nimport { CliError } from \"../lib/errors.js\";\nimport { emitJson, isJsonMode, log, pc } from \"../lib/output.js\";\nimport { resolveRegistry } from \"../lib/config.js\";\nimport { RegistryClient } from \"../lib/registry.js\";\nimport { RealGitHubClient, type GitHubClient } from \"../lib/github.js\";\nimport { downloadFromRegistry } from \"../lib/download.js\";\nimport { sha256Hex } from \"../lib/checksum.js\";\nimport { getFetch, type FetchLike } from \"../lib/http.js\";\nimport { detectSandcastle, compatibilityNote } from \"../lib/sandcastle-version.js\";\nimport { renderPreview } from \"../lib/preview.js\";\nimport {\n planInstall,\n hasConflicts,\n toPreviewPlan,\n renderDiff,\n writePlan,\n postInstallNotes,\n type PlannedFile,\n} from \"../lib/installer.js\";\nimport { writeLockEntry } from \"../lib/lockfile.js\";\nimport { getInstallId } from \"../lib/install-id.js\";\n\nexport interface AddOptions {\n dir?: string;\n dryRun?: boolean;\n yes?: boolean;\n json?: boolean;\n registry?: string;\n /** Test seams. */\n fetchImpl?: FetchLike;\n github?: GitHubClient;\n cwd?: string;\n}\n\ninterface ResolvedInstall {\n manifest: Manifest;\n author: string;\n slug: string;\n version: string;\n commitSha: string;\n files: { path: string; bytes: Uint8Array; sha256: string }[];\n githubMode: boolean;\n source: string;\n registry?: RegistryClient;\n}\n\n// --- Spec parsing ------------------------------------------------------------\n\ninterface ParsedRef {\n githubMode: boolean;\n author?: string;\n slug?: string;\n version?: string;\n owner?: string;\n repo?: string;\n directory?: string;\n ref?: string;\n}\n\nexport function parseAddSpec(spec: string): ParsedRef {\n if (spec.startsWith(\"github:\")) {\n const rest = spec.slice(\"github:\".length);\n const atIdx = rest.indexOf(\"@\");\n const pathPart = atIdx === -1 ? rest : rest.slice(0, atIdx);\n const ref = atIdx === -1 ? \"\" : rest.slice(atIdx + 1);\n const segs = pathPart.split(\"/\").filter(Boolean);\n if (segs.length < 2) {\n throw new CliError(`invalid github: spec \"${spec}\" — expected github:owner/repo[/dir][@sha]`, {\n code: \"E_BAD_SPEC\",\n });\n }\n const owner = segs[0]!;\n const repo = segs[1]!;\n const dirSegs = segs.slice(2);\n return {\n githubMode: true,\n owner,\n repo,\n ...(dirSegs.length ? { directory: dirSegs.join(\"/\") } : {}),\n ...(ref ? { ref } : {}),\n };\n }\n const atIdx = spec.indexOf(\"@\");\n const namepart = atIdx === -1 ? spec : spec.slice(0, atIdx);\n const version = atIdx === -1 ? \"\" : spec.slice(atIdx + 1);\n const segs = namepart.split(\"/\").filter(Boolean);\n if (segs.length !== 2) {\n throw new CliError(`invalid spec \"${spec}\" — expected author/slug[@version] or github:owner/repo[/dir][@sha]`, {\n code: \"E_BAD_SPEC\",\n });\n }\n return {\n githubMode: false,\n author: segs[0]!,\n slug: segs[1]!,\n ...(version ? { version } : {}),\n };\n}\n\n// --- Resolution --------------------------------------------------------------\n\nasync function resolveFromRegistry(\n parsed: ParsedRef,\n registry: RegistryClient,\n github: GitHubClient,\n fetchImpl: FetchLike,\n): Promise<ResolvedInstall> {\n const record: WorkflowRecord = await registry.getWorkflow(parsed.author!, parsed.slug!, parsed.version);\n const downloaded = await downloadFromRegistry(record, registry, github, fetchImpl);\n return {\n manifest: record.manifest,\n author: record.author,\n slug: record.slug,\n version: record.version,\n commitSha: record.commitSha,\n files: downloaded.map((d) => ({ path: d.path, bytes: d.bytes, sha256: d.sha256 })),\n githubMode: false,\n source: `${record.author}/${record.slug}`,\n registry,\n };\n}\n\nasync function resolveFromGitHub(\n parsed: ParsedRef,\n github: GitHubClient,\n): Promise<ResolvedInstall> {\n const { owner, repo } = parsed;\n const dirPrefix = parsed.directory ? `${parsed.directory.replace(/\\/+$/, \"\")}/` : \"\";\n const sha = await github.resolveRef(owner!, repo!, parsed.ref ?? \"HEAD\");\n\n // Fetch and parse the manifest at the pinned SHA.\n const manifestBytes = await github.fetchRawFile(owner!, repo!, sha, `${dirPrefix}workflow.json`);\n let manifest: Manifest;\n try {\n const parsedManifest = parseManifest(JSON.parse(new TextDecoder().decode(manifestBytes)));\n if (!parsedManifest.ok) {\n throw new CliError(`workflow.json at ${owner}/${repo}@${sha.slice(0, 7)} is invalid`, {\n code: \"E_SCHEMA_INVALID\",\n details: parsedManifest.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`),\n });\n }\n manifest = parsedManifest.manifest;\n } catch (e) {\n if (e instanceof CliError) throw e;\n throw new CliError(`could not parse workflow.json from ${owner}/${repo}`, {\n code: \"E_SCHEMA_INVALID\",\n details: [(e as Error).message],\n });\n }\n\n // Fetch every listed file and compute checksums CLIENT-SIDE (M1: no tamper\n // evidence — surfaced in the preview).\n const files: ResolvedInstall[\"files\"] = [];\n for (const rel of manifest.files) {\n const bytes = await github.fetchRawFile(owner!, repo!, sha, `${dirPrefix}${rel}`);\n files.push({ path: rel, bytes, sha256: sha256Hex(bytes) });\n }\n\n return {\n manifest,\n author: `github:${owner}/${repo}`,\n slug: manifest.slug,\n version: manifest.version,\n commitSha: sha,\n files,\n githubMode: true,\n source: `github:${owner}/${repo}${parsed.directory ? `/${parsed.directory}` : \"\"}`,\n };\n}\n\n// --- Install-dir decision ----------------------------------------------------\n\nasync function decideInstallDir(\n projectRoot: string,\n slug: string,\n files: ResolvedInstall[\"files\"],\n opts: AddOptions,\n): Promise<{ installDir: string; namespaced: boolean }> {\n if (opts.dir) {\n return { installDir: `${PAYLOAD_DIR}/${opts.dir.replace(/^[./]+/, \"\")}`, namespaced: true };\n }\n // Default target: .sandcastle. Detect conflicts against existing files.\n const flatPlan = planInstall(projectRoot, files, PAYLOAD_DIR);\n if (!hasConflicts(flatPlan)) return { installDir: PAYLOAD_DIR, namespaced: false };\n\n // Conflict: default to a namespaced install (spec §11.4). Offer the choice\n // interactively; non-interactive falls back to the safe (namespaced) default.\n const namespacedDir = `${PAYLOAD_DIR}/${slug}`;\n const interactive = !opts.yes && !isJsonMode() && process.stdin.isTTY;\n if (!interactive) return { installDir: namespacedDir, namespaced: true };\n\n const clack = await import(\"@clack/prompts\");\n const choice = await clack.select({\n message: `Existing .sandcastle/ files would be overwritten. Install where?`,\n options: [\n { value: \"namespaced\", label: `Namespaced — ${namespacedDir}/ (recommended)` },\n { value: \"overwrite\", label: `Overwrite files in ${PAYLOAD_DIR}/` },\n ],\n initialValue: \"namespaced\",\n });\n if (clack.isCancel(choice)) throw new CliError(\"\", { exitCode: 1, code: \"E_CANCELLED\" });\n return choice === \"overwrite\"\n ? { installDir: PAYLOAD_DIR, namespaced: false }\n : { installDir: namespacedDir, namespaced: true };\n}\n\n// --- Command -----------------------------------------------------------------\n\nexport async function addCommand(spec: string, opts: AddOptions): Promise<void> {\n const cwd = opts.cwd ?? process.cwd();\n const fetchImpl = getFetch(opts.fetchImpl);\n const github = opts.github ?? new RealGitHubClient({ fetchImpl });\n const parsed = parseAddSpec(spec);\n\n // 1–2. Resolve + download + verify.\n let resolved: ResolvedInstall;\n if (parsed.githubMode) {\n resolved = await resolveFromGitHub(parsed, github);\n } else {\n const registry = new RegistryClient({ baseUrl: resolveRegistry(opts.registry), fetchImpl });\n resolved = await resolveFromRegistry(parsed, registry, github, fetchImpl);\n }\n\n // Install-dir decision (conflict → namespaced default).\n const { installDir } = await decideInstallDir(cwd, resolved.slug, resolved.files, opts);\n const plan = planInstall(cwd, resolved.files, installDir);\n\n // 3. Preview.\n const detected = await detectSandcastle(cwd);\n const note = compatibilityNote(resolved.manifest.sandcastle.versionRange, detected);\n const previewText = renderPreview({\n manifest: resolved.manifest,\n author: resolved.author,\n version: resolved.version,\n compatibilityNote: note,\n filePlan: toPreviewPlan(plan),\n installDir,\n githubMode: resolved.githubMode,\n colorize: pc.isColorSupported && !isJsonMode(),\n });\n\n if (isJsonMode()) {\n // In --json mode we only write when explicitly told to (--yes) and not\n // dry-run. When we're not writing, emit the preview plan and stop; when we\n // are, stay silent here — a single install result is emitted after writing.\n if (opts.dryRun || !opts.yes) {\n emitJson({\n ok: true,\n action: \"preview\",\n author: resolved.author,\n slug: resolved.slug,\n version: resolved.version,\n commitSha: resolved.commitSha,\n installDir,\n githubMode: resolved.githubMode,\n compatibilityNote: note ?? null,\n disclosures: resolved.manifest.disclosures,\n env: resolved.manifest.env,\n files: plan.map((p) => ({ target: p.target, action: p.action, sha256: p.sha256 })),\n });\n return;\n }\n } else {\n log(previewText);\n }\n\n // 3e. Diffs on conflict (interactive, non-json).\n const interactive = !opts.yes && !isJsonMode() && process.stdin.isTTY;\n if (opts.dryRun) {\n if (!isJsonMode()) log(pc.dim(\"\\n(dry run — nothing written)\"));\n return;\n }\n\n const conflicts = plan.filter((p) => p.action === \"overwrite\");\n if (interactive && conflicts.length > 0) {\n const clack = await import(\"@clack/prompts\");\n const showDiff = await clack.confirm({\n message: `${conflicts.length} file(s) will be overwritten. Show diffs?`,\n initialValue: false,\n });\n if (!clack.isCancel(showDiff) && showDiff) {\n for (const p of conflicts) {\n log(\"\");\n log(renderDiff(cwd, p));\n }\n }\n }\n\n // 3/4. Confirm + write.\n if (interactive) {\n const clack = await import(\"@clack/prompts\");\n const ok = await clack.confirm({ message: `Write ${plan.filter((p) => p.action !== \"identical\").length} file(s)?`, initialValue: true });\n if (clack.isCancel(ok) || !ok) {\n throw new CliError(\"aborted — nothing written\", { exitCode: 1, code: \"E_ABORTED\" });\n }\n }\n\n await writePlan(cwd, plan);\n\n // 6. Lockfile.\n const filesRecord: Record<string, string> = {};\n for (const p of plan) filesRecord[p.target] = p.sha256;\n await writeLockEntry(cwd, {\n source: resolved.source,\n slug: resolved.slug,\n ...(resolved.githubMode ? {} : { author: resolved.author }),\n version: resolved.version,\n sha: resolved.commitSha,\n ...(resolved.registry ? { registry: resolved.registry.baseUrl } : {}),\n dir: installDir,\n files: filesRecord,\n installedAt: new Date().toISOString(),\n });\n\n // 5. Post-install output.\n if (!isJsonMode()) {\n const written = plan.filter((p) => p.action !== \"identical\").length;\n log(\"\");\n log(pc.green(`✓ installed ${resolved.slug}@${resolved.version} (${written} file(s) into ${installDir}/)`));\n log(\"\");\n log(pc.bold(\"Next steps:\"));\n for (const line of postInstallNotes(resolved.manifest, installDir)) log(` ${line}`);\n } else {\n emitJson({\n ok: true,\n action: \"install\",\n slug: resolved.slug,\n version: resolved.version,\n installDir,\n written: plan.filter((p) => p.action !== \"identical\").map((p) => p.target),\n });\n }\n\n // 6. Download ping (registry only, fire-and-forget).\n if (resolved.registry) {\n const anonId = await getInstallId();\n void resolved.registry.ping(resolved.author, resolved.slug, anonId).catch(() => {});\n }\n}\n","// A persistent random anonymous install id for download pings (spec §5.4.6 —\n// \"no PII beyond a random anonymous install id\"; dedupe by id per day). Stored\n// alongside the auth token; generated once on first use.\n\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { configDir } from \"./config.js\";\n\nfunction idPath(): string {\n return path.join(configDir(), \"install-id\");\n}\n\nexport async function getInstallId(): Promise<string> {\n const file = idPath();\n try {\n const existing = (await fsp.readFile(file, \"utf8\")).trim();\n if (existing) return existing;\n } catch {\n /* generate below */\n }\n const id = randomUUID();\n try {\n await fsp.mkdir(configDir(), { recursive: true });\n await fsp.writeFile(file, id, \"utf8\");\n } catch {\n /* non-fatal — a transient id is fine for a ping */\n }\n return id;\n}\n"],"mappings":"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","names":["atIdx","segs"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/download.ts","../src/lib/lockfile.ts"],"sourcesContent":["// Download + verify (spec §5.4.2). Registry cache endpoint first, then\n// raw.githubusercontent.com fallback. EVERY file's SHA-256 is verified against\n// the registry's publish-time checksum; a mismatch is a security-flavored abort.\n\nimport {\n type FileChecksum,\n type WorkflowRecord,\n} from \"@castellan/schema\";\nimport { CliError } from \"./errors.js\";\nimport { sha256Hex } from \"./checksum.js\";\nimport { doFetch, type FetchLike } from \"./http.js\";\nimport { USER_AGENT } from \"./config.js\";\nimport type { RegistryClient } from \"./registry.js\";\nimport type { GitHubClient } from \"./github.js\";\n\n/** A downloaded file: raw bytes keyed later by its package-relative path. */\nexport interface DownloadedFile {\n path: string;\n bytes: Uint8Array;\n sha256: string;\n /** Where the bytes came from, for transparency. */\n source: \"registry\" | \"github\";\n}\n\n/**\n * Fetch bytes from the registry cache endpoint. A `410 Gone` is a MODERATION\n * signal (blocked/removed workflow, §8.7): it hard-aborts and must NOT fall\n * back to GitHub. Any other failure (404/5xx/network) returns null so the\n * caller can try the GitHub raw mirror.\n */\nasync function fetchFromRegistry(\n fetchImpl: FetchLike,\n url: string,\n filePath: string,\n): Promise<Uint8Array | null> {\n const res = await doFetch(fetchImpl, url, { headers: { \"user-agent\": USER_AGENT } });\n if (res.status === 410) {\n throw new CliError(\n `${filePath} has been withdrawn by the registry (410) — this workflow was blocked or removed for policy reasons; refusing to fall back to GitHub`,\n { code: \"E_GONE\", details: [url] },\n );\n }\n if (!res.ok) return null;\n return new Uint8Array(await res.arrayBuffer());\n}\n\n/**\n * Download and verify every file in a resolved registry record. Owner/repo are\n * parsed from `record.repo`. On any checksum mismatch, aborts immediately with a\n * security-flavored error naming the file and both digests.\n */\nexport async function downloadFromRegistry(\n record: WorkflowRecord,\n registry: RegistryClient,\n github: GitHubClient,\n fetchImpl: FetchLike,\n): Promise<DownloadedFile[]> {\n const [owner, repo] = record.repo.split(\"/\");\n const dirPrefix = record.directory ? `${record.directory.replace(/\\/+$/, \"\")}/` : \"\";\n const out: DownloadedFile[] = [];\n\n for (const file of record.files as FileChecksum[]) {\n let bytes: Uint8Array | null = null;\n let source: DownloadedFile[\"source\"] = \"registry\";\n const urlsTried: string[] = [];\n\n // 1) Registry cache endpoint (downloadUrl if provided, else constructed).\n const cacheUrl = file.downloadUrl ?? registry.fileUrl(record.author, record.slug, record.version, file.path);\n urlsTried.push(cacheUrl);\n bytes = await fetchFromRegistry(fetchImpl, cacheUrl, file.path);\n\n // 2) raw.githubusercontent.com fallback at the pinned SHA.\n if (!bytes && owner && repo) {\n source = \"github\";\n try {\n bytes = await github.fetchRawFile(owner, repo, record.commitSha, `${dirPrefix}${file.path}`);\n } catch (e) {\n urlsTried.push((e as CliError).message);\n }\n }\n\n if (!bytes) {\n throw new CliError(`could not download ${file.path} from any source`, {\n code: \"E_DOWNLOAD\",\n details: urlsTried,\n });\n }\n\n const digest = sha256Hex(bytes);\n if (digest !== file.sha256) {\n throw new CliError(\n `checksum mismatch for ${file.path} — refusing to write (possible tampering)`,\n {\n code: \"E_CHECKSUM\",\n details: [\n `expected sha256: ${file.sha256}`,\n `actual sha256: ${digest}`,\n `source: ${source}`,\n ],\n },\n );\n }\n\n out.push({ path: file.path, bytes, sha256: digest, source });\n }\n\n return out;\n}\n","// The install lockfile (spec §5.4.6): `.sandcastle/runcastle.lock.json`. Records\n// what was installed so `runcastle update` can diff cleanly. Supports multiple\n// installs (namespaced) keyed by their install directory.\n\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { PAYLOAD_DIR, LOCKFILE_NAME } from \"./config.js\";\n\nexport interface LockEntry {\n /** \"author/slug\" for registry installs, or \"github:owner/repo[/dir]\" for github: installs. */\n source: string;\n slug: string;\n author?: string;\n version: string;\n sha: string;\n registry?: string;\n /** Install directory relative to project root (e.g. \".sandcastle\" or \".sandcastle/<slug>\"). */\n dir: string;\n /** Installed target path (relative to project root) → sha256 of the written bytes. */\n files: Record<string, string>;\n installedAt: string;\n}\n\nexport interface Lockfile {\n lockfileVersion: 1;\n installs: LockEntry[];\n}\n\nfunction lockPath(projectRoot: string): string {\n return path.join(projectRoot, PAYLOAD_DIR, LOCKFILE_NAME);\n}\n\nexport async function readLockfile(projectRoot: string): Promise<Lockfile> {\n try {\n const raw = await fsp.readFile(lockPath(projectRoot), \"utf8\");\n const parsed = JSON.parse(raw) as Partial<Lockfile>;\n if (parsed && Array.isArray(parsed.installs)) {\n return { lockfileVersion: 1, installs: parsed.installs };\n }\n } catch {\n /* absent or unreadable → empty */\n }\n return { lockfileVersion: 1, installs: [] };\n}\n\n/** Upsert an install entry (keyed by install `dir`) and persist. */\nexport async function writeLockEntry(projectRoot: string, entry: LockEntry): Promise<void> {\n const lock = await readLockfile(projectRoot);\n const idx = lock.installs.findIndex((i) => i.dir === entry.dir);\n if (idx >= 0) lock.installs[idx] = entry;\n else lock.installs.push(entry);\n lock.installs.sort((a, b) => a.dir.localeCompare(b.dir));\n const file = lockPath(projectRoot);\n await fsp.mkdir(path.dirname(file), { recursive: true });\n await fsp.writeFile(file, `${JSON.stringify(lock, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/checksum.ts","../src/lib/installer.ts"],"sourcesContent":["// SHA-256 checksums. The tamper-evidence anchor (spec §8.2): the CLI verifies\n// every downloaded file against the registry's publish-time checksum, regardless\n// of where the bytes came from.\n\nimport { createHash } from \"node:crypto\";\n\n/** Lowercase hex SHA-256 of raw bytes. */\nexport function sha256Hex(bytes: Uint8Array): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n","// Shared install mechanics for `add` (registry and github: modes): map package\n// paths to install targets (with namespacing), compute the create/overwrite plan,\n// render per-file diffs, write bytes byte-for-byte, and generate post-install\n// notes. Never executes anything (spec §8.1).\n\nimport fsp from \"node:fs/promises\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createTwoFilesPatch } from \"diff\";\nimport type { Manifest } from \"@castellan/schema\";\nimport { PAYLOAD_DIR } from \"./config.js\";\nimport { CliError } from \"./errors.js\";\nimport { sha256Hex } from \"./checksum.js\";\nimport type { FileAction, PreviewFilePlanEntry } from \"./preview.js\";\n\nexport interface PlannedFile {\n /** Package-relative source path (e.g. \".sandcastle/main.ts\"). */\n packagePath: string;\n /** Target path relative to project root (post-namespacing). */\n target: string;\n bytes: Uint8Array;\n sha256: string;\n action: FileAction;\n}\n\n/**\n * Resolve a project-relative target to an absolute path and assert it stays\n * inside `projectRoot` (defense-in-depth against a malicious manifest whose\n * `files`/entrypoint escape the payload dir — §8 / C1). Aborts the whole\n * install if the resolved path would land outside the project.\n */\nexport function resolveInsideRoot(projectRoot: string, target: string): string {\n const root = path.resolve(projectRoot);\n const abs = path.resolve(root, ...target.split(\"/\"));\n const prefix = root.endsWith(path.sep) ? root : root + path.sep;\n if (abs !== root && !abs.startsWith(prefix)) {\n throw new CliError(\n `refusing to write outside the project: \"${target}\" resolves to ${abs} (possible path-traversal in the workflow manifest)`,\n { code: \"E_UNSAFE_PATH\" },\n );\n }\n return abs;\n}\n\n/** Map a package-relative path into the chosen install directory. */\nexport function mapTarget(packagePath: string, installDir: string): string {\n const payloadPrefix = `${PAYLOAD_DIR}/`;\n if (installDir === PAYLOAD_DIR) return packagePath;\n if (packagePath === PAYLOAD_DIR) return installDir;\n if (packagePath.startsWith(payloadPrefix)) {\n return `${installDir}/${packagePath.slice(payloadPrefix.length)}`;\n }\n return packagePath;\n}\n\nfunction readExisting(projectRoot: string, target: string): Uint8Array | null {\n const abs = path.join(projectRoot, ...target.split(\"/\"));\n try {\n if (!fs.existsSync(abs)) return null;\n return new Uint8Array(fs.readFileSync(abs));\n } catch {\n return null;\n }\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/** Build the full install plan, resolving each file's action against disk. */\nexport function planInstall(\n projectRoot: string,\n files: { path: string; bytes: Uint8Array; sha256?: string }[],\n installDir: string,\n): PlannedFile[] {\n return files.map((f) => {\n const target = mapTarget(f.path, installDir);\n const existing = readExisting(projectRoot, target);\n let action: FileAction = \"create\";\n if (existing) action = bytesEqual(existing, f.bytes) ? \"identical\" : \"overwrite\";\n return {\n packagePath: f.path,\n target,\n bytes: f.bytes,\n sha256: f.sha256 ?? sha256Hex(f.bytes),\n action,\n };\n });\n}\n\n/** True when any planned file would overwrite a differing existing file. */\nexport function hasConflicts(plan: PlannedFile[]): boolean {\n return plan.some((p) => p.action === \"overwrite\");\n}\n\nexport function toPreviewPlan(plan: PlannedFile[]): PreviewFilePlanEntry[] {\n return plan.map((p) => ({ target: p.target, action: p.action }));\n}\n\nconst decoder = new TextDecoder(\"utf-8\", { fatal: false });\n\n/** A unified diff between the existing file and the incoming bytes (text only). */\nexport function renderDiff(projectRoot: string, planned: PlannedFile): string {\n const existing = readExisting(projectRoot, planned.target);\n const oldText = existing ? decoder.decode(existing) : \"\";\n const newText = decoder.decode(planned.bytes);\n return createTwoFilesPatch(\n `a/${planned.target}`,\n `b/${planned.target}`,\n oldText,\n newText,\n \"current\",\n \"incoming\",\n );\n}\n\n/** Write every planned file byte-for-byte, creating parent directories. */\nexport async function writePlan(projectRoot: string, plan: PlannedFile[]): Promise<void> {\n // Assert containment for EVERY target before writing a single byte, so a\n // traversal entry aborts the whole install atomically (§8 / C1).\n const targets = plan\n .filter((p) => p.action !== \"identical\")\n .map((p) => ({ p, abs: resolveInsideRoot(projectRoot, p.target) }));\n for (const { p, abs } of targets) {\n await fsp.mkdir(path.dirname(abs), { recursive: true });\n await fsp.writeFile(abs, p.bytes);\n }\n}\n\n/**\n * Generate the post-install output (spec §5.4.5): manifest notes plus which env\n * vars to set and the build/run commands per sandbox provider.\n */\nexport function postInstallNotes(\n manifest: Manifest,\n installDir: string,\n): string[] {\n const out: string[] = [];\n const entryRel = manifest.sandcastle.entrypoint.replace(new RegExp(`^${PAYLOAD_DIR}/`), \"\");\n const entryTarget = `${installDir}/${entryRel}`;\n const envTarget = `${installDir}/.env`;\n\n const requiredEnv = manifest.env.filter((e) => e.required).map((e) => e.name);\n if (requiredEnv.length > 0) {\n out.push(`1. Set required env vars in ${envTarget}: ${requiredEnv.join(\", \")}`);\n } else {\n out.push(`1. Review ${envTarget} for any env vars you need to set.`);\n }\n\n const providers = manifest.sandboxProviders.length > 0 ? manifest.sandboxProviders : [\"docker\"];\n for (const provider of providers) {\n if (provider === \"docker\" || provider === \"podman\") {\n out.push(`2. Build the image: npx @ai-hero/sandcastle ${provider} build-image`);\n } else if (provider === \"vercel\") {\n out.push(`2. This workflow targets the Vercel sandbox — see the Sandcastle docs for setup.`);\n } else {\n out.push(`2. This workflow uses a custom sandbox — see its README.`);\n }\n }\n out.push(`3. Run it: npx tsx ${entryTarget}`);\n\n if (manifest.postInstallNotes) {\n out.push(\"\");\n out.push(`Author notes: ${manifest.postInstallNotes}`);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/sandcastle-version.ts","../src/lib/preview.ts"],"sourcesContent":["// Best-effort detection of the user's installed @ai-hero/sandcastle version so\n// `add` can warn when it falls outside the workflow's tested versionRange\n// (spec §5.4.3a). Reads (in order) the installed package's package.json, then\n// the consumer's own package.json dependency ranges.\n\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport semver from \"semver\";\nimport { SANDCASTLE_PACKAGE_NAME } from \"@castellan/schema\";\n\n/** Return the concrete installed version, if resolvable. */\nasync function installedVersion(cwd: string): Promise<string | undefined> {\n const pkgJson = path.join(cwd, \"node_modules\", ...SANDCASTLE_PACKAGE_NAME.split(\"/\"), \"package.json\");\n try {\n const raw = await fsp.readFile(pkgJson, \"utf8\");\n const v = (JSON.parse(raw) as { version?: string }).version;\n if (v && semver.valid(v)) return v;\n } catch {\n /* not installed */\n }\n return undefined;\n}\n\n/** Return the declared dependency range from the consumer's package.json. */\nasync function declaredRange(cwd: string): Promise<string | undefined> {\n try {\n const raw = await fsp.readFile(path.join(cwd, \"package.json\"), \"utf8\");\n const pkg = JSON.parse(raw) as Record<string, Record<string, string> | undefined>;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"] as const) {\n const range = pkg[field]?.[SANDCASTLE_PACKAGE_NAME];\n if (range) return range;\n }\n } catch {\n /* no package.json */\n }\n return undefined;\n}\n\nexport interface SandcastleDetection {\n /** Concrete installed version, if found in node_modules. */\n version?: string;\n /** Declared dependency range from package.json, if present. */\n range?: string;\n}\n\nexport async function detectSandcastle(cwd: string): Promise<SandcastleDetection> {\n const [version, range] = await Promise.all([installedVersion(cwd), declaredRange(cwd)]);\n return {\n ...(version ? { version } : {}),\n ...(range ? { range } : {}),\n };\n}\n\n/**\n * Decide whether the user's Sandcastle is compatible with a workflow's\n * versionRange. Returns a human note when there is a mismatch, else undefined.\n */\nexport function compatibilityNote(\n workflowRange: string,\n detected: SandcastleDetection,\n): string | undefined {\n const { version, range } = detected;\n if (version && semver.valid(version) && semver.validRange(workflowRange)) {\n if (!semver.satisfies(version, workflowRange)) {\n return `Your installed ${SANDCASTLE_PACKAGE_NAME} is ${version}, outside this workflow's tested range ${workflowRange}.`;\n }\n return undefined;\n }\n if (range && semver.validRange(range) && semver.validRange(workflowRange)) {\n // Compare ranges via their lowest satisfying versions — coarse but useful.\n const min = semver.minVersion(range);\n if (min && !semver.satisfies(min, workflowRange)) {\n return `Your ${SANDCASTLE_PACKAGE_NAME} dependency (${range}) may fall outside this workflow's tested range ${workflowRange}.`;\n }\n }\n return undefined;\n}\n","// The install preview (spec §5.4.3) — a SECURITY SURFACE. Snapshot-tested;\n// changes require review. Pure and deterministic: given a manifest and a file\n// plan, it returns the exact text shown before any bytes are written. Color is\n// applied only when `colorize` is true so snapshots stay stable.\n\nimport type { Manifest } from \"@castellan/schema\";\nimport pc from \"picocolors\";\n\nexport type FileAction = \"create\" | \"overwrite\" | \"identical\";\n\nexport interface PreviewFilePlanEntry {\n /** Target path relative to the project root (post-namespacing). */\n target: string;\n action: FileAction;\n}\n\nexport interface PreviewInput {\n manifest: Manifest;\n author: string;\n version: string;\n /** Compatibility warning about the user's installed Sandcastle, if any. */\n compatibilityNote?: string | undefined;\n filePlan: PreviewFilePlanEntry[];\n /** Install destination dir relative to project root (e.g. \".sandcastle/<slug>\"). */\n installDir: string;\n /** True for `github:` installs — checksums are client-side, no tamper evidence (M1). */\n githubMode?: boolean;\n colorize?: boolean;\n}\n\nfunction pad(s: string, width: number): string {\n return s.length >= width ? s : s + \" \".repeat(width - s.length);\n}\n\nexport function renderPreview(input: PreviewInput): string {\n const { manifest, author, version } = input;\n const c = input.colorize\n ? pc\n : ({\n bold: (s: string) => s,\n dim: (s: string) => s,\n yellow: (s: string) => s,\n green: (s: string) => s,\n red: (s: string) => s,\n cyan: (s: string) => s,\n } as unknown as typeof pc);\n\n const lines: string[] = [];\n const rule = \"─\".repeat(60);\n\n // --- a. Summary -----------------------------------------------------------\n lines.push(c.bold(`${manifest.name}`));\n lines.push(`${c.dim(\"by\")} ${author} ${c.dim(\"·\")} v${version} ${c.dim(\"·\")} ${manifest.license}`);\n if (manifest.description) lines.push(c.dim(manifest.description));\n lines.push(\"\");\n lines.push(`${c.dim(\"Sandcastle range:\")} ${manifest.sandcastle.versionRange}`);\n if (input.compatibilityNote) {\n lines.push(c.yellow(`⚠ ${input.compatibilityNote}`));\n }\n if (manifest.agents.length > 0) {\n const agents = manifest.agents\n .map((a) => `${a.role} (${a.provider} ${a.model}${a.parallel ? \", parallel\" : \"\"})`)\n .join(\", \");\n lines.push(`${c.dim(\"Agents:\")} ${agents}`);\n }\n if (manifest.sandboxProviders.length > 0) {\n lines.push(`${c.dim(\"Sandbox:\")} ${manifest.sandboxProviders.join(\", \")}`);\n }\n\n // --- b. Required env vars -------------------------------------------------\n if (manifest.env.length > 0) {\n lines.push(\"\");\n lines.push(c.bold(\"Environment variables\"));\n const nameW = Math.max(4, ...manifest.env.map((e) => e.name.length));\n for (const e of manifest.env) {\n const req = e.required ? c.red(\"required\") : c.dim(\"optional\");\n const desc = e.description ? ` ${c.dim(e.description)}` : \"\";\n lines.push(` ${pad(e.name, nameW)} ${req}${desc}`);\n }\n }\n\n // --- c. Disclosures block (prominent / yellow) ----------------------------\n lines.push(\"\");\n lines.push(c.yellow(c.bold(\"⚠ Disclosures — what this workflow can do\")));\n lines.push(c.yellow(rule));\n const d = manifest.disclosures;\n lines.push(\n ` ${c.yellow(\"Host hooks:\")} ${d.hostHooks.length ? d.hostHooks.map((h) => `\\`${h}\\``).join(\", \") : c.dim(\"none\")}`,\n );\n lines.push(\n ` ${c.yellow(\"Sandbox hooks:\")} ${d.sandboxHooks.length ? d.sandboxHooks.map((h) => `\\`${h}\\``).join(\", \") : c.dim(\"none\")}`,\n );\n lines.push(` ${c.yellow(\"Network access:\")} ${d.networkAccess ?? c.dim(\"none declared\")}`);\n lines.push(\n ` ${c.yellow(\"Shell expansion:\")} ${d.usesShellExpansion ? c.red(\"yes — prompt files run shell commands at load time\") : c.dim(\"no\")}`,\n );\n lines.push(c.yellow(rule));\n\n // --- d. File plan ---------------------------------------------------------\n lines.push(\"\");\n lines.push(c.bold(`Files (into ${input.installDir}/)`));\n for (const f of input.filePlan) {\n const tag =\n f.action === \"create\"\n ? c.green(\"create \")\n : f.action === \"overwrite\"\n ? c.yellow(\"overwrite\")\n : c.dim(\"identical\");\n lines.push(` ${tag} ${f.target}`);\n }\n\n if (input.githubMode) {\n lines.push(\"\");\n lines.push(\n c.dim(\n \"Note: installing from github: — checksums are computed client-side from the fetched files (no registry tamper-evidence).\",\n ),\n );\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|