runcastle 0.1.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 +298 -0
- package/dist/add-PRGLHAVL.js.map +1 -0
- package/dist/chunk-44KDPUYJ.js +49 -0
- package/dist/chunk-44KDPUYJ.js.map +1 -0
- package/dist/chunk-5VPKMXSV.js +190 -0
- package/dist/chunk-5VPKMXSV.js.map +1 -0
- package/dist/chunk-6CWCR5SO.js +24 -0
- package/dist/chunk-6CWCR5SO.js.map +1 -0
- package/dist/chunk-AHRTPV2C.js +1082 -0
- package/dist/chunk-AHRTPV2C.js.map +1 -0
- package/dist/chunk-LP5MV76P.js +33 -0
- package/dist/chunk-LP5MV76P.js.map +1 -0
- package/dist/chunk-OJ356IMJ.js +136 -0
- package/dist/chunk-OJ356IMJ.js.map +1 -0
- package/dist/chunk-PRNBOQHW.js +32 -0
- package/dist/chunk-PRNBOQHW.js.map +1 -0
- package/dist/chunk-SNUH2UT6.js +95 -0
- package/dist/chunk-SNUH2UT6.js.map +1 -0
- package/dist/chunk-UCYVC3H6.js +108 -0
- package/dist/chunk-UCYVC3H6.js.map +1 -0
- package/dist/chunk-Y5P5BQWF.js +136 -0
- package/dist/chunk-Y5P5BQWF.js.map +1 -0
- package/dist/chunk-ZQ3BK4ZD.js +134 -0
- package/dist/chunk-ZQ3BK4ZD.js.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/info-U6AZZ2TV.js +78 -0
- package/dist/info-U6AZZ2TV.js.map +1 -0
- package/dist/init-MOJNLWXK.js +212 -0
- package/dist/init-MOJNLWXK.js.map +1 -0
- package/dist/logout-YPGZ6BB6.js +27 -0
- package/dist/logout-YPGZ6BB6.js.map +1 -0
- package/dist/publish-W37Y6TO6.js +211 -0
- package/dist/publish-W37Y6TO6.js.map +1 -0
- package/dist/search-2KNAXPM5.js +55 -0
- package/dist/search-2KNAXPM5.js.map +1 -0
- package/dist/update-NIIUPL2E.js +123 -0
- package/dist/update-NIIUPL2E.js.map +1 -0
- package/dist/validate-GFDSEVPV.js +12 -0
- package/dist/validate-GFDSEVPV.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PAYLOAD_DIR
|
|
3
|
+
} from "./chunk-AHRTPV2C.js";
|
|
4
|
+
import {
|
|
5
|
+
CliError
|
|
6
|
+
} from "./chunk-6CWCR5SO.js";
|
|
7
|
+
|
|
8
|
+
// src/lib/checksum.ts
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
function sha256Hex(bytes) {
|
|
11
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/lib/installer.ts
|
|
15
|
+
import fsp from "fs/promises";
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { createTwoFilesPatch } from "diff";
|
|
19
|
+
function resolveInsideRoot(projectRoot, target) {
|
|
20
|
+
const root = path.resolve(projectRoot);
|
|
21
|
+
const abs = path.resolve(root, ...target.split("/"));
|
|
22
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
23
|
+
if (abs !== root && !abs.startsWith(prefix)) {
|
|
24
|
+
throw new CliError(
|
|
25
|
+
`refusing to write outside the project: "${target}" resolves to ${abs} (possible path-traversal in the workflow manifest)`,
|
|
26
|
+
{ code: "E_UNSAFE_PATH" }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return abs;
|
|
30
|
+
}
|
|
31
|
+
function mapTarget(packagePath, installDir) {
|
|
32
|
+
const payloadPrefix = `${PAYLOAD_DIR}/`;
|
|
33
|
+
if (installDir === PAYLOAD_DIR) return packagePath;
|
|
34
|
+
if (packagePath === PAYLOAD_DIR) return installDir;
|
|
35
|
+
if (packagePath.startsWith(payloadPrefix)) {
|
|
36
|
+
return `${installDir}/${packagePath.slice(payloadPrefix.length)}`;
|
|
37
|
+
}
|
|
38
|
+
return packagePath;
|
|
39
|
+
}
|
|
40
|
+
function readExisting(projectRoot, target) {
|
|
41
|
+
const abs = path.join(projectRoot, ...target.split("/"));
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(abs)) return null;
|
|
44
|
+
return new Uint8Array(fs.readFileSync(abs));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function bytesEqual(a, b) {
|
|
50
|
+
if (a.length !== b.length) return false;
|
|
51
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function planInstall(projectRoot, files, installDir) {
|
|
55
|
+
return files.map((f) => {
|
|
56
|
+
const target = mapTarget(f.path, installDir);
|
|
57
|
+
const existing = readExisting(projectRoot, target);
|
|
58
|
+
let action = "create";
|
|
59
|
+
if (existing) action = bytesEqual(existing, f.bytes) ? "identical" : "overwrite";
|
|
60
|
+
return {
|
|
61
|
+
packagePath: f.path,
|
|
62
|
+
target,
|
|
63
|
+
bytes: f.bytes,
|
|
64
|
+
sha256: f.sha256 ?? sha256Hex(f.bytes),
|
|
65
|
+
action
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function hasConflicts(plan) {
|
|
70
|
+
return plan.some((p) => p.action === "overwrite");
|
|
71
|
+
}
|
|
72
|
+
function toPreviewPlan(plan) {
|
|
73
|
+
return plan.map((p) => ({ target: p.target, action: p.action }));
|
|
74
|
+
}
|
|
75
|
+
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
76
|
+
function renderDiff(projectRoot, planned) {
|
|
77
|
+
const existing = readExisting(projectRoot, planned.target);
|
|
78
|
+
const oldText = existing ? decoder.decode(existing) : "";
|
|
79
|
+
const newText = decoder.decode(planned.bytes);
|
|
80
|
+
return createTwoFilesPatch(
|
|
81
|
+
`a/${planned.target}`,
|
|
82
|
+
`b/${planned.target}`,
|
|
83
|
+
oldText,
|
|
84
|
+
newText,
|
|
85
|
+
"current",
|
|
86
|
+
"incoming"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
async function writePlan(projectRoot, plan) {
|
|
90
|
+
const targets = plan.filter((p) => p.action !== "identical").map((p) => ({ p, abs: resolveInsideRoot(projectRoot, p.target) }));
|
|
91
|
+
for (const { p, abs } of targets) {
|
|
92
|
+
await fsp.mkdir(path.dirname(abs), { recursive: true });
|
|
93
|
+
await fsp.writeFile(abs, p.bytes);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function postInstallNotes(manifest, installDir) {
|
|
97
|
+
const out = [];
|
|
98
|
+
const entryRel = manifest.sandcastle.entrypoint.replace(new RegExp(`^${PAYLOAD_DIR}/`), "");
|
|
99
|
+
const entryTarget = `${installDir}/${entryRel}`;
|
|
100
|
+
const envTarget = `${installDir}/.env`;
|
|
101
|
+
const requiredEnv = manifest.env.filter((e) => e.required).map((e) => e.name);
|
|
102
|
+
if (requiredEnv.length > 0) {
|
|
103
|
+
out.push(`1. Set required env vars in ${envTarget}: ${requiredEnv.join(", ")}`);
|
|
104
|
+
} else {
|
|
105
|
+
out.push(`1. Review ${envTarget} for any env vars you need to set.`);
|
|
106
|
+
}
|
|
107
|
+
const providers = manifest.sandboxProviders.length > 0 ? manifest.sandboxProviders : ["docker"];
|
|
108
|
+
for (const provider of providers) {
|
|
109
|
+
if (provider === "docker" || provider === "podman") {
|
|
110
|
+
out.push(`2. Build the image: npx @ai-hero/sandcastle ${provider} build-image`);
|
|
111
|
+
} else if (provider === "vercel") {
|
|
112
|
+
out.push(`2. This workflow targets the Vercel sandbox \u2014 see the Sandcastle docs for setup.`);
|
|
113
|
+
} else {
|
|
114
|
+
out.push(`2. This workflow uses a custom sandbox \u2014 see its README.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
out.push(`3. Run it: npx tsx ${entryTarget}`);
|
|
118
|
+
if (manifest.postInstallNotes) {
|
|
119
|
+
out.push("");
|
|
120
|
+
out.push(`Author notes: ${manifest.postInstallNotes}`);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
sha256Hex,
|
|
127
|
+
resolveInsideRoot,
|
|
128
|
+
mapTarget,
|
|
129
|
+
planInstall,
|
|
130
|
+
hasConflicts,
|
|
131
|
+
toPreviewPlan,
|
|
132
|
+
renderDiff,
|
|
133
|
+
writePlan,
|
|
134
|
+
postInstallNotes
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=chunk-Y5P5BQWF.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 * 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":[]}
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
// src/lib/preview.ts
|
|
56
|
+
import pc from "picocolors";
|
|
57
|
+
function pad(s, width) {
|
|
58
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
59
|
+
}
|
|
60
|
+
function renderPreview(input) {
|
|
61
|
+
const { manifest, author, version } = input;
|
|
62
|
+
const c = input.colorize ? pc : {
|
|
63
|
+
bold: (s) => s,
|
|
64
|
+
dim: (s) => s,
|
|
65
|
+
yellow: (s) => s,
|
|
66
|
+
green: (s) => s,
|
|
67
|
+
red: (s) => s,
|
|
68
|
+
cyan: (s) => s
|
|
69
|
+
};
|
|
70
|
+
const lines = [];
|
|
71
|
+
const rule = "\u2500".repeat(60);
|
|
72
|
+
lines.push(c.bold(`${manifest.name}`));
|
|
73
|
+
lines.push(`${c.dim("by")} ${author} ${c.dim("\xB7")} v${version} ${c.dim("\xB7")} ${manifest.license}`);
|
|
74
|
+
if (manifest.description) lines.push(c.dim(manifest.description));
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push(`${c.dim("Sandcastle range:")} ${manifest.sandcastle.versionRange}`);
|
|
77
|
+
if (input.compatibilityNote) {
|
|
78
|
+
lines.push(c.yellow(`\u26A0 ${input.compatibilityNote}`));
|
|
79
|
+
}
|
|
80
|
+
if (manifest.agents.length > 0) {
|
|
81
|
+
const agents = manifest.agents.map((a) => `${a.role} (${a.provider} ${a.model}${a.parallel ? ", parallel" : ""})`).join(", ");
|
|
82
|
+
lines.push(`${c.dim("Agents:")} ${agents}`);
|
|
83
|
+
}
|
|
84
|
+
if (manifest.sandboxProviders.length > 0) {
|
|
85
|
+
lines.push(`${c.dim("Sandbox:")} ${manifest.sandboxProviders.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
if (manifest.env.length > 0) {
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push(c.bold("Environment variables"));
|
|
90
|
+
const nameW = Math.max(4, ...manifest.env.map((e) => e.name.length));
|
|
91
|
+
for (const e of manifest.env) {
|
|
92
|
+
const req = e.required ? c.red("required") : c.dim("optional");
|
|
93
|
+
const desc = e.description ? ` ${c.dim(e.description)}` : "";
|
|
94
|
+
lines.push(` ${pad(e.name, nameW)} ${req}${desc}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push(c.yellow(c.bold("\u26A0 Disclosures \u2014 what this workflow can do")));
|
|
99
|
+
lines.push(c.yellow(rule));
|
|
100
|
+
const d = manifest.disclosures;
|
|
101
|
+
lines.push(
|
|
102
|
+
` ${c.yellow("Host hooks:")} ${d.hostHooks.length ? d.hostHooks.map((h) => `\`${h}\``).join(", ") : c.dim("none")}`
|
|
103
|
+
);
|
|
104
|
+
lines.push(
|
|
105
|
+
` ${c.yellow("Sandbox hooks:")} ${d.sandboxHooks.length ? d.sandboxHooks.map((h) => `\`${h}\``).join(", ") : c.dim("none")}`
|
|
106
|
+
);
|
|
107
|
+
lines.push(` ${c.yellow("Network access:")} ${d.networkAccess ?? c.dim("none declared")}`);
|
|
108
|
+
lines.push(
|
|
109
|
+
` ${c.yellow("Shell expansion:")} ${d.usesShellExpansion ? c.red("yes \u2014 prompt files run shell commands at load time") : c.dim("no")}`
|
|
110
|
+
);
|
|
111
|
+
lines.push(c.yellow(rule));
|
|
112
|
+
lines.push("");
|
|
113
|
+
lines.push(c.bold(`Files (into ${input.installDir}/)`));
|
|
114
|
+
for (const f of input.filePlan) {
|
|
115
|
+
const tag = f.action === "create" ? c.green("create ") : f.action === "overwrite" ? c.yellow("overwrite") : c.dim("identical");
|
|
116
|
+
lines.push(` ${tag} ${f.target}`);
|
|
117
|
+
}
|
|
118
|
+
if (input.githubMode) {
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push(
|
|
121
|
+
c.dim(
|
|
122
|
+
"Note: installing from github: \u2014 checksums are computed client-side from the fetched files (no registry tamper-evidence)."
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
detectSandcastle,
|
|
131
|
+
compatibilityNote,
|
|
132
|
+
renderPreview
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=chunk-ZQ3BK4ZD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isCliError
|
|
4
|
+
} from "./chunk-6CWCR5SO.js";
|
|
5
|
+
import {
|
|
6
|
+
logErr,
|
|
7
|
+
pc,
|
|
8
|
+
setJsonMode
|
|
9
|
+
} from "./chunk-PRNBOQHW.js";
|
|
10
|
+
|
|
11
|
+
// src/index.ts
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
function fail(e) {
|
|
14
|
+
if (isCliError(e)) {
|
|
15
|
+
if (e.message) logErr(`${pc.red("error")} ${e.message}`);
|
|
16
|
+
for (const d of e.details) if (d) logErr(` ${pc.dim(d)}`);
|
|
17
|
+
process.exitCode = e.exitCode;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
logErr(`${pc.red("error")} ${e?.message ?? String(e)}`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
|
23
|
+
function ctx(command) {
|
|
24
|
+
const opts = command.optsWithGlobals();
|
|
25
|
+
setJsonMode(Boolean(opts.json));
|
|
26
|
+
return opts;
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const program = new Command();
|
|
30
|
+
program.name("runcastle").description("Install and publish Sandcastle workflow packages").version("0.1.0").option("--json", "machine-readable JSON output").option("--registry <url>", "registry base URL").option("-y, --yes", "skip confirmation prompts");
|
|
31
|
+
program.command("init").description("Scaffold a workflow.json from an existing .sandcastle/ directory").action(async (_o, cmd) => {
|
|
32
|
+
const g = ctx(cmd);
|
|
33
|
+
const { initCommand } = await import("./init-MOJNLWXK.js");
|
|
34
|
+
await initCommand({ ...g.yes ? { yes: true } : {} });
|
|
35
|
+
});
|
|
36
|
+
program.command("validate").description("Validate a workflow package (exit 0/1)").argument("[path]", "package directory", ".").action(async (pathArg, _o, cmd) => {
|
|
37
|
+
ctx(cmd);
|
|
38
|
+
const { validateCommand } = await import("./validate-GFDSEVPV.js");
|
|
39
|
+
await validateCommand({ path: pathArg });
|
|
40
|
+
});
|
|
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
|
+
const g = ctx(cmd);
|
|
43
|
+
const { addCommand } = await import("./add-PRGLHAVL.js");
|
|
44
|
+
await addCommand(spec, {
|
|
45
|
+
...o.dir ? { dir: o.dir } : {},
|
|
46
|
+
...o.dryRun ? { dryRun: true } : {},
|
|
47
|
+
...g.yes ? { yes: true } : {},
|
|
48
|
+
...g.json ? { json: true } : {},
|
|
49
|
+
...g.registry ? { registry: g.registry } : {}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
program.command("info").description("Show a workflow's preview without installing").argument("<spec>", "author/slug[@version]").action(async (spec, _o, cmd) => {
|
|
53
|
+
const g = ctx(cmd);
|
|
54
|
+
const { infoCommand } = await import("./info-U6AZZ2TV.js");
|
|
55
|
+
await infoCommand(spec, {
|
|
56
|
+
...g.json ? { json: true } : {},
|
|
57
|
+
...g.registry ? { registry: g.registry } : {}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
program.command("search").description("Search the registry").argument("[query]", "search text", "").option("--sort <sort>", "downloads | newest | name").action(async (query, o, cmd) => {
|
|
61
|
+
const g = ctx(cmd);
|
|
62
|
+
const { searchCommand } = await import("./search-2KNAXPM5.js");
|
|
63
|
+
await searchCommand(query, {
|
|
64
|
+
...o.sort ? { sort: o.sort } : {},
|
|
65
|
+
...g.json ? { json: true } : {},
|
|
66
|
+
...g.registry ? { registry: g.registry } : {}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
program.command("publish").description("Validate, authenticate, and register the workflow at the current commit").action(async (_o, cmd) => {
|
|
70
|
+
const g = ctx(cmd);
|
|
71
|
+
const { publishCommand } = await import("./publish-W37Y6TO6.js");
|
|
72
|
+
await publishCommand({
|
|
73
|
+
...g.yes ? { yes: true } : {},
|
|
74
|
+
...g.json ? { json: true } : {},
|
|
75
|
+
...g.registry ? { registry: g.registry } : {}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
program.command("update").description("Update installed workflows to newer versions (unmodified files only)").action(async (_o, cmd) => {
|
|
79
|
+
const g = ctx(cmd);
|
|
80
|
+
const { updateCommand } = await import("./update-NIIUPL2E.js");
|
|
81
|
+
await updateCommand({
|
|
82
|
+
...g.json ? { json: true } : {},
|
|
83
|
+
...g.registry ? { registry: g.registry } : {}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
program.command("logout").description("Remove the cached GitHub token").action(async (_o, cmd) => {
|
|
87
|
+
ctx(cmd);
|
|
88
|
+
const { logoutCommand } = await import("./logout-YPGZ6BB6.js");
|
|
89
|
+
await logoutCommand();
|
|
90
|
+
});
|
|
91
|
+
await program.parseAsync(process.argv);
|
|
92
|
+
}
|
|
93
|
+
main().catch(fail);
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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.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(\"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":[]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compatibilityNote,
|
|
3
|
+
detectSandcastle,
|
|
4
|
+
renderPreview
|
|
5
|
+
} from "./chunk-ZQ3BK4ZD.js";
|
|
6
|
+
import {
|
|
7
|
+
mapTarget
|
|
8
|
+
} from "./chunk-Y5P5BQWF.js";
|
|
9
|
+
import {
|
|
10
|
+
RegistryClient
|
|
11
|
+
} from "./chunk-5VPKMXSV.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveRegistry
|
|
14
|
+
} from "./chunk-LP5MV76P.js";
|
|
15
|
+
import {
|
|
16
|
+
PAYLOAD_DIR
|
|
17
|
+
} from "./chunk-AHRTPV2C.js";
|
|
18
|
+
import {
|
|
19
|
+
CliError
|
|
20
|
+
} from "./chunk-6CWCR5SO.js";
|
|
21
|
+
import {
|
|
22
|
+
emitJson,
|
|
23
|
+
isJsonMode,
|
|
24
|
+
log,
|
|
25
|
+
pc
|
|
26
|
+
} from "./chunk-PRNBOQHW.js";
|
|
27
|
+
|
|
28
|
+
// src/commands/info.ts
|
|
29
|
+
import fs from "fs";
|
|
30
|
+
import path from "path";
|
|
31
|
+
async function infoCommand(spec, opts) {
|
|
32
|
+
const atIdx = spec.indexOf("@");
|
|
33
|
+
const namepart = atIdx === -1 ? spec : spec.slice(0, atIdx);
|
|
34
|
+
const inlineVersion = atIdx === -1 ? "" : spec.slice(atIdx + 1);
|
|
35
|
+
const segs = namepart.split("/").filter(Boolean);
|
|
36
|
+
if (segs.length !== 2) {
|
|
37
|
+
throw new CliError(`invalid spec "${spec}" \u2014 expected author/slug[@version]`, { code: "E_BAD_SPEC" });
|
|
38
|
+
}
|
|
39
|
+
const [author, slug] = segs;
|
|
40
|
+
const version = opts.version ?? inlineVersion;
|
|
41
|
+
const registry = new RegistryClient({
|
|
42
|
+
baseUrl: resolveRegistry(opts.registry),
|
|
43
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
44
|
+
});
|
|
45
|
+
const record = await registry.getWorkflow(author, slug, version);
|
|
46
|
+
if (isJsonMode()) {
|
|
47
|
+
emitJson(record);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
const filePlan = record.files.map((f) => {
|
|
52
|
+
const target = mapTarget(f.path, PAYLOAD_DIR);
|
|
53
|
+
const exists = fs.existsSync(path.join(cwd, ...target.split("/")));
|
|
54
|
+
return { target, action: exists ? "overwrite" : "create" };
|
|
55
|
+
});
|
|
56
|
+
const detected = await detectSandcastle(cwd);
|
|
57
|
+
const note = compatibilityNote(record.manifest.sandcastle.versionRange, detected);
|
|
58
|
+
log(
|
|
59
|
+
renderPreview({
|
|
60
|
+
manifest: record.manifest,
|
|
61
|
+
author: record.author,
|
|
62
|
+
version: record.version,
|
|
63
|
+
compatibilityNote: note,
|
|
64
|
+
filePlan,
|
|
65
|
+
installDir: PAYLOAD_DIR,
|
|
66
|
+
colorize: pc.isColorSupported
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
log("");
|
|
70
|
+
log(pc.dim(`Install: npx runcastle add ${record.author}/${record.slug}${version ? `@${record.version}` : ""}`));
|
|
71
|
+
if (record.versions.length > 1) {
|
|
72
|
+
log(pc.dim(`Versions: ${record.versions.join(", ")}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
infoCommand
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=info-U6AZZ2TV.js.map
|
|
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;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":[]}
|