@atolis-hq/wake 0.1.22 → 0.2.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 +109 -55
- 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 +43 -13
- 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
|
@@ -1,4 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<img src="https://raw.githubusercontent.com/atolis-hq/wake/refs/heads/main/assets/wake-logo.svg" alt="logo" width="200" height="auto" />
|
|
5
|
+
<h1>Wake</h1>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
Autonomous software engineering control plane
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
<!-- Badges -->
|
|
13
|
+
<p>
|
|
14
|
+
<a href="https://github.com/atolis-hq/wake/commits/main">
|
|
15
|
+
<img src="https://img.shields.io/github/last-commit/atolis-hq/wake" alt="last update" />
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://github.com/atolis-hq/wake/actions/workflows/ci-cd.yml">
|
|
18
|
+
<img src="https://github.com/atolis-hq/wake/actions/workflows/ci-cd.yml/badge.svg" alt="CI/CD status" />
|
|
19
|
+
</a>
|
|
20
|
+
<!-- <a href="https://github.com/atolis-hq/wake/network/members">
|
|
21
|
+
<img src="https://img.shields.io/github/forks/atolis-hq/wake" alt="forks" />
|
|
22
|
+
</a>
|
|
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>
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<br />
|
|
45
|
+
|
|
2
46
|
|
|
3
47
|
Wake is a control plane for autonomous software engineering. It watches the
|
|
4
48
|
channels your team already uses, coordinates agent activity and involves humans
|
|
@@ -10,7 +54,19 @@ asks for input when human judgment matters, proposes a plan, launches local
|
|
|
10
54
|
coding-agent CLIs to implement changes, opens pull requests, and carries the
|
|
11
55
|
conversation forward wherever the work is already happening.
|
|
12
56
|
|
|
13
|
-
|
|
57
|
+
|
|
58
|
+
## Table of Contents
|
|
59
|
+
|
|
60
|
+
- [The Problem](#the-problem)
|
|
61
|
+
- [Vision](#vision)
|
|
62
|
+
- [Feature Overview](#feature-overview)
|
|
63
|
+
- [Where the Work Happens](#where-the-work-happens)
|
|
64
|
+
- [Supported Agent CLIs](#supported-agent-clis)
|
|
65
|
+
- [Getting Started](#getting-started)
|
|
66
|
+
- [Documentation](#documentation)
|
|
67
|
+
- [Issues & Feature Requests](#issues--feature-requests)
|
|
68
|
+
- [License](#license)
|
|
69
|
+
|
|
14
70
|
|
|
15
71
|
## The Problem
|
|
16
72
|
|
|
@@ -58,8 +114,10 @@ For more detail, see [docs/vision.md](docs/vision.md) and
|
|
|
58
114
|
- **Event-sourced and restart-safe.** The durable record is an append-only event
|
|
59
115
|
log; projections can be rebuilt, and the loop can crash and resume without
|
|
60
116
|
losing its place.
|
|
61
|
-
- **Local and inspectable.**
|
|
62
|
-
|
|
117
|
+
- **Local and inspectable.** Everything lives in a plain-file Wake home
|
|
118
|
+
directory: `config.json`, `prompts/`, and `workspaces/` at the top level for
|
|
119
|
+
what you edit or browse day-to-day, with durable/internal state (events,
|
|
120
|
+
projections, runs, logs, sandbox auth) nested under a hidden `.wake/`.
|
|
63
121
|
- **Sandbox-oriented execution.** Wake can run from a persistent Docker sandbox
|
|
64
122
|
with durable auth state and mounted Wake home data.
|
|
65
123
|
- **Runner agnostic.** Claude Code, Codex, Cursor, and fake runners sit behind
|
|
@@ -75,71 +133,67 @@ For more detail, see [docs/vision.md](docs/vision.md) and
|
|
|
75
133
|
Current runner capability differences are documented in
|
|
76
134
|
[docs/runner-comparison.md](docs/runner-comparison.md).
|
|
77
135
|
|
|
78
|
-
##
|
|
136
|
+
## Where the Work Happens
|
|
79
137
|
|
|
80
|
-
Wake
|
|
81
|
-
|
|
138
|
+
Wake has no chat UI you need to check for status. Your ticketing system is the
|
|
139
|
+
interface: Wake posts progress updates, asks clarifying questions, and reports
|
|
140
|
+
results as comments on the ticket, and reflects stage and status as labels on
|
|
141
|
+
it. When it's ready, it opens a pull request against your repo the normal way.
|
|
142
|
+
Reviewing, approving, and merging happen exactly where they already do today —
|
|
143
|
+
nothing new to learn, no separate dashboard to babysit.
|
|
82
144
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
145
|
+
A local control-plane UI exists for operators who want to watch runs, inspect
|
|
146
|
+
events, or resume a session directly, but it's a window into the same state —
|
|
147
|
+
not a required part of the workflow.
|
|
86
148
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
## Supported Agent CLIs
|
|
150
|
+
|
|
151
|
+
Wake wraps existing coding-agent CLIs rather than replacing them. Runner
|
|
152
|
+
adapters currently exist for:
|
|
91
153
|
|
|
92
|
-
|
|
93
|
-
|
|
154
|
+
- **[Claude Code](https://claude.com/claude-code)**
|
|
155
|
+
- **[Codex](https://openai.com/codex/)**
|
|
156
|
+
- **[Cursor](https://cursor.com/cli)**
|
|
94
157
|
|
|
95
|
-
|
|
96
|
-
|
|
158
|
+
Each runner sits behind the same `AgentRunner` contract, so Wake's routing,
|
|
159
|
+
lifecycle, and sandbox behavior stay the same regardless of which CLI executes
|
|
160
|
+
a given step. A fake runner adapter also exists for zero-token testing of the
|
|
161
|
+
control plane itself. See [docs/runner-comparison.md](docs/runner-comparison.md)
|
|
162
|
+
for capability differences between runners.
|
|
97
163
|
|
|
98
|
-
|
|
99
|
-
launcher runs host setup commands locally and forwards runtime commands into the
|
|
100
|
-
sandbox with the correct Wake home mounted at `/wake`.
|
|
164
|
+
## Getting Started
|
|
101
165
|
|
|
102
166
|
```sh
|
|
167
|
+
npm install -g @atolis-hq/wake
|
|
168
|
+
cd ~/
|
|
169
|
+
wake init ./wake-home
|
|
103
170
|
cd ./wake-home
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
./wake.sh start
|
|
171
|
+
wake sandbox build
|
|
172
|
+
wake sandbox up
|
|
173
|
+
wake sandbox setup
|
|
174
|
+
wake start
|
|
109
175
|
```
|
|
110
176
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
then rebuild with `./wake.sh sandbox build`. Wake writes the package location to
|
|
115
|
-
`config.json` as `dev.repoRoot` so sandbox rebuilds use the same bundled assets;
|
|
116
|
-
editing your generated Dockerfile or prompts is expected and future package
|
|
117
|
-
upgrades should not overwrite that Wake home.
|
|
118
|
-
|
|
119
|
-
Common commands:
|
|
120
|
-
|
|
121
|
-
```sh
|
|
122
|
-
./wake.sh ui
|
|
123
|
-
./wake.sh tick
|
|
124
|
-
./wake.sh start
|
|
125
|
-
./wake.sh stop
|
|
126
|
-
./wake.sh sandbox resume <session-id> --cwd "/wake/workspaces/<workId>"
|
|
127
|
-
```
|
|
177
|
+
Full setup instructions are in
|
|
178
|
+
[docs/getting-started.md](docs/getting-started.md). Run `wake --help` at
|
|
179
|
+
any time for the full command list.
|
|
128
180
|
|
|
129
|
-
|
|
181
|
+
## Documentation
|
|
130
182
|
|
|
131
|
-
- [docs/
|
|
183
|
+
- [docs/getting-started.md](docs/getting-started.md) — packaged-install setup, sandbox lifecycle, `wake doctor`.
|
|
184
|
+
- [docs/vision.md](docs/vision.md) — the rationale and long-term direction for Wake.
|
|
185
|
+
- [docs/architecture.md](docs/architecture.md) — module boundaries and the event-sourced core.
|
|
186
|
+
- [docs/workflows.md](docs/workflows.md) — how stages, prompts, and runner routes are configured.
|
|
187
|
+
- [docs/prompts.md](docs/prompts.md) — how prompt templates map to workflow stages.
|
|
188
|
+
- [docs/configuration.md](docs/configuration.md) — `config.json` options and the operator correlation escape hatch.
|
|
189
|
+
- [docs/development.md](docs/development.md) — source-checkout dev setup (`wake-dev`), npm scripts, formatting, self-update, GitHub polling.
|
|
190
|
+
- [docs/runner-comparison.md](docs/runner-comparison.md) — capability differences between supported runners.
|
|
132
191
|
|
|
133
|
-
|
|
192
|
+
## Issues & Feature Requests
|
|
134
193
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
- Prefer the prebuilt sandbox flow when running real agent work locally.
|
|
138
|
-
- Treat the default Dockerfile as a starting point. It includes common tooling
|
|
139
|
-
such as Node, but it is expected to be edited for the repositories and agents
|
|
140
|
-
you want Wake to operate on.
|
|
194
|
+
Found a bug or have an idea for Wake? [Open an issue](https://github.com/atolis-hq/wake/issues/new) —
|
|
195
|
+
bug reports and feature requests are both welcome.
|
|
141
196
|
|
|
142
|
-
##
|
|
197
|
+
## License
|
|
143
198
|
|
|
144
|
-
|
|
145
|
-
polling details are documented in [docs/development.md](docs/development.md).
|
|
199
|
+
Wake is licensed under the [Apache License 2.0](LICENSE).
|
|
@@ -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);
|
|
@@ -47,10 +62,17 @@ export async function runSandboxCommand(input) {
|
|
|
47
62
|
if (repoRoot === undefined || repoRoot.length === 0) {
|
|
48
63
|
throw new Error('Sandbox build requires config.dev.repoRoot');
|
|
49
64
|
}
|
|
65
|
+
const effectiveDevMode = input.config.dev?.mode ?? 'packaged';
|
|
66
|
+
await ensureDockerfile({
|
|
67
|
+
wakeRoot: input.wakeRoot,
|
|
68
|
+
devMode: effectiveDevMode,
|
|
69
|
+
packagedTemplatesRoot: input.packagedTemplatesRoot,
|
|
70
|
+
});
|
|
50
71
|
await input.docker.build({
|
|
51
72
|
image: input.config.sandbox.image,
|
|
52
73
|
dockerfile: resolve(input.wakeRoot, 'docker', 'Dockerfile'),
|
|
53
74
|
contextDir: repoRoot,
|
|
75
|
+
...(effectiveDevMode === 'packaged' ? { buildArgs: { WAKE_VERSION: wakeVersion } } : {}),
|
|
54
76
|
});
|
|
55
77
|
return;
|
|
56
78
|
}
|
|
@@ -113,7 +135,8 @@ export async function runSandboxCommand(input) {
|
|
|
113
135
|
throw new Error('Sandbox self-update requires config.dev.repoRoot');
|
|
114
136
|
}
|
|
115
137
|
if (input.selfUpdate === undefined) {
|
|
116
|
-
throw new Error('Sandbox self-update requires
|
|
138
|
+
throw new Error('Sandbox self-update requires dev.mode: "source". For a packaged install, update instead with:\n' +
|
|
139
|
+
' npm install -g @atolis-hq/wake@latest && wake sandbox build && wake sandbox update');
|
|
117
140
|
}
|
|
118
141
|
const selfUpdateArgs = input.args.slice(1);
|
|
119
142
|
const runSelfUpdate = selfUpdateArgs.includes('--loop')
|
|
@@ -146,7 +169,10 @@ export async function runSandboxCommand(input) {
|
|
|
146
169
|
return;
|
|
147
170
|
}
|
|
148
171
|
if (subcommand === 'setup') {
|
|
149
|
-
|
|
172
|
+
const setupCommand = input.config.dev?.mode === 'source'
|
|
173
|
+
? ['node', '/app/dist/src/main.js', 'sandbox-setup']
|
|
174
|
+
: ['wake', 'sandbox-setup'];
|
|
175
|
+
await input.docker.exec(input.config.sandbox.containerName, setupCommand, {
|
|
150
176
|
interactive: true,
|
|
151
177
|
});
|
|
152
178
|
return;
|
|
@@ -154,15 +180,18 @@ export async function runSandboxCommand(input) {
|
|
|
154
180
|
if (subcommand === 'exec') {
|
|
155
181
|
const commandArgs = input.args.slice(1);
|
|
156
182
|
const wrappedCommand = commandArgs[0] === '--' ? commandArgs.slice(1) : commandArgs;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
if (wrappedCommand.length === 0) {
|
|
184
|
+
// No command given: drop into an interactive shell with a real TTY,
|
|
185
|
+
// same as `docker exec -it ... bash`. That's a genuine interactive
|
|
186
|
+
// use case, so it stays on the inherited-stdio path rather than the
|
|
187
|
+
// piped/scrubbed one below (which would break TTY behavior).
|
|
188
|
+
await input.docker.exec(input.config.sandbox.containerName, []);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await input.docker.execCaptured(input.config.sandbox.containerName, wrappedCommand, {
|
|
192
|
+
onStdout: (line) => input.logger.info(line),
|
|
193
|
+
onStderr: (line) => (input.logger.error ?? input.logger.info)(line),
|
|
194
|
+
});
|
|
166
195
|
return;
|
|
167
196
|
}
|
|
168
197
|
if (subcommand === 'logs') {
|
|
@@ -186,6 +215,7 @@ export async function runSandboxCommand(input) {
|
|
|
186
215
|
wakeRoot: input.wakeRoot,
|
|
187
216
|
containerHomeRoot: input.containerHomeRoot,
|
|
188
217
|
buildResumeCommand: runnerAdapter.buildResumeCommand,
|
|
218
|
+
logger: input.logger,
|
|
189
219
|
});
|
|
190
220
|
return;
|
|
191
221
|
}
|
|
@@ -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
|
}
|