maesttro 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/README.md +155 -0
- package/bin/maesttro.js +49 -0
- package/docker/Dockerfile +25 -0
- package/docker/configs/claude/settings.json +3 -0
- package/docker/configs/codex/config.json +3 -0
- package/docker/configs/opencode/config.json +3 -0
- package/package.json +23 -0
- package/src/clients.js +26 -0
- package/src/doctor.js +78 -0
- package/src/launch.js +68 -0
- package/src/paths.js +12 -0
- package/src/setup.js +101 -0
- package/src/update.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# maesttro
|
|
2
|
+
|
|
3
|
+
Isolated Docker sandbox for local coding agents — `claude`, `opencode`, `codex`,
|
|
4
|
+
and others — running under a dedicated bot GitHub identity, never your personal
|
|
5
|
+
`~/.ssh` or `~/.config/gh`. Install once, then `maesttro` just works from any
|
|
6
|
+
project.
|
|
7
|
+
|
|
8
|
+
Companion piece to Maesttro's orchestration side: this is the local, single-
|
|
9
|
+
machine entry point — a curated, versioned way to run any supported client
|
|
10
|
+
safely, with sane defaults baked in.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
**Global install (recommended for regular use — instant, offline):**
|
|
17
|
+
```bash
|
|
18
|
+
npm i -g maesttro
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Zero-install, try it once (npx/bunx resolve the package by this exact name):**
|
|
22
|
+
```bash
|
|
23
|
+
npx maesttro
|
|
24
|
+
bunx maesttro
|
|
25
|
+
```
|
|
26
|
+
Note: `npx`/`bunx` check the registry for the latest version on every
|
|
27
|
+
invocation (small network hit + latency each time), and typically run
|
|
28
|
+
whatever's tagged `latest` unless you pin a version. Good for a first try or
|
|
29
|
+
demoing to someone else; `npm i -g` is the better daily-driver since all real
|
|
30
|
+
state lives in Docker anyway — the CLI itself is stateless either way.
|
|
31
|
+
|
|
32
|
+
**From GitHub Packages** — map the scope in `~/.npmrc` first, so `maesttro update`
|
|
33
|
+
resolves against the same registry:
|
|
34
|
+
```ini
|
|
35
|
+
@esatto:registry=https://npm.pkg.github.com
|
|
36
|
+
//npm.pkg.github.com/:_authToken=<GitHub token with read:packages>
|
|
37
|
+
```
|
|
38
|
+
```bash
|
|
39
|
+
npm i -g @esatto/maesttro
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**From source** (the package lives in the `stagehand/` directory of the Maesttro monorepo):
|
|
43
|
+
```bash
|
|
44
|
+
npm i -g ./stagehand
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
maesttro setup /path/to/bot-token.txt # one-time: build image, create volume, auth bot
|
|
53
|
+
maesttro list # see available clients
|
|
54
|
+
maesttro doctor # health check anytime
|
|
55
|
+
|
|
56
|
+
maesttro # launch the default client (claude)
|
|
57
|
+
maesttro opencode # launch opencode instead
|
|
58
|
+
maesttro codex # launch codex instead
|
|
59
|
+
maesttro bash # plain shell inside the sandbox, for debugging
|
|
60
|
+
|
|
61
|
+
maesttro update # bump the package + rebuild image with refreshed configs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Curated clients
|
|
67
|
+
|
|
68
|
+
Defined in `src/clients.js`, installed in `docker/Dockerfile`, configured via
|
|
69
|
+
`docker/configs/<client>/`:
|
|
70
|
+
|
|
71
|
+
| Key | Client | Package |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `claude` (default) | Claude Code | `@anthropic-ai/claude-code` |
|
|
74
|
+
| `opencode` | opencode | `opencode-ai` |
|
|
75
|
+
| `codex` | OpenAI Codex CLI | `@openai/codex` |
|
|
76
|
+
|
|
77
|
+
Adding a new client: add an entry to `src/clients.js`, an install line in
|
|
78
|
+
`docker/Dockerfile`, and a `docker/configs/<key>/` directory with any default
|
|
79
|
+
config — then `maesttro update` rebuilds the image with it included.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## How `maesttro update` keeps configs current
|
|
84
|
+
|
|
85
|
+
`docker/configs/<client>/` files are baked into the image at build time —
|
|
86
|
+
they become each client's out-of-the-box defaults inside the sandbox. When you
|
|
87
|
+
publish a new version of `maesttro` with updated curated configs:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
maesttro update
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
does two things, in order:
|
|
94
|
+
1. `npm i -g maesttro@latest` — refreshes the installed package files
|
|
95
|
+
(including the bundled `docker/` directory) in place
|
|
96
|
+
2. Rebuilds the sandbox image from that same, now-refreshed, `docker/`
|
|
97
|
+
directory
|
|
98
|
+
|
|
99
|
+
Because the Dockerfile path is resolved relative to the running script's own
|
|
100
|
+
install location, step 2 automatically picks up whatever step 1 just wrote —
|
|
101
|
+
no extra plumbing needed to keep the two in sync.
|
|
102
|
+
|
|
103
|
+
Your bot identity (in the `maesttro-home` volume) and your project files are
|
|
104
|
+
untouched by an update — only the image (client versions + curated configs)
|
|
105
|
+
gets refreshed.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## What's mounted, and why
|
|
110
|
+
|
|
111
|
+
| Mount | Source | Purpose |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `/home/agent` | `maesttro-home` named volume | Bot's persistent identity (`gh` auth, per-client session state). Survives across runs, lives only in Docker. |
|
|
114
|
+
| `/workspace` | current host directory | The project being worked on — repo-level config comes along automatically. |
|
|
115
|
+
| `/home/agent/.claude/skills` (read-only) | `~/agent-shared/skills` on host, if present | Reusable skills/house-style files shared across sessions. Read-only. |
|
|
116
|
+
|
|
117
|
+
Nothing from your personal `~/.ssh` or `~/.config/gh` is ever mounted.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Package layout
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
maesttro/
|
|
125
|
+
├── package.json
|
|
126
|
+
├── bin/maesttro.js # entry point, dispatches subcommands
|
|
127
|
+
├── src/
|
|
128
|
+
│ ├── clients.js # curated client registry
|
|
129
|
+
│ ├── setup.js # `maesttro setup`
|
|
130
|
+
│ ├── update.js # `maesttro update`
|
|
131
|
+
│ ├── doctor.js # `maesttro doctor`
|
|
132
|
+
│ ├── launch.js # `maesttro` / `maesttro <client>` / `maesttro list`
|
|
133
|
+
│ └── paths.js # locates bundled Dockerfile wherever npm installs the package
|
|
134
|
+
└── docker/
|
|
135
|
+
├── Dockerfile
|
|
136
|
+
└── configs/
|
|
137
|
+
├── claude/
|
|
138
|
+
├── opencode/
|
|
139
|
+
└── codex/
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Publishing
|
|
143
|
+
|
|
144
|
+
Released by CI, not by hand: bumping `version` in `package.json` and merging
|
|
145
|
+
to `main` publishes `maesttro` to npmjs and `@esatto/maesttro` to GitHub
|
|
146
|
+
Packages. `maesttro update` always pulls whatever is tagged `latest` on the
|
|
147
|
+
registry it was installed from.
|
|
148
|
+
|
|
149
|
+
## Open items
|
|
150
|
+
|
|
151
|
+
- Fill in real curated defaults in `docker/configs/*` (currently placeholders)
|
|
152
|
+
- Pin exact client CLI versions in the Dockerfile once stable
|
|
153
|
+
- Decide whether each client gets its own bot token/volume for finer audit
|
|
154
|
+
trail, or shares one (current setup: shared)
|
|
155
|
+
- Homebrew tap as an alternative install path for non-Node users — optional
|
package/bin/maesttro.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { setup } = require("../src/setup")
|
|
4
|
+
const { doctor } = require("../src/doctor")
|
|
5
|
+
const { launch, list } = require("../src/launch")
|
|
6
|
+
const { update } = require("../src/update")
|
|
7
|
+
|
|
8
|
+
const [, , cmd, ...rest] = process.argv
|
|
9
|
+
|
|
10
|
+
const HELP = `maesttro — isolated sandbox for local coding agents
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
maesttro Launch the default client (claude) in the sandbox
|
|
14
|
+
maesttro <client> Launch a specific client — see \`maesttro list\`
|
|
15
|
+
maesttro list Show available clients
|
|
16
|
+
maesttro setup [token] One-time: build image, create volume, authenticate bot identity
|
|
17
|
+
maesttro update Update the package and rebuild the image with refreshed client configs
|
|
18
|
+
maesttro doctor Health check — Docker, image, volume, bot auth, isolation, clients
|
|
19
|
+
maesttro help Show this message
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
switch (cmd) {
|
|
24
|
+
case undefined:
|
|
25
|
+
launch([])
|
|
26
|
+
break
|
|
27
|
+
case "setup":
|
|
28
|
+
await setup(rest)
|
|
29
|
+
break
|
|
30
|
+
case "update":
|
|
31
|
+
update()
|
|
32
|
+
break
|
|
33
|
+
case "doctor":
|
|
34
|
+
doctor()
|
|
35
|
+
break
|
|
36
|
+
case "list":
|
|
37
|
+
list()
|
|
38
|
+
break
|
|
39
|
+
case "help":
|
|
40
|
+
case "--help":
|
|
41
|
+
case "-h":
|
|
42
|
+
console.log(HELP)
|
|
43
|
+
break
|
|
44
|
+
default:
|
|
45
|
+
launch([cmd, ...rest])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM node:22-slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y \
|
|
4
|
+
git curl gh ca-certificates \
|
|
5
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
6
|
+
|
|
7
|
+
# Curated client CLIs — keep in sync with src/clients.js
|
|
8
|
+
RUN npm install -g \
|
|
9
|
+
@anthropic-ai/claude-code \
|
|
10
|
+
opencode-ai \
|
|
11
|
+
@openai/codex
|
|
12
|
+
|
|
13
|
+
RUN useradd -m -s /bin/bash agent
|
|
14
|
+
USER agent
|
|
15
|
+
WORKDIR /workspace
|
|
16
|
+
|
|
17
|
+
# Bake curated default configs into each client's config path.
|
|
18
|
+
# These become each client's out-of-the-box defaults inside the sandbox;
|
|
19
|
+
# `maesttro update` refreshes them by rebuilding the image.
|
|
20
|
+
COPY --chown=agent:agent configs/claude/ /home/agent/.claude/
|
|
21
|
+
COPY --chown=agent:agent configs/opencode/ /home/agent/.config/opencode/
|
|
22
|
+
COPY --chown=agent:agent configs/codex/ /home/agent/.codex/
|
|
23
|
+
|
|
24
|
+
ENTRYPOINT ["/bin/bash", "-lc"]
|
|
25
|
+
CMD ["claude"]
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "maesttro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Isolated Docker sandbox for running local coding agents (claude, opencode, codex, ...) under a dedicated bot GitHub identity, with curated configs baked in per client.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"maesttro": "./bin/maesttro.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"src",
|
|
11
|
+
"docker"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/esatto/maesttro.git",
|
|
16
|
+
"directory": "stagehand"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"private": false
|
|
23
|
+
}
|
package/src/clients.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Curated list of supported agent clients. Adding a new one here:
|
|
2
|
+
// 1. add the entry below
|
|
3
|
+
// 2. add `RUN npm install -g <npmPackage>` to docker/Dockerfile
|
|
4
|
+
// 3. create docker/configs/<key>/ with any default config to bake in
|
|
5
|
+
// then `maesttro update` (or `agent setup` during dev) picks it up.
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
claude: {
|
|
9
|
+
label: "Claude Code",
|
|
10
|
+
npmPackage: "@anthropic-ai/claude-code",
|
|
11
|
+
binary: "claude",
|
|
12
|
+
configDir: "/home/agent/.claude",
|
|
13
|
+
},
|
|
14
|
+
opencode: {
|
|
15
|
+
label: "opencode",
|
|
16
|
+
npmPackage: "opencode-ai",
|
|
17
|
+
binary: "opencode",
|
|
18
|
+
configDir: "/home/agent/.config/opencode",
|
|
19
|
+
},
|
|
20
|
+
codex: {
|
|
21
|
+
label: "OpenAI Codex CLI",
|
|
22
|
+
npmPackage: "@openai/codex",
|
|
23
|
+
binary: "codex",
|
|
24
|
+
configDir: "/home/agent/.codex",
|
|
25
|
+
},
|
|
26
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const { execSync } = require("child_process")
|
|
2
|
+
const { IMAGE, VOLUME } = require("./paths")
|
|
3
|
+
const clients = require("./clients")
|
|
4
|
+
|
|
5
|
+
function check(label, fn) {
|
|
6
|
+
try {
|
|
7
|
+
const detail = fn()
|
|
8
|
+
console.log(` [ok] ${label}${detail ? " — " + detail : ""}`)
|
|
9
|
+
return true
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.log(` [fail] ${label}`)
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function doctor() {
|
|
17
|
+
console.log("== maesttro doctor ==\n")
|
|
18
|
+
let allOk = true
|
|
19
|
+
|
|
20
|
+
allOk &= check("Docker daemon reachable", () => {
|
|
21
|
+
execSync("docker info", { stdio: "ignore" })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
allOk &= check("Sandbox image built", () => {
|
|
25
|
+
const out = execSync(
|
|
26
|
+
`docker image inspect ${IMAGE} --format "{{.Created}}"`,
|
|
27
|
+
)
|
|
28
|
+
.toString()
|
|
29
|
+
.trim()
|
|
30
|
+
return `built ${out}`
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
allOk &= check("Persistent volume exists", () => {
|
|
34
|
+
execSync(`docker volume inspect ${VOLUME}`, { stdio: "ignore" })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
allOk &= check("Bot identity authenticated", () => {
|
|
38
|
+
const out = execSync(
|
|
39
|
+
`docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "gh auth status 2>&1"`,
|
|
40
|
+
).toString()
|
|
41
|
+
const match = out.match(/Logged in to github\.com account (\S+)/)
|
|
42
|
+
return match ? match[1] : "authenticated"
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
allOk &= check(
|
|
46
|
+
"Personal SSH key NOT visible inside sandbox (isolation intact)",
|
|
47
|
+
() => {
|
|
48
|
+
const out = execSync(
|
|
49
|
+
`docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || echo MISSING"`,
|
|
50
|
+
).toString()
|
|
51
|
+
if (!out.includes("MISSING") && !out.includes("No such file")) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"SSH key is reachable inside the sandbox — isolation broken",
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
for (const [key, c] of Object.entries(clients)) {
|
|
60
|
+
allOk &= check(`Client available: ${c.label}`, () => {
|
|
61
|
+
execSync(`docker run --rm ${IMAGE} "which ${c.binary}"`, {
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log()
|
|
68
|
+
if (allOk) {
|
|
69
|
+
console.log("All checks passed.")
|
|
70
|
+
} else {
|
|
71
|
+
console.log(
|
|
72
|
+
"Some checks failed — run `maesttro setup` or `maesttro update` to fix, or check the failing item above.",
|
|
73
|
+
)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { doctor }
|
package/src/launch.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { spawnSync, execSync } = require("child_process")
|
|
2
|
+
const fs = require("fs")
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const os = require("os")
|
|
5
|
+
const { IMAGE, VOLUME } = require("./paths")
|
|
6
|
+
const clients = require("./clients")
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CLIENT = "claude"
|
|
9
|
+
|
|
10
|
+
function ensureReady() {
|
|
11
|
+
try {
|
|
12
|
+
execSync(`docker image inspect ${IMAGE}`, { stdio: "ignore" })
|
|
13
|
+
execSync(`docker volume inspect ${VOLUME}`, { stdio: "ignore" })
|
|
14
|
+
} catch {
|
|
15
|
+
console.error("Sandbox isn't set up yet. Run `maesttro setup` first.")
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function list() {
|
|
21
|
+
console.log("Available clients:\n")
|
|
22
|
+
for (const [key, c] of Object.entries(clients)) {
|
|
23
|
+
console.log(` ${key.padEnd(10)} ${c.label} (${c.npmPackage})`)
|
|
24
|
+
}
|
|
25
|
+
console.log(
|
|
26
|
+
`\nDefault: ${DEFAULT_CLIENT}. Run \`maesttro <client>\` to pick one, or \`maesttro <client> -- <args>\` to pass args through.`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function launch(cmdArgs) {
|
|
31
|
+
ensureReady()
|
|
32
|
+
|
|
33
|
+
const cwd = process.cwd()
|
|
34
|
+
const sharedSkills =
|
|
35
|
+
process.env.AGENT_SHARED_SKILLS ||
|
|
36
|
+
path.join(os.homedir(), "agent-shared", "skills")
|
|
37
|
+
|
|
38
|
+
let clientKey = DEFAULT_CLIENT
|
|
39
|
+
let extraArgs = []
|
|
40
|
+
|
|
41
|
+
if (cmdArgs.length > 0 && clients[cmdArgs[0]]) {
|
|
42
|
+
clientKey = cmdArgs[0]
|
|
43
|
+
extraArgs = cmdArgs.slice(1)
|
|
44
|
+
} else if (cmdArgs.length > 0) {
|
|
45
|
+
// not a known client — treat the whole thing as a raw command, e.g. `maesttro bash`
|
|
46
|
+
return runRaw(cmdArgs.join(" "), cwd, sharedSkills)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const client = clients[clientKey]
|
|
50
|
+
const command = [client.binary, ...extraArgs].join(" ")
|
|
51
|
+
runRaw(command, cwd, sharedSkills)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runRaw(command, cwd, sharedSkills) {
|
|
55
|
+
const mounts = ["-v", `${VOLUME}:/home/agent`, "-v", `${cwd}:/workspace`]
|
|
56
|
+
if (fs.existsSync(sharedSkills)) {
|
|
57
|
+
mounts.push("-v", `${sharedSkills}:/home/agent/.claude/skills:ro`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
"docker",
|
|
62
|
+
["run", "-it", "--rm", ...mounts, "-w", "/workspace", IMAGE, command],
|
|
63
|
+
{ stdio: "inherit" },
|
|
64
|
+
)
|
|
65
|
+
process.exit(result.status ?? 0)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { launch, list }
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const path = require("path")
|
|
2
|
+
|
|
3
|
+
// __dirname here is <install>/src — package root is one level up.
|
|
4
|
+
// This works identically whether installed via `npm link`, `npm i -g .`,
|
|
5
|
+
// or from the registry — npm always keeps the package's own files together.
|
|
6
|
+
const PKG_ROOT = path.resolve(__dirname, "..")
|
|
7
|
+
const DOCKERFILE_DIR = path.join(PKG_ROOT, "docker")
|
|
8
|
+
|
|
9
|
+
const IMAGE = "maesttro-runner:latest"
|
|
10
|
+
const VOLUME = "maesttro-home"
|
|
11
|
+
|
|
12
|
+
module.exports = { PKG_ROOT, DOCKERFILE_DIR, IMAGE, VOLUME }
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { execSync, spawnSync } = require("child_process")
|
|
2
|
+
const readline = require("readline")
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const { DOCKERFILE_DIR, IMAGE, VOLUME } = require("./paths")
|
|
5
|
+
|
|
6
|
+
function run(cmd, opts = {}) {
|
|
7
|
+
return execSync(cmd, { stdio: "inherit", ...opts })
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ask(question) {
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
})
|
|
15
|
+
return new Promise((resolve) =>
|
|
16
|
+
rl.question(question, (answer) => {
|
|
17
|
+
rl.close()
|
|
18
|
+
resolve(answer.trim())
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function setup(args) {
|
|
24
|
+
console.log("== maesttro setup ==\n")
|
|
25
|
+
|
|
26
|
+
// 1. Docker present?
|
|
27
|
+
try {
|
|
28
|
+
execSync("docker info", { stdio: "ignore" })
|
|
29
|
+
} catch {
|
|
30
|
+
console.error(
|
|
31
|
+
"Docker doesn't seem to be running. Start Docker Desktop (or the docker daemon) and try again.",
|
|
32
|
+
)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Build image from the Dockerfile bundled inside this package
|
|
37
|
+
console.log(
|
|
38
|
+
"-> Building sandbox image (this pulls node:22-slim + installs CLIs, first run only)...",
|
|
39
|
+
)
|
|
40
|
+
run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`)
|
|
41
|
+
|
|
42
|
+
// 3. Create persistent volume
|
|
43
|
+
console.log("\n-> Ensuring persistent volume exists...")
|
|
44
|
+
execSync(`docker volume create ${VOLUME}`, { stdio: "ignore" })
|
|
45
|
+
|
|
46
|
+
// 4. Get bot token — file path arg, or interactive paste
|
|
47
|
+
const tokenPath = args[0]
|
|
48
|
+
let token
|
|
49
|
+
if (tokenPath && fs.existsSync(tokenPath)) {
|
|
50
|
+
token = fs.readFileSync(tokenPath, "utf8").trim()
|
|
51
|
+
} else {
|
|
52
|
+
console.log(
|
|
53
|
+
"\nPaste the bot account's fine-grained GitHub token (input hidden not supported in plain readline — clear your scrollback after):",
|
|
54
|
+
)
|
|
55
|
+
token = await ask("Token: ")
|
|
56
|
+
}
|
|
57
|
+
if (!token) {
|
|
58
|
+
console.error(
|
|
59
|
+
"No token provided. Run `maesttro setup /path/to/token.txt` or paste one when prompted.",
|
|
60
|
+
)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 5. Authenticate the bot identity into the volume
|
|
65
|
+
console.log("\n-> Authenticating bot identity into the sandbox volume...")
|
|
66
|
+
const auth = spawnSync(
|
|
67
|
+
"docker",
|
|
68
|
+
[
|
|
69
|
+
"run",
|
|
70
|
+
"-i",
|
|
71
|
+
"--rm",
|
|
72
|
+
"-v",
|
|
73
|
+
`${VOLUME}:/home/agent`,
|
|
74
|
+
IMAGE,
|
|
75
|
+
"gh auth login --with-token && gh auth setup-git",
|
|
76
|
+
],
|
|
77
|
+
{ input: token, stdio: ["pipe", "inherit", "inherit"] },
|
|
78
|
+
)
|
|
79
|
+
if (auth.status !== 0) {
|
|
80
|
+
console.error(
|
|
81
|
+
"\nToken authentication failed — check the token is valid and has repo access.",
|
|
82
|
+
)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 6. Verify isolation
|
|
87
|
+
console.log("\n-> Verifying isolation...")
|
|
88
|
+
console.log(" gh identity inside sandbox:")
|
|
89
|
+
run(`docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "gh auth status"`)
|
|
90
|
+
console.log(" personal SSH key should NOT be visible:")
|
|
91
|
+
run(
|
|
92
|
+
`docker run --rm -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
console.log(
|
|
96
|
+
"\nSetup complete. Run `maesttro` from any project directory to start a session,",
|
|
97
|
+
)
|
|
98
|
+
console.log("or `maesttro list` to see available clients.")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { setup }
|
package/src/update.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { execSync } = require("child_process")
|
|
2
|
+
const { DOCKERFILE_DIR, IMAGE, PKG_ROOT } = require("./paths")
|
|
3
|
+
const { name: packageName } = require(`${PKG_ROOT}/package.json`)
|
|
4
|
+
|
|
5
|
+
function update() {
|
|
6
|
+
console.log("== maesttro update ==\n")
|
|
7
|
+
|
|
8
|
+
console.log(`-> Updating ${packageName} package...`)
|
|
9
|
+
execSync(`npm i -g ${packageName}@latest`, { stdio: "inherit" })
|
|
10
|
+
|
|
11
|
+
console.log("\n-> Rebuilding sandbox image with refreshed client configs...")
|
|
12
|
+
execSync(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`, { stdio: "inherit" })
|
|
13
|
+
|
|
14
|
+
console.log(
|
|
15
|
+
"\nUpdate complete. Bot identity and your project files are untouched —",
|
|
16
|
+
)
|
|
17
|
+
console.log(
|
|
18
|
+
"only the image (client versions + curated configs) was refreshed.",
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { update }
|