@ai-hero/sandcastle 0.0.1

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.
Files changed (133) hide show
  1. package/README.md +284 -0
  2. package/dist/AgentProvider.d.ts +8 -0
  3. package/dist/AgentProvider.d.ts.map +1 -0
  4. package/dist/AgentProvider.js +60 -0
  5. package/dist/AgentProvider.js.map +1 -0
  6. package/dist/Config.d.ts +35 -0
  7. package/dist/Config.d.ts.map +1 -0
  8. package/dist/Config.js +56 -0
  9. package/dist/Config.js.map +1 -0
  10. package/dist/CopyToSandbox.d.ts +8 -0
  11. package/dist/CopyToSandbox.d.ts.map +1 -0
  12. package/dist/CopyToSandbox.js +32 -0
  13. package/dist/CopyToSandbox.js.map +1 -0
  14. package/dist/Display.d.ts +52 -0
  15. package/dist/Display.d.ts.map +1 -0
  16. package/dist/Display.js +132 -0
  17. package/dist/Display.js.map +1 -0
  18. package/dist/DockerLifecycle.d.ts +37 -0
  19. package/dist/DockerLifecycle.d.ts.map +1 -0
  20. package/dist/DockerLifecycle.js +109 -0
  21. package/dist/DockerLifecycle.js.map +1 -0
  22. package/dist/DockerSandbox.d.ts +6 -0
  23. package/dist/DockerSandbox.d.ts.map +1 -0
  24. package/dist/DockerSandbox.js +122 -0
  25. package/dist/DockerSandbox.js.map +1 -0
  26. package/dist/EnvResolver.d.ts +11 -0
  27. package/dist/EnvResolver.d.ts.map +1 -0
  28. package/dist/EnvResolver.js +43 -0
  29. package/dist/EnvResolver.js.map +1 -0
  30. package/dist/ErrorHandler.d.ts +15 -0
  31. package/dist/ErrorHandler.d.ts.map +1 -0
  32. package/dist/ErrorHandler.js +58 -0
  33. package/dist/ErrorHandler.js.map +1 -0
  34. package/dist/FilesystemSandbox.d.ts +6 -0
  35. package/dist/FilesystemSandbox.d.ts.map +1 -0
  36. package/dist/FilesystemSandbox.js +83 -0
  37. package/dist/FilesystemSandbox.js.map +1 -0
  38. package/dist/InitService.d.ts +11 -0
  39. package/dist/InitService.d.ts.map +1 -0
  40. package/dist/InitService.js +111 -0
  41. package/dist/InitService.js.map +1 -0
  42. package/dist/Orchestrator.d.ts +49 -0
  43. package/dist/Orchestrator.d.ts.map +1 -0
  44. package/dist/Orchestrator.js +155 -0
  45. package/dist/Orchestrator.js.map +1 -0
  46. package/dist/PromptArgumentSubstitution.d.ts +6 -0
  47. package/dist/PromptArgumentSubstitution.d.ts.map +1 -0
  48. package/dist/PromptArgumentSubstitution.js +33 -0
  49. package/dist/PromptArgumentSubstitution.js.map +1 -0
  50. package/dist/PromptPreprocessor.d.ts +7 -0
  51. package/dist/PromptPreprocessor.d.ts.map +1 -0
  52. package/dist/PromptPreprocessor.js +34 -0
  53. package/dist/PromptPreprocessor.js.map +1 -0
  54. package/dist/PromptResolver.d.ts +9 -0
  55. package/dist/PromptResolver.d.ts.map +1 -0
  56. package/dist/PromptResolver.js +26 -0
  57. package/dist/PromptResolver.js.map +1 -0
  58. package/dist/RecoveryMessage.d.ts +15 -0
  59. package/dist/RecoveryMessage.d.ts.map +1 -0
  60. package/dist/RecoveryMessage.js +81 -0
  61. package/dist/RecoveryMessage.js.map +1 -0
  62. package/dist/Sandbox.d.ts +23 -0
  63. package/dist/Sandbox.d.ts.map +1 -0
  64. package/dist/Sandbox.js +5 -0
  65. package/dist/Sandbox.js.map +1 -0
  66. package/dist/SandboxFactory.d.ts +56 -0
  67. package/dist/SandboxFactory.d.ts.map +1 -0
  68. package/dist/SandboxFactory.js +219 -0
  69. package/dist/SandboxFactory.js.map +1 -0
  70. package/dist/SandboxLifecycle.d.ts +32 -0
  71. package/dist/SandboxLifecycle.d.ts.map +1 -0
  72. package/dist/SandboxLifecycle.js +152 -0
  73. package/dist/SandboxLifecycle.js.map +1 -0
  74. package/dist/SyncService.d.ts +20 -0
  75. package/dist/SyncService.d.ts.map +1 -0
  76. package/dist/SyncService.js +504 -0
  77. package/dist/SyncService.js.map +1 -0
  78. package/dist/TokenResolver.d.ts +6 -0
  79. package/dist/TokenResolver.d.ts.map +1 -0
  80. package/dist/TokenResolver.js +43 -0
  81. package/dist/TokenResolver.js.map +1 -0
  82. package/dist/WorktreeManager.d.ts +42 -0
  83. package/dist/WorktreeManager.d.ts.map +1 -0
  84. package/dist/WorktreeManager.js +170 -0
  85. package/dist/WorktreeManager.js.map +1 -0
  86. package/dist/cli.d.ts +22 -0
  87. package/dist/cli.d.ts.map +1 -0
  88. package/dist/cli.js +217 -0
  89. package/dist/cli.js.map +1 -0
  90. package/dist/errors.d.ts +95 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/errors.js +35 -0
  93. package/dist/errors.js.map +1 -0
  94. package/dist/index.d.ts +4 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/main.d.ts +3 -0
  99. package/dist/main.d.ts.map +1 -0
  100. package/dist/main.js +16 -0
  101. package/dist/main.js.map +1 -0
  102. package/dist/run.d.ts +91 -0
  103. package/dist/run.d.ts.map +1 -0
  104. package/dist/run.js +155 -0
  105. package/dist/run.js.map +1 -0
  106. package/dist/templates/blank/main.ts +9 -0
  107. package/dist/templates/blank/prompt.md +12 -0
  108. package/dist/templates/blank/template.json +4 -0
  109. package/dist/templates/parallel-planner/implement-prompt.md +62 -0
  110. package/dist/templates/parallel-planner/main.ts +200 -0
  111. package/dist/templates/parallel-planner/merge-prompt.md +22 -0
  112. package/dist/templates/parallel-planner/plan-prompt.md +33 -0
  113. package/dist/templates/parallel-planner/template.json +4 -0
  114. package/dist/templates/sequential-reviewer/implement-prompt.md +62 -0
  115. package/dist/templates/sequential-reviewer/main.ts +102 -0
  116. package/dist/templates/sequential-reviewer/review-prompt.md +43 -0
  117. package/dist/templates/sequential-reviewer/template.json +4 -0
  118. package/dist/templates/simple-loop/main.ts +37 -0
  119. package/dist/templates/simple-loop/prompt.md +51 -0
  120. package/dist/templates/simple-loop/template.json +4 -0
  121. package/dist/templates.d.ts +2 -0
  122. package/dist/templates.d.ts.map +1 -0
  123. package/dist/templates.js +26 -0
  124. package/dist/templates.js.map +1 -0
  125. package/dist/terminalCleanup.d.ts +30 -0
  126. package/dist/terminalCleanup.d.ts.map +1 -0
  127. package/dist/terminalCleanup.js +37 -0
  128. package/dist/terminalCleanup.js.map +1 -0
  129. package/dist/testSandbox.d.ts +8 -0
  130. package/dist/testSandbox.d.ts.map +1 -0
  131. package/dist/testSandbox.js +101 -0
  132. package/dist/testSandbox.js.map +1 -0
  133. package/package.json +62 -0
@@ -0,0 +1,62 @@
1
+ # TASK
2
+
3
+ Fix issue #{{ISSUE_NUMBER}}: {{ISSUE_TITLE}}
4
+
5
+ Pull in the issue using `gh issue view`. If it has a parent PRD, pull that in too.
6
+
7
+ Only work on the issue specified.
8
+
9
+ Work on branch {{BRANCH}}. Make commits, run tests, and close the issue when done.
10
+
11
+ # CONTEXT
12
+
13
+ Here are the last 10 commits:
14
+
15
+ <recent-commits>
16
+
17
+ !`git log -n 10 --format="%H%n%ad%n%B---" --date=short`
18
+
19
+ </recent-commits>
20
+
21
+ # EXPLORATION
22
+
23
+ Explore the repo and fill your context window with relevant information that will allow you to complete the task.
24
+
25
+ Pay extra attention to test files that touch the relevant parts of the code.
26
+
27
+ # EXECUTION
28
+
29
+ If applicable, use RGR to complete the task.
30
+
31
+ 1. RED: write one test
32
+ 2. GREEN: write the implementation to pass that test
33
+ 3. REPEAT until done
34
+ 4. REFACTOR the code
35
+
36
+ # FEEDBACK LOOPS
37
+
38
+ Before committing, run `npm run typecheck` and `npm run test` to ensure the tests pass.
39
+
40
+ # COMMIT
41
+
42
+ Make a git commit. The commit message must:
43
+
44
+ 1. Start with `RALPH:` prefix
45
+ 2. Include task completed + PRD reference
46
+ 3. Key decisions made
47
+ 4. Files changed
48
+ 5. Blockers or notes for next iteration
49
+
50
+ Keep it concise.
51
+
52
+ # THE ISSUE
53
+
54
+ If the task is not complete, leave a comment on the GitHub issue with what was done.
55
+
56
+ Do not close the issue - this will be done later.
57
+
58
+ Once complete, output <promise>COMPLETE</promise>.
59
+
60
+ # FINAL RULES
61
+
62
+ ONLY WORK ON A SINGLE TASK.
@@ -0,0 +1,102 @@
1
+ // Sequential Reviewer — implement-then-review loop
2
+ //
3
+ // This template drives a two-phase workflow per issue:
4
+ // Phase 1 (Implement): A sonnet agent picks an open GitHub issue, works on it
5
+ // on a dedicated branch, commits the changes, and signals
6
+ // completion.
7
+ // Phase 2 (Review): A second sonnet agent reviews the branch diff and either
8
+ // approves it or makes corrections directly on the branch.
9
+ //
10
+ // The outer loop repeats up to MAX_ITERATIONS times, processing one issue per
11
+ // iteration. This is a middle-complexity option between the simple-loop (no review
12
+ // gate) and the parallel-planner (concurrent execution with a planning phase).
13
+ //
14
+ // Usage:
15
+ // npx tsx .sandcastle/main.ts
16
+ // Or add to package.json:
17
+ // "scripts": { "sandcastle": "npx tsx .sandcastle/main.ts" }
18
+
19
+ import * as sandcastle from "@ai-hero/sandcastle";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Configuration
23
+ // ---------------------------------------------------------------------------
24
+
25
+ // Maximum number of implement→review cycles to run before stopping.
26
+ // Each cycle works on one issue. Raise this to process more issues per run.
27
+ const MAX_ITERATIONS = 10;
28
+
29
+ // Hooks run inside the sandbox before the agent starts each iteration.
30
+ // npm install ensures the sandbox always has fresh dependencies.
31
+ const hooks = {
32
+ onSandboxReady: [{ command: "npm install" }],
33
+ };
34
+
35
+ // Copy node_modules from the host into the worktree before each sandbox
36
+ // starts. Avoids a full npm install from scratch; the hook above handles
37
+ // platform-specific binaries and any packages added since the last copy.
38
+ const copyToSandbox = ["node_modules"];
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Main loop
42
+ // ---------------------------------------------------------------------------
43
+
44
+ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
45
+ console.log(`\n=== Iteration ${iteration}/${MAX_ITERATIONS} ===\n`);
46
+
47
+ // -------------------------------------------------------------------------
48
+ // Phase 1: Implement
49
+ //
50
+ // A sonnet agent picks the next open GitHub issue, creates a branch, writes
51
+ // the implementation (using RGR: Red → Green → Repeat → Refactor), and
52
+ // commits the result.
53
+ //
54
+ // The agent signals completion via <promise>COMPLETE</promise> when done.
55
+ // The result contains the branch name the agent worked on.
56
+ // -------------------------------------------------------------------------
57
+ const implement = await sandcastle.run({
58
+ hooks,
59
+ copyToSandbox,
60
+ name: "implementer",
61
+ maxIterations: 100,
62
+ model: "claude-sonnet-4-6",
63
+ promptFile: "./.sandcastle/implement-prompt.md",
64
+ });
65
+
66
+ // Extract the branch the agent worked on so the reviewer can target it.
67
+ const branch = implement.branch;
68
+
69
+ if (!implement.commits.length) {
70
+ console.log("Implementation agent made no commits. Skipping review.");
71
+ continue;
72
+ }
73
+
74
+ console.log(`\nImplementation complete on branch: ${branch}`);
75
+ console.log(`Commits: ${implement.commits.length}`);
76
+
77
+ // -------------------------------------------------------------------------
78
+ // Phase 2: Review
79
+ //
80
+ // A second sonnet agent reviews the diff of the branch produced by Phase 1.
81
+ // It uses the {{BRANCH}} prompt argument to inspect the right branch, and
82
+ // either approves or makes corrections directly on the branch.
83
+ // -------------------------------------------------------------------------
84
+ await sandcastle.run({
85
+ hooks,
86
+ copyToSandbox,
87
+ name: "reviewer",
88
+ maxIterations: 10,
89
+ model: "claude-sonnet-4-6",
90
+ promptFile: "./.sandcastle/review-prompt.md",
91
+ // Prompt arguments substitute {{BRANCH}} in review-prompt.md before the
92
+ // agent sees the prompt.
93
+ promptArgs: {
94
+ BRANCH: branch,
95
+ },
96
+ branch,
97
+ });
98
+
99
+ console.log("\nReview complete.");
100
+ }
101
+
102
+ console.log("\nAll done.");
@@ -0,0 +1,43 @@
1
+ # TASK
2
+
3
+ Review the code changes on branch `{{BRANCH}}` and provide actionable feedback.
4
+
5
+ # CONTEXT
6
+
7
+ ## Branch diff
8
+
9
+ !`git diff main...{{BRANCH}}`
10
+
11
+ ## Commits on this branch
12
+
13
+ !`git log main..{{BRANCH}} --oneline`
14
+
15
+ # REVIEW CHECKLIST
16
+
17
+ Examine the diff above and assess:
18
+
19
+ 1. **Correctness** — Does the implementation match the intent of the issue? Are edge cases handled?
20
+ 2. **Tests** — Are the new/changed behaviours covered by tests? Do the tests actually exercise the right code paths?
21
+ 3. **Code quality** — Is the code clear, minimal, and consistent with the surrounding codebase? No dead code or unnecessary complexity?
22
+ 4. **Type safety** — Are there any unsafe casts, `any` types, or unchecked assumptions?
23
+ 5. **Security** — Does the change introduce injection vulnerabilities, credential leaks, or other security issues?
24
+
25
+ # OUTPUT
26
+
27
+ If the branch is acceptable:
28
+
29
+ ```
30
+ REVIEW RESULT: APPROVED
31
+
32
+ <summary of what the implementation does and why it looks good>
33
+ ```
34
+
35
+ If changes are needed, make them directly on the branch, then commit and output:
36
+
37
+ ```
38
+ REVIEW RESULT: CHANGES MADE
39
+
40
+ <summary of what was changed and why>
41
+ ```
42
+
43
+ Once your review is complete, output <promise>COMPLETE</promise>.
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "sequential-reviewer",
3
+ "description": "Implements issues one by one, with a code review step after each"
4
+ }
@@ -0,0 +1,37 @@
1
+ import { run } from "@ai-hero/sandcastle";
2
+
3
+ // Simple loop: an agent that picks open GitHub issues one by one and closes them.
4
+ // Run this with: npx tsx .sandcastle/main.ts
5
+ // Or add to package.json scripts: "sandcastle": "npx tsx .sandcastle/main.ts"
6
+
7
+ await run({
8
+ // A name for this run, shown as a prefix in log output.
9
+ name: "worker",
10
+
11
+ // Path to the prompt file. Shell expressions inside are evaluated inside the
12
+ // sandbox at the start of each iteration, so the agent always sees fresh data.
13
+ promptFile: "./.sandcastle/prompt.md",
14
+
15
+ // Maximum number of iterations (agent invocations) to run in a session.
16
+ // Each iteration works on a single issue. Increase this to process more issues
17
+ // per run, or set it to 1 for a single-shot mode.
18
+ maxIterations: 3,
19
+
20
+ // The Claude model to use. Sonnet balances capability and speed for most tasks.
21
+ // Switch to claude-opus-4-6 for harder problems, or claude-haiku-4-5 for speed.
22
+ model: "claude-sonnet-4-6",
23
+
24
+ // Copy node_modules from the host into the worktree before the sandbox
25
+ // starts. This avoids a full npm install from scratch on every iteration.
26
+ // The onSandboxReady hook still runs npm install as a safety net to handle
27
+ // platform-specific binaries and any packages added since the last copy.
28
+ copyToSandbox: ["node_modules"],
29
+
30
+ // Lifecycle hooks — commands that run inside the sandbox at specific points.
31
+ hooks: {
32
+ // onSandboxReady runs once after the sandbox is initialised and the repo is
33
+ // synced in, before the agent starts. Use it to install dependencies or run
34
+ // any other setup steps your project needs.
35
+ onSandboxReady: [{ command: "npm install" }],
36
+ },
37
+ });
@@ -0,0 +1,51 @@
1
+ # Context
2
+
3
+ ## Open issues
4
+
5
+ !`gh issue list --json number,title,body --limit 20`
6
+
7
+ ## Recent RALPH commits (last 10)
8
+
9
+ !`git log --oneline --grep="RALPH" -10`
10
+
11
+ # Task
12
+
13
+ You are RALPH — an autonomous coding agent working through GitHub issues one at a time.
14
+
15
+ ## Priority order
16
+
17
+ Work on issues in this order:
18
+
19
+ 1. **Bug fixes** — broken behaviour affecting users
20
+ 2. **Tracer bullets** — thin end-to-end slices that prove an approach works
21
+ 3. **Polish** — improving existing functionality (error messages, UX, docs)
22
+ 4. **Refactors** — internal cleanups with no user-visible change
23
+
24
+ Pick the highest-priority open issue that is not blocked by another open issue.
25
+
26
+ ## Workflow
27
+
28
+ 1. **Explore** — read the issue carefully. Pull in the parent PRD if referenced. Read the relevant source files and tests before writing any code.
29
+ 2. **Plan** — decide what to change and why. Keep the change as small as possible.
30
+ 3. **Execute** — use RGR (Red → Green → Repeat → Refactor): write a failing test first, then write the implementation to pass it.
31
+ 4. **Verify** — run `npm run typecheck` and `npm run test` before committing. Fix any failures before proceeding.
32
+ 5. **Commit** — make a single git commit. The message MUST:
33
+ - Start with `RALPH:` prefix
34
+ - Include the task completed and any PRD reference
35
+ - List key decisions made
36
+ - List files changed
37
+ - Note any blockers for the next iteration
38
+ 6. **Close** — close the issue with `gh issue close <number> --comment "..."` explaining what was done.
39
+
40
+ ## Rules
41
+
42
+ - Work on **one issue per iteration**. Do not attempt multiple issues in a single iteration.
43
+ - Do not close an issue until you have committed the fix and verified tests pass.
44
+ - Do not leave commented-out code or TODO comments in committed code.
45
+ - If you are blocked (missing context, failing tests you cannot fix, external dependency), leave a comment on the issue and move on — do not close it.
46
+
47
+ # Done
48
+
49
+ When all actionable issues are complete (or you are blocked on all remaining ones), output the completion signal:
50
+
51
+ <promise>COMPLETE</promise>
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "simple-loop",
3
+ "description": "Picks GitHub issues one by one and closes them"
4
+ }
@@ -0,0 +1,2 @@
1
+ export declare const SKELETON_PROMPT: string;
2
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,QAyB3B,CAAC"}
@@ -0,0 +1,26 @@
1
+ export const SKELETON_PROMPT = `# Context
2
+
3
+ <!-- Use !` +
4
+ "`" +
5
+ `command` +
6
+ "`" +
7
+ ` to pull in dynamic context. Commands run inside the sandbox. -->
8
+ <!-- Example: !` +
9
+ "`" +
10
+ `git log --oneline -10` +
11
+ "`" +
12
+ ` or !` +
13
+ "`" +
14
+ `gh issue list --json number,title` +
15
+ "`" +
16
+ ` -->
17
+
18
+ # Task
19
+
20
+ <!-- Describe what the agent should do. -->
21
+
22
+ # Done
23
+
24
+ <!-- When the task is complete, output <promise>COMPLETE</promise> to signal early termination. -->
25
+ `;
26
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAC1B;;WAES;IACT,GAAG;IACH,SAAS;IACT,GAAG;IACH;gBACc;IACd,GAAG;IACH,uBAAuB;IACvB,GAAG;IACH,OAAO;IACP,GAAG;IACH,mCAAmC;IACnC,GAAG;IACH;;;;;;;;;CASD,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Terminal cleanup for abrupt exits.
3
+ *
4
+ * @clack/prompts' spinner and taskLog call stdin.setRawMode(true) and hide
5
+ * the cursor via escape sequences. When the process is killed by a signal
6
+ * handler that calls process.exit() directly (e.g. during Ctrl-C cleanup in
7
+ * SandboxFactory), clack's own cleanup is bypassed and the terminal is left
8
+ * in raw mode with the cursor hidden.
9
+ *
10
+ * Registering a process 'exit' listener that restores these guarantees the
11
+ * terminal is always left in a usable state.
12
+ */
13
+ /** Escape sequence to show the cursor (DECTCEM). */
14
+ export declare const SHOW_CURSOR = "\u001B[?25h";
15
+ /**
16
+ * Creates a synchronous exit handler that restores terminal state.
17
+ * Extracted as a pure function so it can be unit-tested without side effects.
18
+ */
19
+ export declare const makeTerminalCleanupHandler: (stdin: {
20
+ isTTY?: boolean | undefined;
21
+ setRawMode?: ((raw: boolean) => void) | undefined;
22
+ }, stdout: {
23
+ write: (data: string) => boolean;
24
+ }) => () => void;
25
+ /**
26
+ * Registers the terminal cleanup handler on process 'exit'.
27
+ * Call once at program startup (main.ts).
28
+ */
29
+ export declare const setupTerminalCleanup: () => void;
30
+ //# sourceMappingURL=terminalCleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminalCleanup.d.ts","sourceRoot":"","sources":["../src/terminalCleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,oDAAoD;AACpD,eAAO,MAAM,WAAW,gBAAc,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;gBAcpC,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,oBAAoB,YAEhC,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Terminal cleanup for abrupt exits.
3
+ *
4
+ * @clack/prompts' spinner and taskLog call stdin.setRawMode(true) and hide
5
+ * the cursor via escape sequences. When the process is killed by a signal
6
+ * handler that calls process.exit() directly (e.g. during Ctrl-C cleanup in
7
+ * SandboxFactory), clack's own cleanup is bypassed and the terminal is left
8
+ * in raw mode with the cursor hidden.
9
+ *
10
+ * Registering a process 'exit' listener that restores these guarantees the
11
+ * terminal is always left in a usable state.
12
+ */
13
+ /** Escape sequence to show the cursor (DECTCEM). */
14
+ export const SHOW_CURSOR = "\x1b[?25h";
15
+ /**
16
+ * Creates a synchronous exit handler that restores terminal state.
17
+ * Extracted as a pure function so it can be unit-tested without side effects.
18
+ */
19
+ export const makeTerminalCleanupHandler = (stdin, stdout) => () => {
20
+ if (stdin.isTTY && stdin.setRawMode) {
21
+ try {
22
+ stdin.setRawMode(false);
23
+ }
24
+ catch {
25
+ // Best-effort — may fail if stdin is already closed
26
+ }
27
+ }
28
+ stdout.write(SHOW_CURSOR);
29
+ };
30
+ /**
31
+ * Registers the terminal cleanup handler on process 'exit'.
32
+ * Call once at program startup (main.ts).
33
+ */
34
+ export const setupTerminalCleanup = () => {
35
+ process.on("exit", makeTerminalCleanupHandler(process.stdin, process.stdout));
36
+ };
37
+ //# sourceMappingURL=terminalCleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminalCleanup.js","sourceRoot":"","sources":["../src/terminalCleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,oDAAoD;AACpD,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AAEvC;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GACrC,CACE,KAA+D,EAC/D,MAA4C,EAC5C,EAAE,CACJ,GAAS,EAAE;IACT,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;QACtD,CAAC;IACH,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AAC5B,CAAC,CAAC;AAEJ;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAS,EAAE;IAC7C,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,0BAA0B,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAChF,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Test helper: creates a local (filesystem-based) Sandbox layer for unit tests.
3
+ * This replaces FilesystemSandbox which has been removed.
4
+ */
5
+ import { Layer } from "effect";
6
+ import { Sandbox } from "./SandboxFactory.js";
7
+ export declare const makeLocalSandboxLayer: (sandboxDir: string) => Layer.Layer<Sandbox, never, never>;
8
+ //# sourceMappingURL=testSandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testSandbox.d.ts","sourceRoot":"","sources":["../src/testSandbox.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAQvC,OAAO,EAAmB,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAa/D,eAAO,MAAM,qBAAqB,4DA8GjC,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Test helper: creates a local (filesystem-based) Sandbox layer for unit tests.
3
+ * This replaces FilesystemSandbox which has been removed.
4
+ */
5
+ import { Effect, Layer } from "effect";
6
+ import { execFile, spawn } from "node:child_process";
7
+ import { copyFile, mkdir } from "node:fs/promises";
8
+ import { mkdtempSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { createInterface } from "node:readline";
12
+ import { CopyError, ExecError } from "./errors.js";
13
+ import { Sandbox } from "./SandboxFactory.js";
14
+ /**
15
+ * Creates an isolated git global config env so that test sandbox
16
+ * `git config --global` writes don't corrupt the developer's real ~/.gitconfig.
17
+ */
18
+ const createIsolatedGitEnv = () => {
19
+ const tmpDir = mkdtempSync(join(tmpdir(), "test-gitconfig-"));
20
+ const globalConfigPath = join(tmpDir, ".gitconfig");
21
+ writeFileSync(globalConfigPath, "");
22
+ return { GIT_CONFIG_GLOBAL: globalConfigPath };
23
+ };
24
+ export const makeLocalSandboxLayer = (sandboxDir) => {
25
+ const gitEnv = createIsolatedGitEnv();
26
+ const env = { ...process.env, ...gitEnv };
27
+ return Layer.succeed(Sandbox, {
28
+ exec: (command, options) => Effect.async((resume) => {
29
+ execFile("sh", ["-c", command], {
30
+ cwd: options?.cwd ?? sandboxDir,
31
+ maxBuffer: 10 * 1024 * 1024,
32
+ env,
33
+ }, (error, stdout, stderr) => {
34
+ if (error && error.code === undefined) {
35
+ resume(Effect.fail(new ExecError({
36
+ command,
37
+ message: `Failed to exec: ${error.message}`,
38
+ })));
39
+ }
40
+ else {
41
+ resume(Effect.succeed({
42
+ stdout: stdout.toString(),
43
+ stderr: stderr.toString(),
44
+ exitCode: typeof error?.code === "number"
45
+ ? error.code
46
+ : 0,
47
+ }));
48
+ }
49
+ });
50
+ }),
51
+ execStreaming: (command, onStdoutLine, options) => Effect.async((resume) => {
52
+ const proc = spawn("sh", ["-c", command], {
53
+ cwd: options?.cwd ?? sandboxDir,
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ env,
56
+ });
57
+ const stdoutChunks = [];
58
+ const stderrChunks = [];
59
+ const rl = createInterface({ input: proc.stdout });
60
+ rl.on("line", (line) => {
61
+ stdoutChunks.push(line);
62
+ onStdoutLine(line);
63
+ });
64
+ proc.stderr.on("data", (chunk) => {
65
+ stderrChunks.push(chunk.toString());
66
+ });
67
+ proc.on("error", (error) => {
68
+ resume(Effect.fail(new ExecError({
69
+ command,
70
+ message: `Failed to exec: ${error.message}`,
71
+ })));
72
+ });
73
+ proc.on("close", (code) => {
74
+ resume(Effect.succeed({
75
+ stdout: stdoutChunks.join("\n"),
76
+ stderr: stderrChunks.join(""),
77
+ exitCode: code ?? 0,
78
+ }));
79
+ });
80
+ }),
81
+ copyIn: (hostPath, sandboxPath) => Effect.tryPromise({
82
+ try: async () => {
83
+ await mkdir(dirname(sandboxPath), { recursive: true });
84
+ await copyFile(hostPath, sandboxPath);
85
+ },
86
+ catch: (e) => new CopyError({
87
+ message: `Failed to copy ${hostPath} -> ${sandboxPath}: ${e}`,
88
+ }),
89
+ }),
90
+ copyOut: (sandboxPath, hostPath) => Effect.tryPromise({
91
+ try: async () => {
92
+ await mkdir(dirname(hostPath), { recursive: true });
93
+ await copyFile(sandboxPath, hostPath);
94
+ },
95
+ catch: (e) => new CopyError({
96
+ message: `Failed to copy ${sandboxPath} -> ${hostPath}: ${e}`,
97
+ }),
98
+ }),
99
+ });
100
+ };
101
+ //# sourceMappingURL=testSandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testSandbox.js","sourceRoot":"","sources":["../src/testSandbox.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAmB,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE/D;;;GAGG;AACH,MAAM,oBAAoB,GAAG,GAA2B,EAAE;IACxD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC9D,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpD,aAAa,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IACpC,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;AACjD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,UAAkB,EACI,EAAE;IACxB,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC;IACtC,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAE1C,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE;QAC5B,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CACzB,MAAM,CAAC,KAAK,CAAwB,CAAC,MAAM,EAAE,EAAE;YAC7C,QAAQ,CACN,IAAI,EACJ,CAAC,IAAI,EAAE,OAAO,CAAC,EACf;gBACE,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,UAAU;gBAC/B,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;gBAC3B,GAAG;aACJ,EACD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;gBACxB,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBACtC,MAAM,CACJ,MAAM,CAAC,IAAI,CACT,IAAI,SAAS,CAAC;wBACZ,OAAO;wBACP,OAAO,EAAE,mBAAmB,KAAK,CAAC,OAAO,EAAE;qBAC5C,CAAC,CACH,CACF,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,MAAM,CACJ,MAAM,CAAC,OAAO,CAAC;wBACb,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE;wBACzB,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE;wBACzB,QAAQ,EACN,OAAO,KAAK,EAAE,IAAI,KAAK,QAAQ;4BAC7B,CAAC,CAAC,KAAK,CAAC,IAAI;4BACZ,CAAC,CAAE,CAAY;qBACpB,CAAC,CACH,CAAC;gBACJ,CAAC;YACH,CAAC,CACF,CAAC;QACJ,CAAC,CAAC;QAEJ,aAAa,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,CAChD,MAAM,CAAC,KAAK,CAAwB,CAAC,MAAM,EAAE,EAAE;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE;gBACxC,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,UAAU;gBAC/B,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;gBACjC,GAAG;aACJ,CAAC,CAAC;YAEH,MAAM,YAAY,GAAa,EAAE,CAAC;YAClC,MAAM,YAAY,GAAa,EAAE,CAAC;YAElC,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAO,EAAE,CAAC,CAAC;YACpD,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBACrB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACxB,YAAY,CAAC,IAAI,CAAC,CAAC;YACrB,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,MAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACxC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;gBACzB,MAAM,CACJ,MAAM,CAAC,IAAI,CACT,IAAI,SAAS,CAAC;oBACZ,OAAO;oBACP,OAAO,EAAE,mBAAmB,KAAK,CAAC,OAAO,EAAE;iBAC5C,CAAC,CACH,CACF,CAAC;YACJ,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gBACxB,MAAM,CACJ,MAAM,CAAC,OAAO,CAAC;oBACb,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;oBAC/B,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC7B,QAAQ,EAAE,IAAI,IAAI,CAAC;iBACpB,CAAC,CACH,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEJ,MAAM,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,CAChC,MAAM,CAAC,UAAU,CAAC;YAChB,GAAG,EAAE,KAAK,IAAI,EAAE;gBACd,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvD,MAAM,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;YACxC,CAAC;YACD,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CACX,IAAI,SAAS,CAAC;gBACZ,OAAO,EAAE,kBAAkB,QAAQ,OAAO,WAAW,KAAK,CAAC,EAAE;aAC9D,CAAC;SACL,CAAC;QAEJ,OAAO,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,EAAE,CACjC,MAAM,CAAC,UAAU,CAAC;YAChB,GAAG,EAAE,KAAK,IAAI,EAAE;gBACd,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpD,MAAM,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;YACxC,CAAC;YACD,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CACX,IAAI,SAAS,CAAC;gBACZ,OAAO,EAAE,kBAAkB,WAAW,OAAO,QAAQ,KAAK,CAAC,EAAE;aAC9D,CAAC;SACL,CAAC;KACL,CAAC,CAAC;AACL,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@ai-hero/sandcastle",
3
+ "version": "0.0.1",
4
+ "description": "CLI for orchestrating AI agents in isolated sandbox environments",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "sandcastle": "dist/main.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsgo --project tsconfig.build.json",
19
+ "postbuild": "rm -rf dist/templates && cp -r src/templates dist/templates",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "typecheck": "tsgo --noEmit",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check .",
25
+ "prepare": "husky",
26
+ "release": "changeset publish",
27
+ "run-sandcastle": "tsx .sandcastle/run.ts"
28
+ },
29
+ "keywords": [
30
+ "cli",
31
+ "sandbox",
32
+ "docker",
33
+ "ai",
34
+ "agent"
35
+ ],
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@changesets/cli": "^2.30.0",
39
+ "@effect/vitest": "^0.28.0",
40
+ "@typescript/native-preview": "^7.0.0-dev.20260317.1",
41
+ "husky": "^9.1.7",
42
+ "lint-staged": "^15.5.1",
43
+ "prettier": "^3.5.3",
44
+ "tsx": "^4.21.0",
45
+ "vitest": "^3.2.0"
46
+ },
47
+ "dependencies": {
48
+ "@clack/prompts": "^1.1.0",
49
+ "@effect/cli": "^0.74.0",
50
+ "@effect/platform": "^0.95.0",
51
+ "@effect/platform-node": "^0.105.0",
52
+ "@effect/printer": "^0.48.0",
53
+ "@effect/printer-ansi": "^0.48.0",
54
+ "effect": "^3.20.0"
55
+ },
56
+ "lint-staged": {
57
+ "*.{ts,tsx,js,jsx,json,md}": "prettier --write"
58
+ },
59
+ "files": [
60
+ "dist"
61
+ ]
62
+ }