@ai-hero/sandcastle 0.6.5 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -54
- package/dist/{MountConfig.d.ts → MountConfig-CmXclHA5.d.ts} +3 -2
- package/dist/{SandboxProvider.d.ts → SandboxProvider-EkSMuBp8.d.ts} +25 -39
- package/dist/chunk-72UVAC7B.js +99 -0
- package/dist/chunk-72UVAC7B.js.map +1 -0
- package/dist/chunk-BIWNFKGV.js +22 -0
- package/dist/chunk-BIWNFKGV.js.map +1 -0
- package/dist/chunk-NGBM7T3E.js +76 -0
- package/dist/chunk-NGBM7T3E.js.map +1 -0
- package/dist/chunk-Q5W3WQVU.js +25569 -0
- package/dist/chunk-Q5W3WQVU.js.map +1 -0
- package/dist/chunk-UPDEQ2U7.js +362 -0
- package/dist/chunk-UPDEQ2U7.js.map +1 -0
- package/dist/chunk-Z7O2WNRU.js +26934 -0
- package/dist/chunk-Z7O2WNRU.js.map +1 -0
- package/dist/index.d.ts +920 -22
- package/dist/index.js +3212 -9
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19256 -13
- package/dist/main.js.map +1 -1
- package/dist/mountUtils-CCA-bbpK.d.ts +25 -0
- package/dist/sandboxes/daytona.d.ts +8 -5
- package/dist/sandboxes/daytona.js +118 -124
- package/dist/sandboxes/daytona.js.map +1 -1
- package/dist/sandboxes/docker.d.ts +10 -8
- package/dist/sandboxes/docker.js +8 -255
- package/dist/sandboxes/docker.js.map +1 -1
- package/dist/sandboxes/no-sandbox.d.ts +7 -4
- package/dist/sandboxes/no-sandbox.js +6 -114
- package/dist/sandboxes/no-sandbox.js.map +1 -1
- package/dist/sandboxes/podman.d.ts +10 -8
- package/dist/sandboxes/podman.js +287 -297
- package/dist/sandboxes/podman.js.map +1 -1
- package/dist/sandboxes/vercel.d.ts +7 -4
- package/dist/sandboxes/vercel.js +144 -165
- package/dist/sandboxes/vercel.js.map +1 -1
- package/package.json +15 -14
- package/dist/AgentProvider.d.ts +0 -134
- package/dist/AgentProvider.d.ts.map +0 -1
- package/dist/AgentProvider.js +0 -647
- package/dist/AgentProvider.js.map +0 -1
- package/dist/AgentStreamEmitter.d.ts +0 -36
- package/dist/AgentStreamEmitter.d.ts.map +0 -1
- package/dist/AgentStreamEmitter.js +0 -21
- package/dist/AgentStreamEmitter.js.map +0 -1
- package/dist/CopyToWorktree.d.ts +0 -15
- package/dist/CopyToWorktree.d.ts.map +0 -1
- package/dist/CopyToWorktree.js +0 -60
- package/dist/CopyToWorktree.js.map +0 -1
- package/dist/Display.d.ts +0 -58
- package/dist/Display.d.ts.map +0 -1
- package/dist/Display.js +0 -142
- package/dist/Display.js.map +0 -1
- package/dist/DockerLifecycle.d.ts +0 -54
- package/dist/DockerLifecycle.d.ts.map +0 -1
- package/dist/DockerLifecycle.js +0 -123
- package/dist/DockerLifecycle.js.map +0 -1
- package/dist/EnvResolver.d.ts +0 -11
- package/dist/EnvResolver.d.ts.map +0 -1
- package/dist/EnvResolver.js +0 -63
- package/dist/EnvResolver.js.map +0 -1
- package/dist/ErrorHandler.d.ts +0 -15
- package/dist/ErrorHandler.d.ts.map +0 -1
- package/dist/ErrorHandler.js +0 -85
- package/dist/ErrorHandler.js.map +0 -1
- package/dist/InitService.d.ts +0 -92
- package/dist/InitService.d.ts.map +0 -1
- package/dist/InitService.js +0 -836
- package/dist/InitService.js.map +0 -1
- package/dist/MountConfig.d.ts.map +0 -1
- package/dist/MountConfig.js +0 -7
- package/dist/MountConfig.js.map +0 -1
- package/dist/Orchestrator.d.ts +0 -56
- package/dist/Orchestrator.d.ts.map +0 -1
- package/dist/Orchestrator.js +0 -293
- package/dist/Orchestrator.js.map +0 -1
- package/dist/Output.d.ts +0 -107
- package/dist/Output.d.ts.map +0 -1
- package/dist/Output.js +0 -95
- package/dist/Output.js.map +0 -1
- package/dist/PodmanLifecycle.d.ts +0 -17
- package/dist/PodmanLifecycle.d.ts.map +0 -1
- package/dist/PodmanLifecycle.js +0 -45
- package/dist/PodmanLifecycle.js.map +0 -1
- package/dist/PromptArgumentSubstitution.d.ts +0 -32
- package/dist/PromptArgumentSubstitution.d.ts.map +0 -1
- package/dist/PromptArgumentSubstitution.js +0 -104
- package/dist/PromptArgumentSubstitution.js.map +0 -1
- package/dist/PromptPreprocessor.d.ts +0 -15
- package/dist/PromptPreprocessor.d.ts.map +0 -1
- package/dist/PromptPreprocessor.js +0 -55
- package/dist/PromptPreprocessor.js.map +0 -1
- package/dist/PromptResolver.d.ts +0 -21
- package/dist/PromptResolver.d.ts.map +0 -1
- package/dist/PromptResolver.js +0 -27
- package/dist/PromptResolver.js.map +0 -1
- package/dist/RecoveryMessage.d.ts +0 -15
- package/dist/RecoveryMessage.d.ts.map +0 -1
- package/dist/RecoveryMessage.js +0 -81
- package/dist/RecoveryMessage.js.map +0 -1
- package/dist/SandboxFactory.d.ts +0 -90
- package/dist/SandboxFactory.d.ts.map +0 -1
- package/dist/SandboxFactory.js +0 -324
- package/dist/SandboxFactory.js.map +0 -1
- package/dist/SandboxLifecycle.d.ts +0 -65
- package/dist/SandboxLifecycle.d.ts.map +0 -1
- package/dist/SandboxLifecycle.js +0 -296
- package/dist/SandboxLifecycle.js.map +0 -1
- package/dist/SandboxProvider.d.ts.map +0 -1
- package/dist/SandboxProvider.js +0 -28
- package/dist/SandboxProvider.js.map +0 -1
- package/dist/SessionStore.d.ts +0 -110
- package/dist/SessionStore.d.ts.map +0 -1
- package/dist/SessionStore.js +0 -330
- package/dist/SessionStore.js.map +0 -1
- package/dist/TextDeltaBuffer.d.ts +0 -24
- package/dist/TextDeltaBuffer.d.ts.map +0 -1
- package/dist/TextDeltaBuffer.js +0 -68
- package/dist/TextDeltaBuffer.js.map +0 -1
- package/dist/WorktreeManager.d.ts +0 -79
- package/dist/WorktreeManager.d.ts.map +0 -1
- package/dist/WorktreeManager.js +0 -283
- package/dist/WorktreeManager.js.map +0 -1
- package/dist/boundedTail.d.ts +0 -48
- package/dist/boundedTail.d.ts.map +0 -1
- package/dist/boundedTail.js +0 -64
- package/dist/boundedTail.js.map +0 -1
- package/dist/cli.d.ts +0 -30
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -340
- package/dist/cli.js.map +0 -1
- package/dist/createSandbox.d.ts +0 -154
- package/dist/createSandbox.d.ts.map +0 -1
- package/dist/createSandbox.js +0 -476
- package/dist/createSandbox.js.map +0 -1
- package/dist/createWorktree.d.ts +0 -154
- package/dist/createWorktree.d.ts.map +0 -1
- package/dist/createWorktree.js +0 -391
- package/dist/createWorktree.js.map +0 -1
- package/dist/errors.d.ts +0 -227
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -81
- package/dist/errors.js.map +0 -1
- package/dist/extractStructuredOutput.d.ts +0 -23
- package/dist/extractStructuredOutput.d.ts.map +0 -1
- package/dist/extractStructuredOutput.js +0 -102
- package/dist/extractStructuredOutput.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interactive.d.ts +0 -74
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js +0 -279
- package/dist/interactive.js.map +0 -1
- package/dist/main.d.ts.map +0 -1
- package/dist/mergeProviderEnv.d.ts +0 -13
- package/dist/mergeProviderEnv.d.ts.map +0 -1
- package/dist/mergeProviderEnv.js +0 -23
- package/dist/mergeProviderEnv.js.map +0 -1
- package/dist/mountUtils.d.ts +0 -146
- package/dist/mountUtils.d.ts.map +0 -1
- package/dist/mountUtils.js +0 -301
- package/dist/mountUtils.js.map +0 -1
- package/dist/raceAbortSignal.d.ts +0 -18
- package/dist/raceAbortSignal.d.ts.map +0 -1
- package/dist/raceAbortSignal.js +0 -32
- package/dist/raceAbortSignal.js.map +0 -1
- package/dist/resolveCwd.d.ts +0 -24
- package/dist/resolveCwd.d.ts.map +0 -1
- package/dist/resolveCwd.js +0 -32
- package/dist/resolveCwd.js.map +0 -1
- package/dist/resumePrecheck.d.ts +0 -26
- package/dist/resumePrecheck.d.ts.map +0 -1
- package/dist/resumePrecheck.js +0 -40
- package/dist/resumePrecheck.js.map +0 -1
- package/dist/run.d.ts +0 -216
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -313
- package/dist/run.js.map +0 -1
- package/dist/sandboxExec.d.ts +0 -12
- package/dist/sandboxExec.d.ts.map +0 -1
- package/dist/sandboxExec.js +0 -26
- package/dist/sandboxExec.js.map +0 -1
- package/dist/sandboxes/daytona.d.ts.map +0 -1
- package/dist/sandboxes/docker.d.ts.map +0 -1
- package/dist/sandboxes/no-sandbox.d.ts.map +0 -1
- package/dist/sandboxes/podman.d.ts.map +0 -1
- package/dist/sandboxes/test-bind-mount.d.ts +0 -17
- package/dist/sandboxes/test-bind-mount.d.ts.map +0 -1
- package/dist/sandboxes/test-bind-mount.js +0 -92
- package/dist/sandboxes/test-bind-mount.js.map +0 -1
- package/dist/sandboxes/test-isolated.d.ts +0 -17
- package/dist/sandboxes/test-isolated.d.ts.map +0 -1
- package/dist/sandboxes/test-isolated.js +0 -98
- package/dist/sandboxes/test-isolated.js.map +0 -1
- package/dist/sandboxes/vercel.d.ts.map +0 -1
- package/dist/shutdownRegistry.d.ts +0 -30
- package/dist/shutdownRegistry.d.ts.map +0 -1
- package/dist/shutdownRegistry.js +0 -73
- package/dist/shutdownRegistry.js.map +0 -1
- package/dist/startSandbox.d.ts +0 -50
- package/dist/startSandbox.d.ts.map +0 -1
- package/dist/startSandbox.js +0 -117
- package/dist/startSandbox.js.map +0 -1
- package/dist/syncIn.d.ts +0 -24
- package/dist/syncIn.d.ts.map +0 -1
- package/dist/syncIn.js +0 -107
- package/dist/syncIn.js.map +0 -1
- package/dist/syncOut.d.ts +0 -27
- package/dist/syncOut.d.ts.map +0 -1
- package/dist/syncOut.js +0 -271
- package/dist/syncOut.js.map +0 -1
- package/dist/templates.d.ts +0 -2
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -26
- package/dist/templates.js.map +0 -1
- package/dist/terminalCleanup.d.ts +0 -30
- package/dist/terminalCleanup.d.ts.map +0 -1
- package/dist/terminalCleanup.js +0 -37
- package/dist/terminalCleanup.js.map +0 -1
- package/dist/testSandbox.d.ts +0 -8
- package/dist/testSandbox.d.ts.map +0 -1
- package/dist/testSandbox.js +0 -109
- package/dist/testSandbox.js.map +0 -1
- package/dist/testSetup.d.ts +0 -2
- package/dist/testSetup.d.ts.map +0 -1
- package/dist/testSetup.js +0 -29
- package/dist/testSetup.js.map +0 -1
package/dist/InitService.js
DELETED
|
@@ -1,836 +0,0 @@
|
|
|
1
|
-
import { FileSystem } from "@effect/platform";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { SANDBOX_REPO_DIR } from "./SandboxFactory.js";
|
|
6
|
-
const GITIGNORE = `.env
|
|
7
|
-
logs/
|
|
8
|
-
worktrees/
|
|
9
|
-
`;
|
|
10
|
-
/**
|
|
11
|
-
* Filename of the setup prompt scaffolded for the `custom` issue tracker.
|
|
12
|
-
* Both the per-agent `setupCommand` and the in-scaffold sentinels point at it,
|
|
13
|
-
* so it is defined once here.
|
|
14
|
-
*/
|
|
15
|
-
const SETUP_ISSUE_TRACKER_DOC = "SETUP_ISSUE_TRACKER.md";
|
|
16
|
-
const SETUP_ISSUE_TRACKER_PATH = `.sandcastle/${SETUP_ISSUE_TRACKER_DOC}`;
|
|
17
|
-
const TEMPLATES = [
|
|
18
|
-
{
|
|
19
|
-
name: "blank",
|
|
20
|
-
description: "Bare scaffold — write your own prompt and orchestration",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
name: "simple-loop",
|
|
24
|
-
description: "Picks issues one by one and closes them",
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "sequential-reviewer",
|
|
28
|
-
description: "Implements issues one by one, with a code review step after each",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "parallel-planner",
|
|
32
|
-
description: "Plans parallelizable issues, executes on separate branches, merges",
|
|
33
|
-
dependencies: ["zod"],
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: "parallel-planner-with-review",
|
|
37
|
-
description: "Plans parallelizable issues, executes with per-branch review, merges",
|
|
38
|
-
dependencies: ["zod"],
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
|
-
export const listTemplates = () => TEMPLATES;
|
|
42
|
-
/**
|
|
43
|
-
* Host-side npm packages the given template imports directly. Empty when the
|
|
44
|
-
* template name is unknown or the template declares no extra dependencies.
|
|
45
|
-
*/
|
|
46
|
-
export const getTemplateDependencies = (templateName) => TEMPLATES.find((t) => t.name === templateName)?.dependencies ?? [];
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Package manager detection (internal — not part of public API)
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
51
|
-
// Lockfiles checked in priority order. bun.lock / bun.lockb are both valid bun
|
|
52
|
-
// lockfiles (text vs binary), so both map to bun.
|
|
53
|
-
const LOCKFILES = [
|
|
54
|
-
["bun.lockb", "bun"],
|
|
55
|
-
["bun.lock", "bun"],
|
|
56
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
57
|
-
["yarn.lock", "yarn"],
|
|
58
|
-
["package-lock.json", "npm"],
|
|
59
|
-
];
|
|
60
|
-
/**
|
|
61
|
-
* Detect the host project's package manager. An explicit corepack-style
|
|
62
|
-
* `packageManager` field in package.json wins; otherwise the first matching
|
|
63
|
-
* lockfile decides. Defaults to npm when nothing matches.
|
|
64
|
-
*/
|
|
65
|
-
export const detectPackageManager = (repoDir) => Effect.gen(function* () {
|
|
66
|
-
const fs = yield* FileSystem.FileSystem;
|
|
67
|
-
const pkgPath = join(repoDir, "package.json");
|
|
68
|
-
const pkgExists = yield* fs
|
|
69
|
-
.exists(pkgPath)
|
|
70
|
-
.pipe(Effect.orElseSucceed(() => false));
|
|
71
|
-
if (pkgExists) {
|
|
72
|
-
const content = yield* fs
|
|
73
|
-
.readFileString(pkgPath)
|
|
74
|
-
.pipe(Effect.orElseSucceed(() => ""));
|
|
75
|
-
try {
|
|
76
|
-
const pkg = JSON.parse(content);
|
|
77
|
-
const field = pkg["packageManager"];
|
|
78
|
-
if (typeof field === "string") {
|
|
79
|
-
const name = field.split("@")[0];
|
|
80
|
-
const match = PACKAGE_MANAGERS.find((pm) => pm === name);
|
|
81
|
-
if (match)
|
|
82
|
-
return match;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
// Malformed package.json — fall through to lockfile detection.
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
for (const [file, pm] of LOCKFILES) {
|
|
90
|
-
const exists = yield* fs
|
|
91
|
-
.exists(join(repoDir, file))
|
|
92
|
-
.pipe(Effect.orElseSucceed(() => false));
|
|
93
|
-
if (exists)
|
|
94
|
-
return pm;
|
|
95
|
-
}
|
|
96
|
-
return "npm";
|
|
97
|
-
});
|
|
98
|
-
/** Build the command that adds a runtime dependency for the given package manager. */
|
|
99
|
-
export const addDependencyCommand = (packageManager, pkg) => {
|
|
100
|
-
switch (packageManager) {
|
|
101
|
-
case "pnpm":
|
|
102
|
-
return `pnpm add ${pkg}`;
|
|
103
|
-
case "yarn":
|
|
104
|
-
return `yarn add ${pkg}`;
|
|
105
|
-
case "bun":
|
|
106
|
-
return `bun add ${pkg}`;
|
|
107
|
-
case "npm":
|
|
108
|
-
return `npm install ${pkg}`;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* Whether the host package.json already declares `pkg` in any of its dependency
|
|
113
|
-
* maps. Used so init doesn't offer to install something already present.
|
|
114
|
-
*/
|
|
115
|
-
export const hostHasDependency = (repoDir, pkg) => Effect.gen(function* () {
|
|
116
|
-
const fs = yield* FileSystem.FileSystem;
|
|
117
|
-
const pkgPath = join(repoDir, "package.json");
|
|
118
|
-
const exists = yield* fs
|
|
119
|
-
.exists(pkgPath)
|
|
120
|
-
.pipe(Effect.orElseSucceed(() => false));
|
|
121
|
-
if (!exists)
|
|
122
|
-
return false;
|
|
123
|
-
const content = yield* fs
|
|
124
|
-
.readFileString(pkgPath)
|
|
125
|
-
.pipe(Effect.orElseSucceed(() => ""));
|
|
126
|
-
try {
|
|
127
|
-
const parsed = JSON.parse(content);
|
|
128
|
-
const depMaps = [
|
|
129
|
-
"dependencies",
|
|
130
|
-
"devDependencies",
|
|
131
|
-
"peerDependencies",
|
|
132
|
-
"optionalDependencies",
|
|
133
|
-
];
|
|
134
|
-
return depMaps.some((key) => {
|
|
135
|
-
const deps = parsed[key];
|
|
136
|
-
return (typeof deps === "object" && deps !== null && pkg in deps);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
const CLAUDE_CODE_DOCKERFILE = `FROM node:22-bookworm
|
|
144
|
-
|
|
145
|
-
# Install system dependencies
|
|
146
|
-
RUN apt-get update && apt-get install -y \\
|
|
147
|
-
git \\
|
|
148
|
-
curl \\
|
|
149
|
-
jq \\
|
|
150
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
151
|
-
|
|
152
|
-
{{ISSUE_TRACKER_TOOLS}}
|
|
153
|
-
|
|
154
|
-
# Build-args for UID/GID alignment: sandcastle docker build-image
|
|
155
|
-
# defaults these to the host user's UID/GID so image-built files
|
|
156
|
-
# and bind-mounted files share an owner without runtime chown.
|
|
157
|
-
ARG AGENT_UID=1000
|
|
158
|
-
ARG AGENT_GID=1000
|
|
159
|
-
|
|
160
|
-
# Rename the base image's "node" user to "agent" and align UID/GID.
|
|
161
|
-
RUN groupmod -o -g $AGENT_GID node && usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
162
|
-
USER \${AGENT_UID}:\${AGENT_GID}
|
|
163
|
-
|
|
164
|
-
# Install Claude Code CLI
|
|
165
|
-
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
166
|
-
|
|
167
|
-
# Add Claude to PATH
|
|
168
|
-
ENV PATH="/home/agent/.local/bin:$PATH"
|
|
169
|
-
|
|
170
|
-
WORKDIR /home/agent
|
|
171
|
-
|
|
172
|
-
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
|
|
173
|
-
# and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
|
|
174
|
-
# Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
|
|
175
|
-
ENTRYPOINT ["sleep", "infinity"]
|
|
176
|
-
`;
|
|
177
|
-
const PI_DOCKERFILE = `FROM node:22-bookworm
|
|
178
|
-
|
|
179
|
-
# Install system dependencies
|
|
180
|
-
RUN apt-get update && apt-get install -y \\
|
|
181
|
-
git \\
|
|
182
|
-
curl \\
|
|
183
|
-
jq \\
|
|
184
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
185
|
-
|
|
186
|
-
{{ISSUE_TRACKER_TOOLS}}
|
|
187
|
-
|
|
188
|
-
# Build-args for UID/GID alignment: sandcastle docker build-image
|
|
189
|
-
# defaults these to the host user's UID/GID so image-built files
|
|
190
|
-
# and bind-mounted files share an owner without runtime chown.
|
|
191
|
-
ARG AGENT_UID=1000
|
|
192
|
-
ARG AGENT_GID=1000
|
|
193
|
-
|
|
194
|
-
# Rename the base image's "node" user to "agent" and align UID/GID.
|
|
195
|
-
RUN groupmod -o -g $AGENT_GID node && usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
196
|
-
|
|
197
|
-
# Install pi coding agent (run as root before USER agent)
|
|
198
|
-
RUN npm install -g @mariozechner/pi-coding-agent
|
|
199
|
-
|
|
200
|
-
USER \${AGENT_UID}:\${AGENT_GID}
|
|
201
|
-
|
|
202
|
-
WORKDIR /home/agent
|
|
203
|
-
|
|
204
|
-
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
|
|
205
|
-
# and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
|
|
206
|
-
# Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
|
|
207
|
-
ENTRYPOINT ["sleep", "infinity"]
|
|
208
|
-
`;
|
|
209
|
-
const CODEX_DOCKERFILE = `FROM node:22-bookworm
|
|
210
|
-
|
|
211
|
-
# Install system dependencies
|
|
212
|
-
RUN apt-get update && apt-get install -y \\
|
|
213
|
-
git \\
|
|
214
|
-
curl \\
|
|
215
|
-
jq \\
|
|
216
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
217
|
-
|
|
218
|
-
{{ISSUE_TRACKER_TOOLS}}
|
|
219
|
-
|
|
220
|
-
# Build-args for UID/GID alignment: sandcastle docker build-image
|
|
221
|
-
# defaults these to the host user's UID/GID so image-built files
|
|
222
|
-
# and bind-mounted files share an owner without runtime chown.
|
|
223
|
-
ARG AGENT_UID=1000
|
|
224
|
-
ARG AGENT_GID=1000
|
|
225
|
-
|
|
226
|
-
# Rename the base image's "node" user to "agent" and align UID/GID.
|
|
227
|
-
RUN groupmod -o -g $AGENT_GID node && usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
228
|
-
|
|
229
|
-
# Install Codex CLI (run as root before USER agent)
|
|
230
|
-
RUN npm install -g @openai/codex
|
|
231
|
-
|
|
232
|
-
USER \${AGENT_UID}:\${AGENT_GID}
|
|
233
|
-
|
|
234
|
-
WORKDIR /home/agent
|
|
235
|
-
|
|
236
|
-
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
|
|
237
|
-
# and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
|
|
238
|
-
# Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
|
|
239
|
-
ENTRYPOINT ["sleep", "infinity"]
|
|
240
|
-
`;
|
|
241
|
-
const CURSOR_DOCKERFILE = `FROM node:22-bookworm
|
|
242
|
-
|
|
243
|
-
# Install system dependencies
|
|
244
|
-
RUN apt-get update && apt-get install -y \\
|
|
245
|
-
git \\
|
|
246
|
-
curl \\
|
|
247
|
-
jq \\
|
|
248
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
249
|
-
|
|
250
|
-
{{ISSUE_TRACKER_TOOLS}}
|
|
251
|
-
|
|
252
|
-
# Build-args for UID/GID alignment: sandcastle docker build-image
|
|
253
|
-
# defaults these to the host user's UID/GID so image-built files
|
|
254
|
-
# and bind-mounted files share an owner without runtime chown.
|
|
255
|
-
ARG AGENT_UID=1000
|
|
256
|
-
ARG AGENT_GID=1000
|
|
257
|
-
|
|
258
|
-
# Rename the base image's "node" user to "agent" and align UID/GID.
|
|
259
|
-
RUN groupmod -g $AGENT_GID node && usermod -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
260
|
-
USER \${AGENT_UID}:\${AGENT_GID}
|
|
261
|
-
|
|
262
|
-
# Install Cursor Agent CLI
|
|
263
|
-
RUN curl https://cursor.com/install -fsS | bash
|
|
264
|
-
|
|
265
|
-
# Add Cursor CLI to PATH
|
|
266
|
-
ENV PATH="/home/agent/.local/bin:$PATH"
|
|
267
|
-
|
|
268
|
-
WORKDIR /home/agent
|
|
269
|
-
|
|
270
|
-
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
|
|
271
|
-
# and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
|
|
272
|
-
# Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
|
|
273
|
-
ENTRYPOINT ["sleep", "infinity"]
|
|
274
|
-
`;
|
|
275
|
-
const OPENCODE_DOCKERFILE = `FROM node:22-bookworm
|
|
276
|
-
|
|
277
|
-
# Install system dependencies
|
|
278
|
-
RUN apt-get update && apt-get install -y \\
|
|
279
|
-
git \\
|
|
280
|
-
curl \\
|
|
281
|
-
jq \\
|
|
282
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
283
|
-
|
|
284
|
-
{{ISSUE_TRACKER_TOOLS}}
|
|
285
|
-
|
|
286
|
-
# Build-args for UID/GID alignment: sandcastle docker build-image
|
|
287
|
-
# defaults these to the host user's UID/GID so image-built files
|
|
288
|
-
# and bind-mounted files share an owner without runtime chown.
|
|
289
|
-
ARG AGENT_UID=1000
|
|
290
|
-
ARG AGENT_GID=1000
|
|
291
|
-
|
|
292
|
-
# Rename the base image's "node" user to "agent" and align UID/GID.
|
|
293
|
-
RUN groupmod -o -g $AGENT_GID node && usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
294
|
-
|
|
295
|
-
# Install OpenCode CLI (run as root before USER agent)
|
|
296
|
-
RUN npm install -g opencode-ai@latest
|
|
297
|
-
|
|
298
|
-
USER \${AGENT_UID}:\${AGENT_GID}
|
|
299
|
-
|
|
300
|
-
WORKDIR /home/agent
|
|
301
|
-
|
|
302
|
-
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at \${SANDBOX_REPO_DIR}
|
|
303
|
-
# and overrides the working directory to \${SANDBOX_REPO_DIR} at container start.
|
|
304
|
-
# Structure your Dockerfile so that \${SANDBOX_REPO_DIR} can serve as the project root.
|
|
305
|
-
ENTRYPOINT ["sleep", "infinity"]
|
|
306
|
-
`;
|
|
307
|
-
const COPILOT_DOCKERFILE = `FROM node:22-bookworm
|
|
308
|
-
|
|
309
|
-
# Install system dependencies
|
|
310
|
-
RUN apt-get update && apt-get install -y \\
|
|
311
|
-
git \\
|
|
312
|
-
curl \\
|
|
313
|
-
jq \\
|
|
314
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
315
|
-
|
|
316
|
-
{{ISSUE_TRACKER_TOOLS}}
|
|
317
|
-
|
|
318
|
-
# Build-args for UID/GID alignment: sandcastle docker build-image
|
|
319
|
-
# defaults these to the host user's UID/GID so image-built files
|
|
320
|
-
# and bind-mounted files share an owner without runtime chown.
|
|
321
|
-
ARG AGENT_UID=1000
|
|
322
|
-
ARG AGENT_GID=1000
|
|
323
|
-
|
|
324
|
-
# Rename the base image's "node" user to "agent" and align UID/GID.
|
|
325
|
-
RUN groupmod -o -g $AGENT_GID node && usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node
|
|
326
|
-
|
|
327
|
-
# Install GitHub Copilot CLI (run as root before USER agent)
|
|
328
|
-
RUN npm install -g @github/copilot
|
|
329
|
-
|
|
330
|
-
USER \${AGENT_UID}:\${AGENT_GID}
|
|
331
|
-
|
|
332
|
-
WORKDIR /home/agent
|
|
333
|
-
|
|
334
|
-
# In worktree sandbox mode, Sandcastle bind-mounts the git worktree at \${SANDBOX_REPO_DIR}
|
|
335
|
-
# and overrides the working directory to \${SANDBOX_REPO_DIR} at container start.
|
|
336
|
-
# Structure your Dockerfile so that \${SANDBOX_REPO_DIR} can serve as the project root.
|
|
337
|
-
ENTRYPOINT ["sleep", "infinity"]
|
|
338
|
-
`;
|
|
339
|
-
const AGENT_REGISTRY = [
|
|
340
|
-
{
|
|
341
|
-
name: "claude-code",
|
|
342
|
-
label: "Claude Code",
|
|
343
|
-
defaultModel: "claude-opus-4-7",
|
|
344
|
-
factoryImport: "claudeCode",
|
|
345
|
-
dockerfileTemplate: CLAUDE_CODE_DOCKERFILE,
|
|
346
|
-
envExample: `# Anthropic API key
|
|
347
|
-
# If you want to use your Claude subscription instead of an API key, see https://github.com/mattpocock/sandcastle/issues/191
|
|
348
|
-
ANTHROPIC_API_KEY=`,
|
|
349
|
-
setupCommand: `claude "$(cat ${SETUP_ISSUE_TRACKER_PATH})"`,
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
name: "pi",
|
|
353
|
-
label: "Pi",
|
|
354
|
-
defaultModel: "claude-sonnet-4-6",
|
|
355
|
-
factoryImport: "pi",
|
|
356
|
-
dockerfileTemplate: PI_DOCKERFILE,
|
|
357
|
-
envExample: `# Anthropic API key
|
|
358
|
-
ANTHROPIC_API_KEY=`,
|
|
359
|
-
setupCommand: `pi "$(cat ${SETUP_ISSUE_TRACKER_PATH})"`,
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
name: "codex",
|
|
363
|
-
label: "Codex",
|
|
364
|
-
defaultModel: "gpt-5.4-mini",
|
|
365
|
-
factoryImport: "codex",
|
|
366
|
-
dockerfileTemplate: CODEX_DOCKERFILE,
|
|
367
|
-
envExample: `# OpenAI API key
|
|
368
|
-
OPENAI_KEY=`,
|
|
369
|
-
setupCommand: `codex "$(cat ${SETUP_ISSUE_TRACKER_PATH})"`,
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
name: "cursor",
|
|
373
|
-
label: "Cursor",
|
|
374
|
-
defaultModel: "composer-2",
|
|
375
|
-
factoryImport: "cursor",
|
|
376
|
-
dockerfileTemplate: CURSOR_DOCKERFILE,
|
|
377
|
-
envExample: `# Cursor API key (recommended)
|
|
378
|
-
# You can also pass --api-key directly to the agent CLI.
|
|
379
|
-
CURSOR_API_KEY=`,
|
|
380
|
-
setupCommand: `agent "$(cat ${SETUP_ISSUE_TRACKER_PATH})"`,
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
name: "opencode",
|
|
384
|
-
label: "OpenCode",
|
|
385
|
-
defaultModel: "opencode/big-pickle",
|
|
386
|
-
factoryImport: "opencode",
|
|
387
|
-
dockerfileTemplate: OPENCODE_DOCKERFILE,
|
|
388
|
-
envExample: `# OpenCode API key
|
|
389
|
-
OPENCODE_API_KEY=`,
|
|
390
|
-
setupCommand: `opencode -p "$(cat ${SETUP_ISSUE_TRACKER_PATH})"`,
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
name: "copilot",
|
|
394
|
-
label: "GitHub Copilot CLI",
|
|
395
|
-
defaultModel: "claude-sonnet-4.5",
|
|
396
|
-
factoryImport: "copilot",
|
|
397
|
-
dockerfileTemplate: COPILOT_DOCKERFILE,
|
|
398
|
-
envExample: `# GitHub token with the "Copilot Requests" permission
|
|
399
|
-
# (a fine-grained PAT, or any token from \`gh auth login\`).
|
|
400
|
-
# COPILOT_GITHUB_TOKEN takes precedence over GH_TOKEN and GITHUB_TOKEN.
|
|
401
|
-
GITHUB_TOKEN=`,
|
|
402
|
-
setupCommand: `copilot -i "$(cat ${SETUP_ISSUE_TRACKER_PATH})"`,
|
|
403
|
-
},
|
|
404
|
-
];
|
|
405
|
-
export const listAgents = () => AGENT_REGISTRY;
|
|
406
|
-
const GITHUB_CLI_TOOLS = `# Install GitHub CLI
|
|
407
|
-
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\
|
|
408
|
-
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\
|
|
409
|
-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \\
|
|
410
|
-
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\
|
|
411
|
-
&& apt-get update && apt-get install -y gh \\
|
|
412
|
-
&& rm -rf /var/lib/apt/lists/*`;
|
|
413
|
-
const BEADS_TOOLS = `# Install system dependencies for Beads
|
|
414
|
-
RUN apt-get update && apt-get install -y \\
|
|
415
|
-
dpkg-dev \\
|
|
416
|
-
libicu72 \\
|
|
417
|
-
&& rm -rf /var/lib/apt/lists/* \\
|
|
418
|
-
&& ARCH_DIR=$(dpkg-architecture -qDEB_HOST_MULTIARCH) \\
|
|
419
|
-
&& for lib in /usr/lib/$ARCH_DIR/libicu*.so.72; do \\
|
|
420
|
-
ln -s "$lib" "\${lib%.72}.74"; \\
|
|
421
|
-
done
|
|
422
|
-
|
|
423
|
-
RUN curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
|
|
424
|
-
|
|
425
|
-
RUN corepack enable`;
|
|
426
|
-
// Sentinels baked into the scaffold for the `custom` issue tracker. The
|
|
427
|
-
// project ships deliberately broken-until-configured; the setup agent finds
|
|
428
|
-
// and replaces these markers in place (see SETUP_ISSUE_TRACKER.md). Defined as
|
|
429
|
-
// shared constants so the registry entry and the setup doc stay in sync.
|
|
430
|
-
const CUSTOM_LIST_TASKS_SENTINEL = `echo 'No issue tracker configured — run ${SETUP_ISSUE_TRACKER_PATH} through your coding agent.' >&2; exit 1`;
|
|
431
|
-
const CUSTOM_VIEW_TASK_MARKER = `<view command — see ${SETUP_ISSUE_TRACKER_PATH}>`;
|
|
432
|
-
const CUSTOM_CLOSE_TASK_MARKER = `<close command — see ${SETUP_ISSUE_TRACKER_PATH}>`;
|
|
433
|
-
const CUSTOM_TRACKER_TOOLS = `# TODO: install your issue tracker's CLI here. See ${SETUP_ISSUE_TRACKER_PATH}`;
|
|
434
|
-
const CUSTOM_ENV_EXAMPLE = `# TODO: add any env vars your issue tracker needs (e.g. an API token).
|
|
435
|
-
# See ${SETUP_ISSUE_TRACKER_PATH}`;
|
|
436
|
-
const ISSUE_TRACKER_REGISTRY = [
|
|
437
|
-
{
|
|
438
|
-
name: "github-issues",
|
|
439
|
-
label: "GitHub Issues",
|
|
440
|
-
templateArgs: {
|
|
441
|
-
LIST_TASKS_COMMAND: `gh issue list --state open --label Sandcastle --limit 100 --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'`,
|
|
442
|
-
VIEW_TASK_COMMAND: "gh issue view <ID>",
|
|
443
|
-
CLOSE_TASK_COMMAND: `gh issue close <ID> --comment "Completed by Sandcastle"`,
|
|
444
|
-
ISSUE_TRACKER_TOOLS: GITHUB_CLI_TOOLS,
|
|
445
|
-
},
|
|
446
|
-
envExample: `# GitHub personal access token — the agent uses it to read and manage GitHub Issues
|
|
447
|
-
# Create a fine-grained token: https://github.com/settings/personal-access-tokens/new
|
|
448
|
-
# Required repository permissions: Issues (Read and write) and Metadata (Read)
|
|
449
|
-
GH_TOKEN=`,
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
name: "beads",
|
|
453
|
-
label: "Beads",
|
|
454
|
-
templateArgs: {
|
|
455
|
-
LIST_TASKS_COMMAND: "bd ready --json",
|
|
456
|
-
VIEW_TASK_COMMAND: "bd show <ID>",
|
|
457
|
-
CLOSE_TASK_COMMAND: `bd close <ID> --reason="Completed by Sandcastle"`,
|
|
458
|
-
ISSUE_TRACKER_TOOLS: BEADS_TOOLS,
|
|
459
|
-
},
|
|
460
|
-
envExample: "",
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
name: "custom",
|
|
464
|
-
label: "Custom",
|
|
465
|
-
templateArgs: {
|
|
466
|
-
// The only real shell expression: PromptPreprocessor fails the run on a
|
|
467
|
-
// non-zero exit and surfaces stderr, so this is the single enforcement
|
|
468
|
-
// point that keeps the scaffold broken until the user configures it.
|
|
469
|
-
LIST_TASKS_COMMAND: CUSTOM_LIST_TASKS_SENTINEL,
|
|
470
|
-
// Inline text markers — replaced by the setup agent, never executed.
|
|
471
|
-
VIEW_TASK_COMMAND: CUSTOM_VIEW_TASK_MARKER,
|
|
472
|
-
CLOSE_TASK_COMMAND: CUSTOM_CLOSE_TASK_MARKER,
|
|
473
|
-
ISSUE_TRACKER_TOOLS: CUSTOM_TRACKER_TOOLS,
|
|
474
|
-
},
|
|
475
|
-
envExample: CUSTOM_ENV_EXAMPLE,
|
|
476
|
-
},
|
|
477
|
-
];
|
|
478
|
-
export const listIssueTrackers = () => ISSUE_TRACKER_REGISTRY;
|
|
479
|
-
export const getIssueTracker = (name) => ISSUE_TRACKER_REGISTRY.find((b) => b.name === name);
|
|
480
|
-
export const getAgent = (name) => AGENT_REGISTRY.find((a) => a.name === name);
|
|
481
|
-
const SANDBOX_PROVIDER_REGISTRY = [
|
|
482
|
-
{
|
|
483
|
-
name: "docker",
|
|
484
|
-
label: "Docker",
|
|
485
|
-
containerfileName: "Dockerfile",
|
|
486
|
-
cliNamespace: "docker",
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
name: "podman",
|
|
490
|
-
label: "Podman",
|
|
491
|
-
containerfileName: "Containerfile",
|
|
492
|
-
cliNamespace: "podman",
|
|
493
|
-
},
|
|
494
|
-
];
|
|
495
|
-
export const listSandboxProviders = () => SANDBOX_PROVIDER_REGISTRY;
|
|
496
|
-
export const getSandboxProvider = (name) => SANDBOX_PROVIDER_REGISTRY.find((p) => p.name === name);
|
|
497
|
-
// ---------------------------------------------------------------------------
|
|
498
|
-
// Next steps
|
|
499
|
-
// ---------------------------------------------------------------------------
|
|
500
|
-
export function getNextStepsLines(template, mainFilename, issueTracker, agent, packageManager) {
|
|
501
|
-
// The custom issue tracker scaffolds a broken-until-configured project, so
|
|
502
|
-
// its next steps are about running the setup prompt — not the template's
|
|
503
|
-
// normal "set env vars and go" flow. This branch wins over template-specific
|
|
504
|
-
// steps regardless of the chosen template.
|
|
505
|
-
if (issueTracker.name === "custom") {
|
|
506
|
-
return [
|
|
507
|
-
"Next steps:",
|
|
508
|
-
"1. Your custom issue tracker isn't wired up yet — runs hard-fail until you configure it.",
|
|
509
|
-
`2. Feed the setup prompt to ${agent.label} on your host to finish wiring it up:`,
|
|
510
|
-
` ${agent.setupCommand}`,
|
|
511
|
-
` (Runs on the host — you need the ${agent.label} CLI installed locally, since the sandbox image isn't built yet.)`,
|
|
512
|
-
`3. Follow .sandcastle/${SETUP_ISSUE_TRACKER_DOC} to edit the scaffolded files in place, build the image, and verify.`,
|
|
513
|
-
];
|
|
514
|
-
}
|
|
515
|
-
if (template === "blank") {
|
|
516
|
-
return [
|
|
517
|
-
"Next steps:",
|
|
518
|
-
`1. Set the required env vars in .sandcastle/.env (see .sandcastle/.env.example)`,
|
|
519
|
-
" If you want to use your Claude subscription instead of an API key, see https://github.com/mattpocock/sandcastle/issues/191",
|
|
520
|
-
"2. Read and customize .sandcastle/prompt.md to describe what you want the agent to do",
|
|
521
|
-
`3. Customize .sandcastle/${mainFilename} — it uses the JS API (\`run()\`) to control how the agent runs`,
|
|
522
|
-
`4. Add "sandcastle": "npx tsx .sandcastle/${mainFilename}" to your package.json scripts`,
|
|
523
|
-
"5. Run `npm run sandcastle` to start the agent",
|
|
524
|
-
];
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
const hasReviewer = template.includes("review");
|
|
528
|
-
const usesPlanSchema = getTemplateDependencies(template).includes("zod");
|
|
529
|
-
let step = 1;
|
|
530
|
-
const lines = [
|
|
531
|
-
"Next steps:",
|
|
532
|
-
`${step++}. Set the required env vars in .sandcastle/.env (see .sandcastle/.env.example)`,
|
|
533
|
-
" If you want to use your Claude subscription instead of an API key, see https://github.com/mattpocock/sandcastle/issues/191",
|
|
534
|
-
`${step++}. Add "sandcastle": "npx tsx .sandcastle/${mainFilename}" to your package.json scripts`,
|
|
535
|
-
`${step++}. Templates use \`copyToWorktree: ["node_modules"]\` to copy your host node_modules into the sandbox for fast startup — the \`npm install\` in the onSandboxReady hook is a safety net for platform-specific binaries. Adjust both if you use a different package manager`,
|
|
536
|
-
];
|
|
537
|
-
if (usesPlanSchema) {
|
|
538
|
-
lines.push(`${step++}. Install a schema validator for the planner's \`<plan>\` output — the template uses Zod (\`${addDependencyCommand(packageManager, "zod")}\`), but Valibot, ArkType, or any Standard Schema library works (https://standardschema.dev)`);
|
|
539
|
-
}
|
|
540
|
-
lines.push(`${step++}. Read and customize the prompt files in .sandcastle/ — they shape what the agent does`);
|
|
541
|
-
if (hasReviewer) {
|
|
542
|
-
lines.push(`${step++}. Customize .sandcastle/CODING_STANDARDS.md with your project's standards — the reviewer agent loads it during review`);
|
|
543
|
-
}
|
|
544
|
-
lines.push(`${step++}. Run \`npm run sandcastle\` to start the agent`);
|
|
545
|
-
return lines;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
// ---------------------------------------------------------------------------
|
|
549
|
-
// Scaffolding helpers
|
|
550
|
-
// ---------------------------------------------------------------------------
|
|
551
|
-
function getTemplatesDir() {
|
|
552
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
553
|
-
return join(dirname(thisFile), "templates");
|
|
554
|
-
}
|
|
555
|
-
const getTemplateDir = (templateName) => Effect.gen(function* () {
|
|
556
|
-
const template = TEMPLATES.find((t) => t.name === templateName);
|
|
557
|
-
if (!template) {
|
|
558
|
-
const names = TEMPLATES.map((t) => t.name).join(", ");
|
|
559
|
-
yield* Effect.fail(new Error(`Unknown template: "${templateName}". Available: ${names}`));
|
|
560
|
-
}
|
|
561
|
-
return join(getTemplatesDir(), templateName);
|
|
562
|
-
});
|
|
563
|
-
const COMPILED_FILE_EXTENSIONS = [
|
|
564
|
-
".js",
|
|
565
|
-
".js.map",
|
|
566
|
-
".d.ts",
|
|
567
|
-
".d.ts.map",
|
|
568
|
-
".mjs",
|
|
569
|
-
".mjs.map",
|
|
570
|
-
".d.mts",
|
|
571
|
-
".d.mts.map",
|
|
572
|
-
];
|
|
573
|
-
const copyTemplateFiles = (templateDir, destDir, mainFilename) => Effect.gen(function* () {
|
|
574
|
-
const fs = yield* FileSystem.FileSystem;
|
|
575
|
-
const files = yield* fs
|
|
576
|
-
.readDirectory(templateDir)
|
|
577
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
578
|
-
yield* Effect.all(files
|
|
579
|
-
.filter((f) => f !== "template.json" &&
|
|
580
|
-
f !== ".env.example" &&
|
|
581
|
-
!COMPILED_FILE_EXTENSIONS.some((ext) => f.endsWith(ext)))
|
|
582
|
-
.map((f) => {
|
|
583
|
-
const destName = f === "main.mts" ? mainFilename : f;
|
|
584
|
-
return fs
|
|
585
|
-
.copyFile(join(templateDir, f), join(destDir, destName))
|
|
586
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
587
|
-
}), { concurrency: "unbounded" });
|
|
588
|
-
});
|
|
589
|
-
/**
|
|
590
|
-
* Replace the agent factory and sandbox provider in a scaffolded main.ts.
|
|
591
|
-
*
|
|
592
|
-
* Templates use `claudeCode` as the default agent factory and `docker` as the
|
|
593
|
-
* default sandbox provider. When a different agent, model, or sandbox provider
|
|
594
|
-
* is selected, this function rewrites the imports and factory calls.
|
|
595
|
-
*/
|
|
596
|
-
const rewriteMainTs = (configDir, agent, model, sandboxProvider, mainFilename) => Effect.gen(function* () {
|
|
597
|
-
const fs = yield* FileSystem.FileSystem;
|
|
598
|
-
const mainTsPath = join(configDir, mainFilename);
|
|
599
|
-
const exists = yield* fs
|
|
600
|
-
.exists(mainTsPath)
|
|
601
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
602
|
-
if (!exists)
|
|
603
|
-
return;
|
|
604
|
-
let content = yield* fs
|
|
605
|
-
.readFileString(mainTsPath)
|
|
606
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
607
|
-
// Templates use main.mts as the canonical filename in comments.
|
|
608
|
-
// When the target is main.ts, rewrite those references.
|
|
609
|
-
if (mainFilename === "main.ts") {
|
|
610
|
-
content = content.replace(/main\.mts/g, "main.ts");
|
|
611
|
-
}
|
|
612
|
-
// Replace factory function name in imports (e.g. claudeCode → pi)
|
|
613
|
-
// and all factory calls with the correct model.
|
|
614
|
-
// Templates always use claudeCode as the placeholder factory.
|
|
615
|
-
content = content.replace(/\bclaudeCode\b/g, agent.factoryImport);
|
|
616
|
-
// Replace model strings in factory calls: factoryImport("any-model")
|
|
617
|
-
const factoryCallRe = new RegExp(`${agent.factoryImport}\\(["']([^"']+)["']\\)`, "g");
|
|
618
|
-
content = content.replace(factoryCallRe, `${agent.factoryImport}("${model}")`);
|
|
619
|
-
// Replace the sandbox provider. Templates always use `docker` as the
|
|
620
|
-
// placeholder, where the registry name doubles as both the factory function
|
|
621
|
-
// name and the `/sandboxes/<name>` import subpath segment. A single
|
|
622
|
-
// case-sensitive word-boundary replace therefore rewrites the named import,
|
|
623
|
-
// the import subpath, and every factory call site — and is a no-op when
|
|
624
|
-
// docker is selected.
|
|
625
|
-
content = content.replace(/\bdocker\b/g, sandboxProvider.name);
|
|
626
|
-
yield* fs
|
|
627
|
-
.writeFileString(mainTsPath, content)
|
|
628
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
629
|
-
});
|
|
630
|
-
/**
|
|
631
|
-
* When the user opted out of the Sandcastle label, strip ` --label Sandcastle`
|
|
632
|
-
* from all `.md` files in the scaffolded config directory so that `gh issue list`
|
|
633
|
-
* commands work without a label filter.
|
|
634
|
-
*/
|
|
635
|
-
const rewritePromptFiles = (configDir) => Effect.gen(function* () {
|
|
636
|
-
const fs = yield* FileSystem.FileSystem;
|
|
637
|
-
const files = yield* fs
|
|
638
|
-
.readDirectory(configDir)
|
|
639
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
640
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
641
|
-
yield* Effect.all(mdFiles.map((f) => Effect.gen(function* () {
|
|
642
|
-
const filePath = join(configDir, f);
|
|
643
|
-
const content = yield* fs
|
|
644
|
-
.readFileString(filePath)
|
|
645
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
646
|
-
const updated = content.replace(/ --label Sandcastle/g, "");
|
|
647
|
-
if (updated !== content) {
|
|
648
|
-
yield* fs
|
|
649
|
-
.writeFileString(filePath, updated)
|
|
650
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
651
|
-
}
|
|
652
|
-
})), { concurrency: "unbounded" });
|
|
653
|
-
});
|
|
654
|
-
/** Text file extensions eligible for `{{KEY}}` template argument substitution. */
|
|
655
|
-
const TEXT_FILE_EXTENSIONS = new Set([
|
|
656
|
-
".md",
|
|
657
|
-
".txt",
|
|
658
|
-
".env",
|
|
659
|
-
".example",
|
|
660
|
-
// Dockerfile / Containerfile have no extension — handled by name check below
|
|
661
|
-
]);
|
|
662
|
-
const isTextFile = (filename) => {
|
|
663
|
-
if (filename === "Dockerfile" ||
|
|
664
|
-
filename === "Containerfile" ||
|
|
665
|
-
filename === ".gitignore")
|
|
666
|
-
return true;
|
|
667
|
-
const dotIdx = filename.lastIndexOf(".");
|
|
668
|
-
if (dotIdx === -1)
|
|
669
|
-
return false;
|
|
670
|
-
return TEXT_FILE_EXTENSIONS.has(filename.slice(dotIdx));
|
|
671
|
-
};
|
|
672
|
-
/**
|
|
673
|
-
* Replace `{{KEY}}` template arguments from the issue tracker's
|
|
674
|
-
* `templateArgs` map in all text files in the scaffolded config directory.
|
|
675
|
-
*/
|
|
676
|
-
const substituteTemplateArgs = (configDir, issueTracker) => Effect.gen(function* () {
|
|
677
|
-
const fs = yield* FileSystem.FileSystem;
|
|
678
|
-
const files = yield* fs
|
|
679
|
-
.readDirectory(configDir)
|
|
680
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
681
|
-
const textFiles = files.filter(isTextFile);
|
|
682
|
-
yield* Effect.all(textFiles.map((f) => Effect.gen(function* () {
|
|
683
|
-
const filePath = join(configDir, f);
|
|
684
|
-
let content = yield* fs
|
|
685
|
-
.readFileString(filePath)
|
|
686
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
687
|
-
const original = content;
|
|
688
|
-
for (const [key, value] of Object.entries(issueTracker.templateArgs)) {
|
|
689
|
-
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
690
|
-
}
|
|
691
|
-
if (content !== original) {
|
|
692
|
-
yield* fs
|
|
693
|
-
.writeFileString(filePath, content)
|
|
694
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
695
|
-
}
|
|
696
|
-
})), { concurrency: "unbounded" });
|
|
697
|
-
});
|
|
698
|
-
/**
|
|
699
|
-
* Build the `SETUP_ISSUE_TRACKER.md` prompt scaffolded for the `custom` issue
|
|
700
|
-
* tracker. It addresses the user's coding agent and walks it through wiring up
|
|
701
|
-
* the tracker by editing the scaffolded files in place. The build command is
|
|
702
|
-
* provider-parameterized so it names the actual CLI namespace (docker/podman).
|
|
703
|
-
*/
|
|
704
|
-
const buildSetupIssueTrackerDoc = (cliNamespace) => `# Set up your custom issue tracker
|
|
705
|
-
|
|
706
|
-
You are a coding agent. Finish wiring up the **custom issue tracker** for this Sandcastle project. It was scaffolded in a deliberately broken-until-configured state: until you complete the steps below, every Sandcastle run hard-fails with a pointer back to this file.
|
|
707
|
-
|
|
708
|
-
## Goal
|
|
709
|
-
|
|
710
|
-
Wire up the issue tracker so the scaffolded prompts can **list**, **view**, and **close** tasks. There is no runtime abstraction to implement — the tracker commands are baked into the scaffolded files, so you edit those files **in place**.
|
|
711
|
-
|
|
712
|
-
## 1. Interview the user
|
|
713
|
-
|
|
714
|
-
Ask the user:
|
|
715
|
-
|
|
716
|
-
- Which issue tracker do they use (e.g. Jira, Linear, a GitHub repo other than this one, an internal API)?
|
|
717
|
-
- How should the sandbox authenticate — a CLI that is already logged in, or an API token? If a token, what is the environment variable name?
|
|
718
|
-
|
|
719
|
-
## 2. Produce three commands
|
|
720
|
-
|
|
721
|
-
Work out, together with the user, the shell commands for:
|
|
722
|
-
|
|
723
|
-
- **list** — print all open tasks **as JSON** (match the shape the built-in trackers emit: an array of objects, each with at least an id/number, title, and body). This is what the agent reads at the start of every iteration.
|
|
724
|
-
- **view** \`<ID>\` — show a single task by id.
|
|
725
|
-
- **close** \`<ID>\` — close a single task by id.
|
|
726
|
-
|
|
727
|
-
## 3. Edit the scaffolded files in place
|
|
728
|
-
|
|
729
|
-
- **Dockerfile / Containerfile** — replace the line
|
|
730
|
-
|
|
731
|
-
\`\`\`
|
|
732
|
-
${CUSTOM_TRACKER_TOOLS}
|
|
733
|
-
\`\`\`
|
|
734
|
-
|
|
735
|
-
with the install steps for your tracker's CLI (if it needs one).
|
|
736
|
-
|
|
737
|
-
- **Prompt files (\`.sandcastle/*.md\`)** — replace the sentinel
|
|
738
|
-
|
|
739
|
-
\`\`\`
|
|
740
|
-
${CUSTOM_LIST_TASKS_SENTINEL}
|
|
741
|
-
\`\`\`
|
|
742
|
-
|
|
743
|
-
with your **list** command. In the prompt file the sentinel sits inside a Sandcastle **shell expression** — a leading \`!\` followed by the command in backticks — whose output is injected into the prompt before each run. Keep that \`!\` and the surrounding backticks; replace only the command between them, and **remove the \`exit 1\`** (leaving it keeps every run hard-failing). Then replace the \`${CUSTOM_VIEW_TASK_MARKER}\` and \`${CUSTOM_CLOSE_TASK_MARKER}\` markers with your **view** and **close** commands.
|
|
744
|
-
|
|
745
|
-
- **\`.env.example\`** — replace the \`# TODO\` block with the real env var(s) your tracker needs, then tell the user to set them in \`.sandcastle/.env\`.
|
|
746
|
-
|
|
747
|
-
## 4. Build the image
|
|
748
|
-
|
|
749
|
-
Once the files are wired up, build the sandbox image:
|
|
750
|
-
|
|
751
|
-
\`\`\`
|
|
752
|
-
sandcastle ${cliNamespace} build-image
|
|
753
|
-
\`\`\`
|
|
754
|
-
|
|
755
|
-
## 5. Verify
|
|
756
|
-
|
|
757
|
-
Run your **list** command inside the built image and confirm it returns the open tasks as JSON. If it errors, fix the command or the auth and rebuild.
|
|
758
|
-
`;
|
|
759
|
-
/**
|
|
760
|
-
* Detect whether the project's package.json has `"type": "module"`.
|
|
761
|
-
* If so, we can use plain `.ts`; otherwise we use `.mts` to ensure ESM.
|
|
762
|
-
*/
|
|
763
|
-
const detectMainFilename = (repoDir) => Effect.gen(function* () {
|
|
764
|
-
const fs = yield* FileSystem.FileSystem;
|
|
765
|
-
const pkgPath = join(repoDir, "package.json");
|
|
766
|
-
const exists = yield* fs
|
|
767
|
-
.exists(pkgPath)
|
|
768
|
-
.pipe(Effect.orElseSucceed(() => false));
|
|
769
|
-
if (!exists)
|
|
770
|
-
return "main.mts";
|
|
771
|
-
const content = yield* fs
|
|
772
|
-
.readFileString(pkgPath)
|
|
773
|
-
.pipe(Effect.orElseSucceed(() => ""));
|
|
774
|
-
try {
|
|
775
|
-
const pkg = JSON.parse(content);
|
|
776
|
-
return pkg["type"] === "module" ? "main.ts" : "main.mts";
|
|
777
|
-
}
|
|
778
|
-
catch {
|
|
779
|
-
return "main.mts";
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
export const scaffold = (repoDir, options) => Effect.gen(function* () {
|
|
783
|
-
const { agent, model, templateName = "blank", createLabel = true, issueTracker = ISSUE_TRACKER_REGISTRY[0], // default: github-issues
|
|
784
|
-
sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0], // default: docker
|
|
785
|
-
} = options;
|
|
786
|
-
const fs = yield* FileSystem.FileSystem;
|
|
787
|
-
const configDir = join(repoDir, ".sandcastle");
|
|
788
|
-
const exists = yield* fs
|
|
789
|
-
.exists(configDir)
|
|
790
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
791
|
-
if (exists) {
|
|
792
|
-
yield* Effect.fail(new Error(".sandcastle/ directory already exists. Remove it first if you want to re-initialize."));
|
|
793
|
-
}
|
|
794
|
-
const mainFilename = yield* detectMainFilename(repoDir);
|
|
795
|
-
yield* fs
|
|
796
|
-
.makeDirectory(configDir, { recursive: false })
|
|
797
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
798
|
-
const templateDir = yield* getTemplateDir(templateName);
|
|
799
|
-
// Build .env.example from agent + issue tracker env blocks
|
|
800
|
-
const envExampleParts = [agent.envExample];
|
|
801
|
-
if (issueTracker.envExample) {
|
|
802
|
-
envExampleParts.push(issueTracker.envExample);
|
|
803
|
-
}
|
|
804
|
-
const envExampleContent = envExampleParts.join("\n") + "\n";
|
|
805
|
-
yield* Effect.all([
|
|
806
|
-
fs
|
|
807
|
-
.writeFileString(join(configDir, sandboxProvider.containerfileName), agent.dockerfileTemplate)
|
|
808
|
-
.pipe(Effect.mapError((e) => new Error(e.message))),
|
|
809
|
-
fs
|
|
810
|
-
.writeFileString(join(configDir, ".gitignore"), GITIGNORE)
|
|
811
|
-
.pipe(Effect.mapError((e) => new Error(e.message))),
|
|
812
|
-
fs
|
|
813
|
-
.writeFileString(join(configDir, ".env.example"), envExampleContent)
|
|
814
|
-
.pipe(Effect.mapError((e) => new Error(e.message))),
|
|
815
|
-
copyTemplateFiles(templateDir, configDir, mainFilename),
|
|
816
|
-
], { concurrency: "unbounded" });
|
|
817
|
-
// Rewrite main file with the selected agent factory, model, and sandbox provider
|
|
818
|
-
yield* rewriteMainTs(configDir, agent, model, sandboxProvider, mainFilename);
|
|
819
|
-
// Replace issue tracker template arguments in all text files (must run before label stripping)
|
|
820
|
-
yield* substituteTemplateArgs(configDir, issueTracker);
|
|
821
|
-
// Strip --label Sandcastle from prompt files when the user declined label creation
|
|
822
|
-
if (!createLabel) {
|
|
823
|
-
yield* rewritePromptFiles(configDir);
|
|
824
|
-
}
|
|
825
|
-
// For the custom issue tracker, drop the setup prompt the user feeds to
|
|
826
|
-
// their coding agent. Written after substituteTemplateArgs so it isn't
|
|
827
|
-
// clobbered and references the resolved sentinel markers the agent finds
|
|
828
|
-
// (not the {{KEY}} names, which are gone by now).
|
|
829
|
-
if (issueTracker.name === "custom") {
|
|
830
|
-
yield* fs
|
|
831
|
-
.writeFileString(join(configDir, SETUP_ISSUE_TRACKER_DOC), buildSetupIssueTrackerDoc(sandboxProvider.cliNamespace))
|
|
832
|
-
.pipe(Effect.mapError((e) => new Error(e.message)));
|
|
833
|
-
}
|
|
834
|
-
return { mainFilename };
|
|
835
|
-
});
|
|
836
|
-
//# sourceMappingURL=InitService.js.map
|