@atolis-hq/wake 0.1.23 → 0.2.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.
- package/README.md +25 -92
- package/dist/src/adapters/docker/docker-cli.js +45 -1
- package/dist/src/adapters/fs/state-store.js +6 -6
- package/dist/src/adapters/github/github-issues-work-source.js +2 -2
- package/dist/src/cli/doctor-command.js +48 -0
- package/dist/src/cli/init-command.js +8 -1
- package/dist/src/cli/sandbox-command.js +71 -15
- package/dist/src/cli/sandbox-entrypoint-command.js +88 -0
- package/dist/src/cli/sandbox-exec-logging.js +7 -0
- package/dist/src/cli/sandbox-resume.js +11 -9
- package/dist/src/cli/sandbox-setup-command.js +31 -0
- package/dist/src/cli/scaffold-assets.js +45 -126
- package/dist/src/cli/self-update-command.js +1 -1
- package/dist/src/cli/startup-preflight.js +5 -1
- package/dist/src/config/load-config.js +5 -1
- package/dist/src/domain/schema.js +1 -0
- package/dist/src/lib/paths.js +26 -23
- package/dist/src/main.js +506 -116
- package/dist/src/version.js +1 -1
- package/docker/Dockerfile +8 -2
- package/docker/Dockerfile.packaged +53 -0
- package/package.json +1 -1
- package/dist/src/cli/sandbox-logging.js +0 -30
package/README.md
CHANGED
|
@@ -11,33 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
<!-- Badges -->
|
|
13
13
|
<p>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<a href="https://github.com/atolis-hq/wake/stargazers">
|
|
24
|
-
<img src="https://img.shields.io/github/stars/atolis-hq/wake" alt="stars" />
|
|
25
|
-
</a> -->
|
|
26
|
-
<a href="https://github.com/atolis-hq/wake/issues/">
|
|
27
|
-
<img src="https://img.shields.io/github/issues/atolis-hq/wake" alt="open issues" />
|
|
28
|
-
</a>
|
|
29
|
-
<a href="https://github.com/atolis-hq/wake/blob/main/LICENSE">
|
|
30
|
-
<img src="https://img.shields.io/github/license/atolis-hq/wake.svg" alt="license" />
|
|
31
|
-
</a>
|
|
32
|
-
<a href="https://github.com/atolis-hq/wake/tags">
|
|
33
|
-
<img src="https://img.shields.io/github/v/tag/atolis-hq/wake" alt="latest tag" />
|
|
34
|
-
</a>
|
|
35
|
-
<a href="https://www.npmjs.com/package/@atolis-hq/wake">
|
|
36
|
-
<img src="https://img.shields.io/npm/v/%40atolis-hq%2Fwake" alt="npm version" />
|
|
37
|
-
</a>
|
|
38
|
-
<a href="docker/Dockerfile">
|
|
39
|
-
<img src="https://img.shields.io/badge/sandbox-docker-2496ED?logo=docker&logoColor=white" alt="runs in docker" />
|
|
40
|
-
</a>
|
|
14
|
+
<a href="https://github.com/atolis-hq/wake/commits/main"><img src="https://img.shields.io/github/last-commit/atolis-hq/wake" alt="last update" /></a>
|
|
15
|
+
<a href="https://github.com/atolis-hq/wake/actions/workflows/ci-cd.yml"><img src="https://github.com/atolis-hq/wake/actions/workflows/ci-cd.yml/badge.svg" alt="CI/CD status" /></a>
|
|
16
|
+
<!-- <a href="https://github.com/atolis-hq/wake/network/members"><img src="https://img.shields.io/github/forks/atolis-hq/wake" alt="forks" /></a>
|
|
17
|
+
<a href="https://github.com/atolis-hq/wake/stargazers"><img src="https://img.shields.io/github/stars/atolis-hq/wake" alt="stars" /></a> -->
|
|
18
|
+
<a href="https://github.com/atolis-hq/wake/issues/"><img src="https://img.shields.io/github/issues/atolis-hq/wake" alt="open issues" /></a>
|
|
19
|
+
<a href="https://github.com/atolis-hq/wake/blob/main/LICENSE"><img src="https://img.shields.io/github/license/atolis-hq/wake.svg" alt="license" /></a>
|
|
20
|
+
<a href="https://github.com/atolis-hq/wake/tags"><img src="https://img.shields.io/github/v/tag/atolis-hq/wake" alt="latest tag" /></a>
|
|
21
|
+
<a href="https://www.npmjs.com/package/@atolis-hq/wake"><img src="https://img.shields.io/npm/v/%40atolis-hq%2Fwake" alt="npm version" /></a>
|
|
22
|
+
<a href="docker/Dockerfile"><img src="https://img.shields.io/badge/sandbox-docker-2496ED?logo=docker&logoColor=white" alt="runs in docker" /></a>
|
|
41
23
|
</p>
|
|
42
24
|
</div>
|
|
43
25
|
|
|
@@ -63,7 +45,6 @@ conversation forward wherever the work is already happening.
|
|
|
63
45
|
- [Where the Work Happens](#where-the-work-happens)
|
|
64
46
|
- [Supported Agent CLIs](#supported-agent-clis)
|
|
65
47
|
- [Getting Started](#getting-started)
|
|
66
|
-
- [Development](#development)
|
|
67
48
|
- [Documentation](#documentation)
|
|
68
49
|
- [Issues & Feature Requests](#issues--feature-requests)
|
|
69
50
|
- [License](#license)
|
|
@@ -115,8 +96,10 @@ For more detail, see [docs/vision.md](docs/vision.md) and
|
|
|
115
96
|
- **Event-sourced and restart-safe.** The durable record is an append-only event
|
|
116
97
|
log; projections can be rebuilt, and the loop can crash and resume without
|
|
117
98
|
losing its place.
|
|
118
|
-
- **Local and inspectable.**
|
|
119
|
-
|
|
99
|
+
- **Local and inspectable.** Everything lives in a plain-file Wake home
|
|
100
|
+
directory: `config.json`, `prompts/`, and `workspaces/` at the top level for
|
|
101
|
+
what you edit or browse day-to-day, with durable/internal state (events,
|
|
102
|
+
projections, runs, logs, sandbox auth) nested under a hidden `.wake/`.
|
|
120
103
|
- **Sandbox-oriented execution.** Wake can run from a persistent Docker sandbox
|
|
121
104
|
with durable auth state and mounted Wake home data.
|
|
122
105
|
- **Runner agnostic.** Claude Code, Codex, Cursor, and fake runners sit behind
|
|
@@ -162,82 +145,32 @@ for capability differences between runners.
|
|
|
162
145
|
|
|
163
146
|
## Getting Started
|
|
164
147
|
|
|
165
|
-
Wake is distributed as the `@atolis-hq/wake` npm package. You can run the CLI
|
|
166
|
-
with `npx` or install it globally:
|
|
167
|
-
|
|
168
|
-
```sh
|
|
169
|
-
npx @atolis-hq/wake init ./wake-home
|
|
170
|
-
```
|
|
171
|
-
|
|
172
148
|
```sh
|
|
173
149
|
npm install -g @atolis-hq/wake
|
|
150
|
+
cd ~/
|
|
174
151
|
wake init ./wake-home
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
`wake init` creates a Wake home directory with `config.json`, prompt templates,
|
|
178
|
-
Docker sandbox assets, runtime directories, and shell launchers:
|
|
179
|
-
|
|
180
|
-
- `wake.sh` for bash, Git Bash, WSL, and similar shells.
|
|
181
|
-
- `wake.ps1` for PowerShell.
|
|
182
|
-
|
|
183
|
-
Use the generated launcher from the Wake home for day-to-day operation. The
|
|
184
|
-
launcher runs host setup commands locally and forwards runtime commands into the
|
|
185
|
-
sandbox with the correct Wake home mounted at `/wake`.
|
|
186
|
-
|
|
187
|
-
```sh
|
|
188
152
|
cd ./wake-home
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
./wake.sh start
|
|
153
|
+
wake sandbox build
|
|
154
|
+
wake sandbox up
|
|
155
|
+
wake sandbox setup
|
|
156
|
+
wake start
|
|
194
157
|
```
|
|
195
158
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
`config.json` as `dev.repoRoot` so sandbox rebuilds use the same bundled assets;
|
|
201
|
-
editing your generated Dockerfile or prompts is expected and future package
|
|
202
|
-
upgrades should not overwrite that Wake home.
|
|
203
|
-
|
|
204
|
-
Common commands:
|
|
205
|
-
|
|
206
|
-
```sh
|
|
207
|
-
./wake.sh ui
|
|
208
|
-
./wake.sh tick
|
|
209
|
-
./wake.sh start
|
|
210
|
-
./wake.sh stop
|
|
211
|
-
./wake.sh sandbox resume <session-id> --cwd "/wake/workspaces/<workId>"
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
For a source checkout development workflow, use:
|
|
215
|
-
|
|
216
|
-
- [docs/development.md](docs/development.md)
|
|
217
|
-
|
|
218
|
-
Recommended local practices:
|
|
219
|
-
|
|
220
|
-
- Use a separate git identity for Wake-managed agent work so automated commits
|
|
221
|
-
and human commits are easy to distinguish.
|
|
222
|
-
- Prefer the prebuilt sandbox flow when running real agent work locally.
|
|
223
|
-
- Treat the default Dockerfile as a starting point. It includes common tooling
|
|
224
|
-
such as Node, but it is expected to be edited for the repositories and agents
|
|
225
|
-
you want Wake to operate on.
|
|
226
|
-
|
|
227
|
-
## Development
|
|
228
|
-
|
|
229
|
-
Local setup, commands, sandbox operation, auth setup, UI notes, and GitHub
|
|
230
|
-
polling details are documented in [docs/development.md](docs/development.md).
|
|
159
|
+
Full setup instructions are in
|
|
160
|
+
[docs/getting-started.md](docs/getting-started.md). Run `wake --help` at
|
|
161
|
+
any time for the full command list, or see
|
|
162
|
+
[docs/cli.md](docs/cli.md) for the full command reference.
|
|
231
163
|
|
|
232
164
|
## Documentation
|
|
233
165
|
|
|
166
|
+
- [docs/getting-started.md](docs/getting-started.md) — packaged-install setup, sandbox lifecycle, `wake doctor`.
|
|
167
|
+
- [docs/cli.md](docs/cli.md) — full CLI command reference.
|
|
234
168
|
- [docs/vision.md](docs/vision.md) — the rationale and long-term direction for Wake.
|
|
235
169
|
- [docs/architecture.md](docs/architecture.md) — module boundaries and the event-sourced core.
|
|
236
|
-
- [docs/implementation.md](docs/implementation.md) — the accepted implementation plan.
|
|
237
170
|
- [docs/workflows.md](docs/workflows.md) — how stages, prompts, and runner routes are configured.
|
|
238
171
|
- [docs/prompts.md](docs/prompts.md) — how prompt templates map to workflow stages.
|
|
239
172
|
- [docs/configuration.md](docs/configuration.md) — `config.json` options and the operator correlation escape hatch.
|
|
240
|
-
- [docs/development.md](docs/development.md) —
|
|
173
|
+
- [docs/development.md](docs/development.md) — source-checkout dev setup (`wake-dev`), npm scripts, formatting, self-update, GitHub polling.
|
|
241
174
|
- [docs/runner-comparison.md](docs/runner-comparison.md) — capability differences between supported runners.
|
|
242
175
|
|
|
243
176
|
## Issues & Feature Requests
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { scrubSecrets } from '../../cli/sandbox-exec-logging.js';
|
|
1
3
|
function buildStopArgs(containerName, timeoutSeconds) {
|
|
2
4
|
return [
|
|
3
5
|
'stop',
|
|
@@ -50,7 +52,19 @@ function buildRunArgs(input) {
|
|
|
50
52
|
export function createDockerCli(deps) {
|
|
51
53
|
return {
|
|
52
54
|
async build(input) {
|
|
53
|
-
|
|
55
|
+
const buildArgFlags = Object.entries(input.buildArgs ?? {}).flatMap(([key, value]) => [
|
|
56
|
+
'--build-arg',
|
|
57
|
+
`${key}=${value}`,
|
|
58
|
+
]);
|
|
59
|
+
await deps.run([
|
|
60
|
+
'build',
|
|
61
|
+
'-t',
|
|
62
|
+
input.image,
|
|
63
|
+
'-f',
|
|
64
|
+
input.dockerfile,
|
|
65
|
+
...buildArgFlags,
|
|
66
|
+
input.contextDir,
|
|
67
|
+
]);
|
|
54
68
|
},
|
|
55
69
|
async up(input) {
|
|
56
70
|
const imageExists = await deps.inspectImage(input.image);
|
|
@@ -90,6 +104,36 @@ export function createDockerCli(deps) {
|
|
|
90
104
|
? ['exec', interactive ? '-it' : '-i', containerName, ...command]
|
|
91
105
|
: ['exec', '-it', containerName, 'bash']);
|
|
92
106
|
},
|
|
107
|
+
/**
|
|
108
|
+
* Runs `docker exec -i <containerName> <command>` with piped (not
|
|
109
|
+
* inherited) stdio, line-buffers stdout/stderr, scrubs secrets from each
|
|
110
|
+
* line, and forwards it to the caller's handlers in real time. Used by
|
|
111
|
+
* `sandbox exec` so live output is observed and redacted on the host
|
|
112
|
+
* instead of being wrapped by a mounted in-container script.
|
|
113
|
+
*/
|
|
114
|
+
async execCaptured(containerName, command, handlers) {
|
|
115
|
+
if (deps.spawnExec === undefined) {
|
|
116
|
+
throw new Error('docker cli adapter was not configured with spawnExec');
|
|
117
|
+
}
|
|
118
|
+
const args = ['exec', '-i', containerName, ...command];
|
|
119
|
+
const child = deps.spawnExec(args);
|
|
120
|
+
const stdoutReader = createInterface({ input: child.stdout });
|
|
121
|
+
stdoutReader.on('line', (line) => handlers.onStdout(scrubSecrets(line)));
|
|
122
|
+
const stderrReader = createInterface({ input: child.stderr });
|
|
123
|
+
stderrReader.on('line', (line) => handlers.onStderr(scrubSecrets(line)));
|
|
124
|
+
await new Promise((resolveExec, reject) => {
|
|
125
|
+
child.on('error', reject);
|
|
126
|
+
child.on('close', (exitCode) => {
|
|
127
|
+
stdoutReader.close();
|
|
128
|
+
stderrReader.close();
|
|
129
|
+
if (exitCode === 0) {
|
|
130
|
+
resolveExec();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
reject(new Error(`docker ${args.join(' ')} failed with exit code ${exitCode ?? 1}`));
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
},
|
|
93
137
|
async logs(containerName, tailLines) {
|
|
94
138
|
await deps.run(['logs', '--tail', String(tailLines), containerName]);
|
|
95
139
|
},
|
|
@@ -58,7 +58,7 @@ function shouldArchiveIssueState(item, options) {
|
|
|
58
58
|
return Number.isFinite(ageMs) && ageMs > options.archiveFreshnessDays * 24 * 60 * 60 * 1000;
|
|
59
59
|
}
|
|
60
60
|
export async function listRunRecords(wakeRoot) {
|
|
61
|
-
const runsRoot = join(wakeRoot, 'runs');
|
|
61
|
+
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
62
62
|
const recordsById = new Map();
|
|
63
63
|
try {
|
|
64
64
|
const files = (await readdir(runsRoot)).filter((file) => file.endsWith('.json')).sort();
|
|
@@ -93,7 +93,7 @@ export async function listRunRecords(wakeRoot) {
|
|
|
93
93
|
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
94
94
|
}
|
|
95
95
|
async function listRunRecordsForDate(wakeRoot, date) {
|
|
96
|
-
const runsRoot = join(wakeRoot, 'runs');
|
|
96
|
+
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
97
97
|
const recordsById = new Map();
|
|
98
98
|
const bucketFiles = (await readdir(join(runsRoot, 'by-date', date)).catch(() => []))
|
|
99
99
|
.filter((file) => file.endsWith('.json'))
|
|
@@ -118,7 +118,7 @@ async function listRunRecordsForDate(wakeRoot, date) {
|
|
|
118
118
|
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
119
119
|
}
|
|
120
120
|
async function listRecentRunRecords(wakeRoot, limit) {
|
|
121
|
-
const runsRoot = join(wakeRoot, 'runs');
|
|
121
|
+
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
122
122
|
const recordsById = new Map();
|
|
123
123
|
const dateDirs = (await readdir(join(runsRoot, 'by-date')).catch(() => [])).sort().reverse();
|
|
124
124
|
for (const dateDir of dateDirs) {
|
|
@@ -227,7 +227,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
227
227
|
}
|
|
228
228
|
},
|
|
229
229
|
async listIssueStates(options = {}) {
|
|
230
|
-
const stateRoot = join(
|
|
230
|
+
const stateRoot = join(paths.dataRoot, 'state');
|
|
231
231
|
try {
|
|
232
232
|
const items = [];
|
|
233
233
|
const includeArchived = options.includeArchived ?? false;
|
|
@@ -284,7 +284,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
284
284
|
}
|
|
285
285
|
},
|
|
286
286
|
async listEventEnvelopes() {
|
|
287
|
-
const eventsRoot = join(
|
|
287
|
+
const eventsRoot = join(paths.dataRoot, 'events');
|
|
288
288
|
try {
|
|
289
289
|
const files = (await readdir(eventsRoot)).sort();
|
|
290
290
|
const envelopes = [];
|
|
@@ -299,7 +299,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
299
299
|
},
|
|
300
300
|
async listRecentEventEnvelopes(filter = {}) {
|
|
301
301
|
const limit = filter.limit ?? 200;
|
|
302
|
-
const eventsRoot = join(
|
|
302
|
+
const eventsRoot = join(paths.dataRoot, 'events');
|
|
303
303
|
const files = (await readdir(eventsRoot).catch(() => []))
|
|
304
304
|
.filter((file) => file.endsWith('.jsonl'))
|
|
305
305
|
.sort()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
2
|
import { defaultAgentIdentity } from '../../domain/schema.js';
|
|
4
3
|
import { buildResourceUri } from '../../domain/resource-uri.js';
|
|
5
4
|
import { wakeStageLabelPrefix } from '../../domain/stages.js';
|
|
6
5
|
import { wakeWorkflowLabelPrefix } from '../../domain/workflows.js';
|
|
7
6
|
import { createEventEnvelope, createUnkeyedEventEnvelope } from '../../lib/event-log.js';
|
|
7
|
+
import { createWakePaths } from '../../lib/paths.js';
|
|
8
8
|
import { wakeVersion } from '../../version.js';
|
|
9
9
|
import { buildResumeCommandForCli } from '../runner/runner-cli-adapter.js';
|
|
10
10
|
const wakeStatusLabelPrefix = 'wake:status.';
|
|
@@ -173,7 +173,7 @@ function formatControlPlaneLink(url) {
|
|
|
173
173
|
}
|
|
174
174
|
export async function readControlPlaneUiUrl(wakeRoot) {
|
|
175
175
|
try {
|
|
176
|
-
const raw = await readFile(
|
|
176
|
+
const raw = await readFile(createWakePaths(wakeRoot).controlPlaneUiUrlFile, 'utf8');
|
|
177
177
|
return formatControlPlaneLink(raw.trim()) ?? undefined;
|
|
178
178
|
}
|
|
179
179
|
catch {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export async function runDoctorCommand(config, deps) {
|
|
2
|
+
const failures = [...(await deps.collectPreflightFailures(config))];
|
|
3
|
+
const notices = [];
|
|
4
|
+
if (config.sources.github.enabled) {
|
|
5
|
+
try {
|
|
6
|
+
await deps.resolveGitHubToken();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
failures.push(`GitHub token could not be resolved: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (await deps.hasDockerfile(deps.wakeRoot)) {
|
|
14
|
+
const reachable = await deps.dockerReachable();
|
|
15
|
+
if (!reachable) {
|
|
16
|
+
failures.push('Docker daemon is not reachable');
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const imageExists = await deps.inspectImage(deps.image);
|
|
20
|
+
if (!imageExists) {
|
|
21
|
+
failures.push(`sandbox image "${deps.image}" not found — run \`wake sandbox build\``);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (await deps.containerRunning()) {
|
|
27
|
+
const sandboxVersion = await deps.execVersionInContainer();
|
|
28
|
+
if (sandboxVersion !== '' && sandboxVersion !== deps.installedVersion) {
|
|
29
|
+
notices.push(`sandbox is running version ${sandboxVersion}, installed CLI is ${deps.installedVersion} — run \`wake sandbox build && wake sandbox update\` to sync`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
notices.push(`could not check sandbox version: ${message}`);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const driftedFiles = await deps.diffPromptsAndDockerfile();
|
|
39
|
+
for (const file of driftedFiles) {
|
|
40
|
+
notices.push(`${file} differs from the currently-shipped default (not auto-overwritten)`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
notices.push(`could not check prompt/Dockerfile drift: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
return { failures, notices };
|
|
48
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { assertEmptyDirectory, scaffoldWakeHome } from './scaffold-assets.js';
|
|
3
3
|
export async function runInitCommand(input) {
|
|
4
|
-
const
|
|
4
|
+
const positionalArgs = input.args.filter((arg) => arg !== '--dev' && arg !== '--packaged');
|
|
5
|
+
const wakeRoot = resolve(input.cwd, positionalArgs[0] ?? '.');
|
|
6
|
+
const devModeOverride = input.args.includes('--dev')
|
|
7
|
+
? 'source'
|
|
8
|
+
: input.args.includes('--packaged')
|
|
9
|
+
? 'packaged'
|
|
10
|
+
: undefined;
|
|
5
11
|
await assertEmptyDirectory(wakeRoot);
|
|
6
12
|
await scaffoldWakeHome({
|
|
7
13
|
wakeRoot,
|
|
8
14
|
repoRoot: input.repoRoot,
|
|
15
|
+
...(devModeOverride === undefined ? {} : { devModeOverride }),
|
|
9
16
|
});
|
|
10
17
|
return { wakeRoot };
|
|
11
18
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { posix, resolve } from 'node:path';
|
|
3
3
|
import { createRunnerCliAdapter } from '../adapters/runner/runner-cli-adapter.js';
|
|
4
4
|
import { runSandboxResumeCommand } from './sandbox-resume.js';
|
|
5
5
|
import { runSelfUpdateCommand, runSelfUpdateLoop } from './self-update-command.js';
|
|
6
6
|
import { runStopCommand } from './stop-command.js';
|
|
7
|
-
import {
|
|
7
|
+
import { wakeVersion } from '../version.js';
|
|
8
|
+
async function ensureDockerfile(input) {
|
|
9
|
+
const targetPath = resolve(input.wakeRoot, 'docker', 'Dockerfile');
|
|
10
|
+
try {
|
|
11
|
+
await access(targetPath);
|
|
12
|
+
return; // already present — user-owned, never overwritten
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// fall through to write it
|
|
16
|
+
}
|
|
17
|
+
const mode = input.devMode ?? 'packaged';
|
|
18
|
+
const templatePath = resolve(input.packagedTemplatesRoot, mode === 'source' ? 'Dockerfile' : 'Dockerfile.packaged');
|
|
19
|
+
const content = await readFile(templatePath, 'utf8');
|
|
20
|
+
await mkdir(resolve(input.wakeRoot, 'docker'), { recursive: true });
|
|
21
|
+
await writeFile(targetPath, content, 'utf8');
|
|
22
|
+
}
|
|
8
23
|
async function ensureContainerHomeMountParents(input) {
|
|
9
24
|
for (const mount of input.extraMounts) {
|
|
10
25
|
const relativeTarget = posix.relative(input.containerHomeMountPath, mount.target);
|
|
@@ -17,6 +32,28 @@ async function ensureContainerHomeMountParents(input) {
|
|
|
17
32
|
});
|
|
18
33
|
}
|
|
19
34
|
}
|
|
35
|
+
const sandboxSubcommands = [
|
|
36
|
+
['build', 'Generate docker/Dockerfile (if missing) and build the sandbox image'],
|
|
37
|
+
['up', 'Start the sandbox container'],
|
|
38
|
+
['update', 'Recreate the sandbox container from the current image'],
|
|
39
|
+
['down', 'Stop and remove the sandbox container'],
|
|
40
|
+
['stop', 'Stop the resident loop gracefully, then the container'],
|
|
41
|
+
['self-update', 'Pull the latest tag and rebuild (source dev.mode only)'],
|
|
42
|
+
['setup', 'Run interactive first-time setup inside the container'],
|
|
43
|
+
['exec', 'Run a command inside the sandbox container'],
|
|
44
|
+
['logs', 'Print sandbox container logs'],
|
|
45
|
+
['resume', 'Resume a previous agent session inside the sandbox'],
|
|
46
|
+
];
|
|
47
|
+
export function printSandboxUsage(stream) {
|
|
48
|
+
const width = Math.max(...sandboxSubcommands.map(([name]) => name.length));
|
|
49
|
+
stream.write([
|
|
50
|
+
'Usage: wake sandbox <subcommand>',
|
|
51
|
+
'',
|
|
52
|
+
'Subcommands:',
|
|
53
|
+
...sandboxSubcommands.map(([name, description]) => ` ${name.padEnd(width)} ${description}`),
|
|
54
|
+
'',
|
|
55
|
+
].join('\n'));
|
|
56
|
+
}
|
|
20
57
|
function readFlag(name, args) {
|
|
21
58
|
const index = args.indexOf(name);
|
|
22
59
|
if (index === -1) {
|
|
@@ -39,18 +76,29 @@ function startDockerInput(config) {
|
|
|
39
76
|
}
|
|
40
77
|
export async function runSandboxCommand(input) {
|
|
41
78
|
const subcommand = input.args[0];
|
|
42
|
-
if (subcommand === undefined
|
|
43
|
-
|
|
79
|
+
if (subcommand === undefined ||
|
|
80
|
+
subcommand === '--help' ||
|
|
81
|
+
subcommand === '-h' ||
|
|
82
|
+
subcommand === 'help') {
|
|
83
|
+
printSandboxUsage(process.stdout);
|
|
84
|
+
return;
|
|
44
85
|
}
|
|
45
86
|
if (subcommand === 'build') {
|
|
46
87
|
const repoRoot = input.config.dev?.repoRoot;
|
|
47
88
|
if (repoRoot === undefined || repoRoot.length === 0) {
|
|
48
89
|
throw new Error('Sandbox build requires config.dev.repoRoot');
|
|
49
90
|
}
|
|
91
|
+
const effectiveDevMode = input.config.dev?.mode ?? 'packaged';
|
|
92
|
+
await ensureDockerfile({
|
|
93
|
+
wakeRoot: input.wakeRoot,
|
|
94
|
+
devMode: effectiveDevMode,
|
|
95
|
+
packagedTemplatesRoot: input.packagedTemplatesRoot,
|
|
96
|
+
});
|
|
50
97
|
await input.docker.build({
|
|
51
98
|
image: input.config.sandbox.image,
|
|
52
99
|
dockerfile: resolve(input.wakeRoot, 'docker', 'Dockerfile'),
|
|
53
100
|
contextDir: repoRoot,
|
|
101
|
+
...(effectiveDevMode === 'packaged' ? { buildArgs: { WAKE_VERSION: wakeVersion } } : {}),
|
|
54
102
|
});
|
|
55
103
|
return;
|
|
56
104
|
}
|
|
@@ -113,7 +161,8 @@ export async function runSandboxCommand(input) {
|
|
|
113
161
|
throw new Error('Sandbox self-update requires config.dev.repoRoot');
|
|
114
162
|
}
|
|
115
163
|
if (input.selfUpdate === undefined) {
|
|
116
|
-
throw new Error('Sandbox self-update requires
|
|
164
|
+
throw new Error('Sandbox self-update requires dev.mode: "source". For a packaged install, update instead with:\n' +
|
|
165
|
+
' npm install -g @atolis-hq/wake@latest && wake sandbox build && wake sandbox update');
|
|
117
166
|
}
|
|
118
167
|
const selfUpdateArgs = input.args.slice(1);
|
|
119
168
|
const runSelfUpdate = selfUpdateArgs.includes('--loop')
|
|
@@ -146,7 +195,10 @@ export async function runSandboxCommand(input) {
|
|
|
146
195
|
return;
|
|
147
196
|
}
|
|
148
197
|
if (subcommand === 'setup') {
|
|
149
|
-
|
|
198
|
+
const setupCommand = input.config.dev?.mode === 'source'
|
|
199
|
+
? ['node', '/app/dist/src/main.js', 'sandbox-setup']
|
|
200
|
+
: ['wake', 'sandbox-setup'];
|
|
201
|
+
await input.docker.exec(input.config.sandbox.containerName, setupCommand, {
|
|
150
202
|
interactive: true,
|
|
151
203
|
});
|
|
152
204
|
return;
|
|
@@ -154,15 +206,18 @@ export async function runSandboxCommand(input) {
|
|
|
154
206
|
if (subcommand === 'exec') {
|
|
155
207
|
const commandArgs = input.args.slice(1);
|
|
156
208
|
const wrappedCommand = commandArgs[0] === '--' ? commandArgs.slice(1) : commandArgs;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
209
|
+
if (wrappedCommand.length === 0) {
|
|
210
|
+
// No command given: drop into an interactive shell with a real TTY,
|
|
211
|
+
// same as `docker exec -it ... bash`. That's a genuine interactive
|
|
212
|
+
// use case, so it stays on the inherited-stdio path rather than the
|
|
213
|
+
// piped/scrubbed one below (which would break TTY behavior).
|
|
214
|
+
await input.docker.exec(input.config.sandbox.containerName, []);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await input.docker.execCaptured(input.config.sandbox.containerName, wrappedCommand, {
|
|
218
|
+
onStdout: (line) => input.logger.info(line),
|
|
219
|
+
onStderr: (line) => (input.logger.error ?? input.logger.info)(line),
|
|
220
|
+
});
|
|
166
221
|
return;
|
|
167
222
|
}
|
|
168
223
|
if (subcommand === 'logs') {
|
|
@@ -186,6 +241,7 @@ export async function runSandboxCommand(input) {
|
|
|
186
241
|
wakeRoot: input.wakeRoot,
|
|
187
242
|
containerHomeRoot: input.containerHomeRoot,
|
|
188
243
|
buildResumeCommand: runnerAdapter.buildResumeCommand,
|
|
244
|
+
logger: input.logger,
|
|
189
245
|
});
|
|
190
246
|
return;
|
|
191
247
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createWakePaths } from '../lib/paths.js';
|
|
2
|
+
// The entrypoint always runs against the fixed in-container mount point
|
|
3
|
+
// (/wake), so a literal wakeRoot here is safe.
|
|
4
|
+
const CONTROL_PLANE_UI_URL_FILE = createWakePaths('/wake').controlPlaneUiUrlFile;
|
|
5
|
+
const DEFAULT_UI_PORT = '4317';
|
|
6
|
+
const DEFAULT_START_RESTART_DELAY_SECONDS = 10;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves how to invoke the wake CLI from inside the container.
|
|
9
|
+
*
|
|
10
|
+
* Source-mode images (docker/Dockerfile) bake `WAKE_MAIN_JS` at build time,
|
|
11
|
+
* pointing at the compiled entrypoint under /app. Packaged-mode images
|
|
12
|
+
* (docker/Dockerfile.packaged) have no /app directory at all — the CLI is
|
|
13
|
+
* installed globally via npm and exposed as a `wake` binary on PATH, so the
|
|
14
|
+
* absence of WAKE_MAIN_JS is the signal to invoke it bare.
|
|
15
|
+
*/
|
|
16
|
+
function resolveWakeInvocation(env) {
|
|
17
|
+
const mainJs = env.WAKE_MAIN_JS;
|
|
18
|
+
return mainJs !== undefined
|
|
19
|
+
? { command: 'node', argsPrefix: [mainJs] }
|
|
20
|
+
: { command: 'wake', argsPrefix: [] };
|
|
21
|
+
}
|
|
22
|
+
async function discoverAndWriteNgrokUrl(deps) {
|
|
23
|
+
await deps.removeFile(CONTROL_PLANE_UI_URL_FILE);
|
|
24
|
+
const publicUrl = await deps.discoverNgrokUrl();
|
|
25
|
+
if (publicUrl !== undefined) {
|
|
26
|
+
await deps.writeFile(CONTROL_PLANE_UI_URL_FILE, `${publicUrl}\n`);
|
|
27
|
+
deps.log(`wake ui: ngrok tunnel available at ${publicUrl}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
deps.log('wake ui: ngrok tunnel started but public URL was not discovered; see /wake/.wake/logs/ngrok.log');
|
|
31
|
+
}
|
|
32
|
+
async function superviseWakeStart(deps, restartDelaySeconds) {
|
|
33
|
+
for (;;) {
|
|
34
|
+
deps.log('wake start: starting resident loop');
|
|
35
|
+
const { command, argsPrefix } = resolveWakeInvocation(deps.env);
|
|
36
|
+
// --no-sandbox: this already IS the sandbox — docker/Dockerfile is
|
|
37
|
+
// bind-mounted at /wake, so without this the process's own
|
|
38
|
+
// dispatchMainCommand would see it and try to auto-delegate into
|
|
39
|
+
// `docker exec`, which doesn't exist inside the container itself.
|
|
40
|
+
const { pid } = deps.spawnDetached(command, [...argsPrefix, 'start', '--wake-root', '/wake', '--no-sandbox'], { logFile: '/wake/.wake/logs/start.log' });
|
|
41
|
+
await deps.writeFile('/wake/.wake/logs/start.pid', String(pid));
|
|
42
|
+
const exitCode = await deps.waitForExit(pid);
|
|
43
|
+
deps.log(`wake start: resident loop exited with status ${exitCode}; restarting in ${restartDelaySeconds}s`);
|
|
44
|
+
await deps.sleep(restartDelaySeconds * 1000);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runSandboxEntrypointCommand(deps) {
|
|
48
|
+
const { env } = deps;
|
|
49
|
+
await deps.ensureDir('/wake/.wake/logs');
|
|
50
|
+
if (env.WAKE_UI_ENABLED === 'true') {
|
|
51
|
+
const port = env.WAKE_UI_PORT ?? DEFAULT_UI_PORT;
|
|
52
|
+
deps.log(`wake ui: starting on 0.0.0.0:${port}`);
|
|
53
|
+
const { command, argsPrefix } = resolveWakeInvocation(env);
|
|
54
|
+
// --no-sandbox: same reason as the `wake start` spawn below — without
|
|
55
|
+
// it, this process would try to `docker exec` itself from inside the
|
|
56
|
+
// container it's already running in.
|
|
57
|
+
const uiArgs = [
|
|
58
|
+
...argsPrefix,
|
|
59
|
+
'ui',
|
|
60
|
+
'--wake-root',
|
|
61
|
+
'/wake',
|
|
62
|
+
'--host',
|
|
63
|
+
'0.0.0.0',
|
|
64
|
+
'--port',
|
|
65
|
+
port,
|
|
66
|
+
'--no-sandbox',
|
|
67
|
+
];
|
|
68
|
+
if (env.WAKE_UI_TOKEN) {
|
|
69
|
+
uiArgs.push('--token', env.WAKE_UI_TOKEN);
|
|
70
|
+
}
|
|
71
|
+
deps.spawnDetached(command, uiArgs, { logFile: '/wake/.wake/logs/ui.log' });
|
|
72
|
+
if (env.WAKE_UI_TUNNEL_ENABLED === 'true') {
|
|
73
|
+
if (env.NGROK_AUTHTOKEN) {
|
|
74
|
+
const { pid } = deps.spawnDetached('ngrok', ['config', 'add-authtoken', env.NGROK_AUTHTOKEN], { logFile: '/wake/.wake/logs/ngrok.log' });
|
|
75
|
+
await deps.waitForExit(pid);
|
|
76
|
+
}
|
|
77
|
+
deps.log(`wake ui: starting ngrok tunnel for 127.0.0.1:${port}`);
|
|
78
|
+
deps.spawnDetached('ngrok', ['http', `127.0.0.1:${port}`, '--log=stdout'], {
|
|
79
|
+
logFile: '/wake/.wake/logs/ngrok.log',
|
|
80
|
+
});
|
|
81
|
+
void discoverAndWriteNgrokUrl(deps);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (env.WAKE_START_ENABLED === 'true') {
|
|
85
|
+
const restartDelaySeconds = Number(env.WAKE_START_RESTART_DELAY_SECONDS ?? DEFAULT_START_RESTART_DELAY_SECONDS);
|
|
86
|
+
void superviseWakeStart(deps, restartDelaySeconds);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const ENV_SECRET_PATTERN = /([A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|KEY)[A-Za-z0-9_]*=)[^\s]+/gi;
|
|
2
|
+
const GITHUB_TOKEN_PATTERN = /(?:gho|ghp|github_pat)_[A-Za-z0-9_]+/g;
|
|
3
|
+
export function scrubSecrets(line) {
|
|
4
|
+
return line
|
|
5
|
+
.replace(ENV_SECRET_PATTERN, '$1[REDACTED]')
|
|
6
|
+
.replace(GITHUB_TOKEN_PATTERN, '[REDACTED]');
|
|
7
|
+
}
|
|
@@ -2,7 +2,13 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { listRunRecords } from '../adapters/fs/state-store.js';
|
|
5
|
-
|
|
5
|
+
function shellQuote(value) {
|
|
6
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
function buildCwdWrappedShellCommand(cwd, command) {
|
|
9
|
+
const shellCommand = `cd ${shellQuote(cwd)} && ${command.map(shellQuote).join(' ')}`;
|
|
10
|
+
return ['sh', '-c', shellCommand];
|
|
11
|
+
}
|
|
6
12
|
function readFlag(name, args) {
|
|
7
13
|
const index = args.indexOf(name);
|
|
8
14
|
if (index === -1) {
|
|
@@ -66,12 +72,8 @@ export async function runSandboxResumeCommand(input) {
|
|
|
66
72
|
}
|
|
67
73
|
throw new Error('No resumable sandbox session selected.');
|
|
68
74
|
}
|
|
69
|
-
await input.docker.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
containerHomeRoot: input.containerHomeRoot,
|
|
74
|
-
cwd: target.workspacePath,
|
|
75
|
-
command: input.buildResumeCommand({ sessionId: target.sessionId }),
|
|
76
|
-
}));
|
|
75
|
+
await input.docker.execCaptured(input.config.sandbox.containerName, buildCwdWrappedShellCommand(target.workspacePath, input.buildResumeCommand({ sessionId: target.sessionId })), {
|
|
76
|
+
onStdout: (line) => input.logger.info(line),
|
|
77
|
+
onStderr: (line) => (input.logger.error ?? input.logger.info)(line),
|
|
78
|
+
});
|
|
77
79
|
}
|