@atolis-hq/wake 0.1.23 → 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 +14 -65
- 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/dist/src/main.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { closeSync, existsSync, openSync } from 'node:fs';
|
|
4
|
+
import { access, chmod, copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
7
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { createDockerCli } from './adapters/docker/docker-cli.js';
|
|
8
|
+
import { createDockerCli, } from './adapters/docker/docker-cli.js';
|
|
6
9
|
import { createFileBackedFakeTicketingSystem } from './adapters/fake/fake-ticketing-system.js';
|
|
7
10
|
import { createFakeWorkspaceManager } from './adapters/fake/fake-workspace-manager.js';
|
|
8
11
|
import { createGitWorkspaceManager } from './adapters/git/git-workspace-manager.js';
|
|
@@ -17,9 +20,12 @@ import { createGitHubClient } from './adapters/github/github-client.js';
|
|
|
17
20
|
import { createGitHubIssuesWorkSource } from './adapters/github/github-issues-work-source.js';
|
|
18
21
|
import { createGitHubPullRequestActivitySource } from './adapters/github/github-pull-request-activity-source.js';
|
|
19
22
|
import { runCorrelateCommand } from './cli/correlate-command.js';
|
|
23
|
+
import { runDoctorCommand } from './cli/doctor-command.js';
|
|
20
24
|
import { runInitCommand } from './cli/init-command.js';
|
|
21
25
|
import { runSandboxCommand } from './cli/sandbox-command.js';
|
|
22
|
-
import {
|
|
26
|
+
import { runSandboxEntrypointCommand } from './cli/sandbox-entrypoint-command.js';
|
|
27
|
+
import { runSandboxSetupCommand } from './cli/sandbox-setup-command.js';
|
|
28
|
+
import { collectStartupPreflightFailures, runStartupPreflight } from './cli/startup-preflight.js';
|
|
23
29
|
import { runUiCommand } from './cli/ui-command.js';
|
|
24
30
|
import { loadWakeConfig } from './config/load-config.js';
|
|
25
31
|
import { createControlPlane } from './core/control-plane.js';
|
|
@@ -44,8 +50,33 @@ export function readFlagBeforeCommandTerminator(name, args) {
|
|
|
44
50
|
}
|
|
45
51
|
return scopedArgs[index + 1];
|
|
46
52
|
}
|
|
53
|
+
// Walks up from this file's own directory until it finds a package.json,
|
|
54
|
+
// rather than assuming a fixed number of directory levels — the file
|
|
55
|
+
// running is `dist/src/main.js` for a built/packaged install (two levels
|
|
56
|
+
// below the package root) but `src/main.ts` for a `tsx` dev invocation
|
|
57
|
+
// (only one level below), so a fixed `resolve(..., '..', '..')` overshoots
|
|
58
|
+
// the root by one directory in the latter case.
|
|
47
59
|
function resolvePackageRoot() {
|
|
48
|
-
|
|
60
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
61
|
+
for (;;) {
|
|
62
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
const parent = dirname(dir);
|
|
66
|
+
if (parent === dir) {
|
|
67
|
+
throw new Error(`Could not locate package root (no package.json found above ${dir})`);
|
|
68
|
+
}
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function hasDockerfile(wakeRoot) {
|
|
73
|
+
try {
|
|
74
|
+
await access(resolve(wakeRoot, 'docker', 'Dockerfile'));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
49
80
|
}
|
|
50
81
|
function routesOnlyToFake(config) {
|
|
51
82
|
return Object.values(config.tiers).every((candidates) => {
|
|
@@ -136,6 +167,130 @@ async function runCommandCapture(command, args) {
|
|
|
136
167
|
});
|
|
137
168
|
});
|
|
138
169
|
}
|
|
170
|
+
const codexBootstrapHome = '/home/wake/.codex';
|
|
171
|
+
const codexRuntimeHome = '/home/wake/.codex-runtime';
|
|
172
|
+
const sshHome = '/home/wake/.ssh';
|
|
173
|
+
const sshKeyPath = join(sshHome, 'id_ed25519');
|
|
174
|
+
async function prepareCodexHome() {
|
|
175
|
+
await mkdir(codexRuntimeHome, { recursive: true });
|
|
176
|
+
const bootstrapConfig = join(codexBootstrapHome, 'config.toml');
|
|
177
|
+
if (existsSync(bootstrapConfig)) {
|
|
178
|
+
await copyFile(bootstrapConfig, join(codexRuntimeHome, 'config.toml'));
|
|
179
|
+
}
|
|
180
|
+
const bootstrapAuth = join(codexBootstrapHome, 'auth.json');
|
|
181
|
+
const runtimeAuth = join(codexRuntimeHome, 'auth.json');
|
|
182
|
+
if (existsSync(bootstrapAuth) && !existsSync(runtimeAuth)) {
|
|
183
|
+
await copyFile(bootstrapAuth, runtimeAuth);
|
|
184
|
+
}
|
|
185
|
+
process.env.CODEX_HOME = codexRuntimeHome;
|
|
186
|
+
}
|
|
187
|
+
async function ensureSshKey() {
|
|
188
|
+
if (!existsSync(sshKeyPath)) {
|
|
189
|
+
await mkdir(sshHome, { recursive: true, mode: 0o700 });
|
|
190
|
+
await chmod(sshHome, 0o700);
|
|
191
|
+
await runCommand('ssh-keygen', ['-t', 'ed25519', '-f', sshKeyPath, '-N', '']);
|
|
192
|
+
}
|
|
193
|
+
// Display the public key to the user
|
|
194
|
+
const publicKeyPath = join(sshHome, 'id_ed25519.pub');
|
|
195
|
+
const publicKey = await readFile(publicKeyPath, 'utf-8');
|
|
196
|
+
console.log(publicKey);
|
|
197
|
+
}
|
|
198
|
+
async function promptYesNo(message) {
|
|
199
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
200
|
+
try {
|
|
201
|
+
const answer = (await rl.question(`${message} `)).trim().toLowerCase();
|
|
202
|
+
return answer === 'y' || answer === 'yes';
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
rl.close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function runSandboxSetup() {
|
|
209
|
+
await runSandboxSetupCommand({
|
|
210
|
+
prompt: promptYesNo,
|
|
211
|
+
runInteractive: (command, args) => runCommand(command, args),
|
|
212
|
+
ensureSshKey,
|
|
213
|
+
prepareCodexHome,
|
|
214
|
+
log: (message) => console.log(message),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const NGROK_TUNNELS_URL = 'http://127.0.0.1:4040/api/tunnels';
|
|
218
|
+
const NGROK_DISCOVERY_ATTEMPTS = 30;
|
|
219
|
+
const NGROK_DISCOVERY_INTERVAL_MS = 1000;
|
|
220
|
+
async function discoverNgrokUrl() {
|
|
221
|
+
for (let attempt = 0; attempt < NGROK_DISCOVERY_ATTEMPTS; attempt++) {
|
|
222
|
+
try {
|
|
223
|
+
const response = await fetch(NGROK_TUNNELS_URL, { signal: AbortSignal.timeout(1000) });
|
|
224
|
+
if (response.ok) {
|
|
225
|
+
const body = (await response.json());
|
|
226
|
+
const tunnels = body.tunnels ?? [];
|
|
227
|
+
const httpsTunnel = tunnels.find((tunnel) => typeof tunnel.public_url === 'string' && tunnel.public_url.startsWith('https://'));
|
|
228
|
+
const anyTunnel = httpsTunnel ?? tunnels.find((tunnel) => typeof tunnel.public_url === 'string');
|
|
229
|
+
if (typeof anyTunnel?.public_url === 'string') {
|
|
230
|
+
return anyTunnel.public_url;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// ignore and retry
|
|
236
|
+
}
|
|
237
|
+
await new Promise((resolveSleep) => setTimeout(resolveSleep, NGROK_DISCOVERY_INTERVAL_MS));
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
function createSandboxEntrypointDeps() {
|
|
242
|
+
const children = new Map();
|
|
243
|
+
return {
|
|
244
|
+
env: process.env,
|
|
245
|
+
spawnDetached: (command, args, options) => {
|
|
246
|
+
const logFd = options?.logFile !== undefined ? openSync(options.logFile, 'a') : 'ignore';
|
|
247
|
+
const child = spawn(command, args, {
|
|
248
|
+
cwd: process.cwd(),
|
|
249
|
+
env: process.env,
|
|
250
|
+
stdio: ['ignore', logFd, logFd],
|
|
251
|
+
detached: true,
|
|
252
|
+
});
|
|
253
|
+
if (typeof child.pid === 'number') {
|
|
254
|
+
children.set(child.pid, child);
|
|
255
|
+
}
|
|
256
|
+
// Registered here (at spawn time) so it runs before any exit listener
|
|
257
|
+
// waitForExit attaches later — though it wouldn't matter either way:
|
|
258
|
+
// waitForExit captures the ChildProcess reference synchronously from
|
|
259
|
+
// `children` when it's called (before the child can possibly have
|
|
260
|
+
// exited) and attaches its own listener directly to that reference,
|
|
261
|
+
// so it never re-reads `children` inside the exit callback. Deleting
|
|
262
|
+
// the map entry here is therefore safe regardless of ordering.
|
|
263
|
+
child.on('exit', () => {
|
|
264
|
+
if (typeof child.pid === 'number') {
|
|
265
|
+
children.delete(child.pid);
|
|
266
|
+
}
|
|
267
|
+
if (logFd !== 'ignore') {
|
|
268
|
+
closeSync(logFd);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return { pid: child.pid ?? -1 };
|
|
272
|
+
},
|
|
273
|
+
waitForExit: (pid) => new Promise((resolveExit) => {
|
|
274
|
+
const child = children.get(pid);
|
|
275
|
+
if (child === undefined) {
|
|
276
|
+
resolveExit(1);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
child.on('exit', (code) => resolveExit(code ?? 1));
|
|
280
|
+
}),
|
|
281
|
+
writeFile: (path, content) => writeFile(path, content, 'utf-8'),
|
|
282
|
+
sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
|
|
283
|
+
discoverNgrokUrl,
|
|
284
|
+
log: (message) => console.log(message),
|
|
285
|
+
ensureDir: async (path) => {
|
|
286
|
+
await mkdir(path, { recursive: true });
|
|
287
|
+
},
|
|
288
|
+
removeFile: (path) => rm(path, { force: true }),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async function runSandboxEntrypoint() {
|
|
292
|
+
await runSandboxEntrypointCommand(createSandboxEntrypointDeps());
|
|
293
|
+
}
|
|
139
294
|
async function readTickRequestId(path) {
|
|
140
295
|
try {
|
|
141
296
|
const request = await readJsonFile(path);
|
|
@@ -181,8 +336,107 @@ async function inspectDockerContainer(containerName) {
|
|
|
181
336
|
});
|
|
182
337
|
});
|
|
183
338
|
}
|
|
339
|
+
async function dockerDaemonReachable() {
|
|
340
|
+
return await new Promise((resolveReachable) => {
|
|
341
|
+
const child = spawn('docker', ['info'], {
|
|
342
|
+
cwd: process.cwd(),
|
|
343
|
+
env: process.env,
|
|
344
|
+
stdio: 'ignore',
|
|
345
|
+
});
|
|
346
|
+
child.on('error', () => resolveReachable(false));
|
|
347
|
+
child.on('close', (exitCode) => resolveReachable(exitCode === 0));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Builds the same `DockerCli` client the `sandbox` command path uses, so
|
|
352
|
+
* `wake doctor`'s Docker/sandbox reachability checks reuse one Docker CLI
|
|
353
|
+
* invocation implementation instead of duplicating it.
|
|
354
|
+
*/
|
|
355
|
+
function createHostDockerCli() {
|
|
356
|
+
return createDockerCli({
|
|
357
|
+
// BuildKit is required for the Dockerfile's cache-mount syntax
|
|
358
|
+
// (`RUN --mount=type=cache`), which keeps `npm`/`apt` package
|
|
359
|
+
// caches warm across builds even when a layer above them changes.
|
|
360
|
+
run: (dockerArgs) => runCommand('docker', dockerArgs, { ...process.env, DOCKER_BUILDKIT: '1' }),
|
|
361
|
+
inspectImage: inspectDockerImage,
|
|
362
|
+
inspectContainer: inspectDockerContainer,
|
|
363
|
+
spawnExec: (dockerArgs) => {
|
|
364
|
+
const child = spawn('docker', dockerArgs, {
|
|
365
|
+
cwd: process.cwd(),
|
|
366
|
+
env: process.env,
|
|
367
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
368
|
+
});
|
|
369
|
+
// stdio: ['inherit', 'pipe', 'pipe'] guarantees stdout/stderr are
|
|
370
|
+
// non-null pipes; child_process's types don't encode that.
|
|
371
|
+
return child;
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Runs `wake --version` inside the sandbox container via `docker exec` and
|
|
377
|
+
* returns the trimmed stdout, so `wake doctor` can compare it against the
|
|
378
|
+
* installed host CLI version. Stderr is discarded (surfaced only as an
|
|
379
|
+
* empty/mismatched version, which the caller already treats as a notice).
|
|
380
|
+
*/
|
|
381
|
+
async function execVersionInContainer(docker, containerName, devMode) {
|
|
382
|
+
let stdout = '';
|
|
383
|
+
const command = devMode === 'source' ? ['node', '/app/dist/src/main.js', '--version'] : ['wake', '--version'];
|
|
384
|
+
await docker.execCaptured(containerName, command, {
|
|
385
|
+
onStdout: (line) => {
|
|
386
|
+
stdout += line;
|
|
387
|
+
},
|
|
388
|
+
onStderr: () => { },
|
|
389
|
+
});
|
|
390
|
+
return stdout.trim();
|
|
391
|
+
}
|
|
392
|
+
async function readFileIfExists(path) {
|
|
393
|
+
try {
|
|
394
|
+
return await readFile(path, 'utf8');
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Compares the wake-home's `prompts/*.md` and `docker/Dockerfile` (both
|
|
402
|
+
* user-owned files, never auto-overwritten after scaffolding — see
|
|
403
|
+
* `scaffold-assets.ts`/`sandbox-command.ts`'s `ensureDockerfile`) against the
|
|
404
|
+
* currently-shipped defaults under `resolvePackageRoot()`, byte-for-byte.
|
|
405
|
+
* Only files present in *both* locations are compared: a prompt file the
|
|
406
|
+
* user hasn't customized locally, or one only shipped in a newer/older CLI
|
|
407
|
+
* version, is not drift.
|
|
408
|
+
*/
|
|
409
|
+
async function diffPromptsAndDockerfile(input) {
|
|
410
|
+
const drifted = [];
|
|
411
|
+
let promptFileNames;
|
|
412
|
+
try {
|
|
413
|
+
promptFileNames = (await readdir(resolve(input.wakeRoot, 'prompts'))).filter((name) => name.endsWith('.md'));
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
promptFileNames = [];
|
|
417
|
+
}
|
|
418
|
+
for (const fileName of promptFileNames) {
|
|
419
|
+
const local = await readFileIfExists(resolve(input.wakeRoot, 'prompts', fileName));
|
|
420
|
+
const shipped = await readFileIfExists(resolve(input.packageRoot, 'prompts', fileName));
|
|
421
|
+
if (local !== null && shipped !== null && local !== shipped) {
|
|
422
|
+
drifted.push(`prompts/${fileName}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Mirrors ensureDockerfile's template selection in sandbox-command.ts: a
|
|
426
|
+
// "source" dev mode wake-home was seeded from docker/Dockerfile, a
|
|
427
|
+
// "packaged" one from docker/Dockerfile.packaged.
|
|
428
|
+
const dockerfileTemplateName = (input.devMode ?? 'packaged') === 'source' ? 'Dockerfile' : 'Dockerfile.packaged';
|
|
429
|
+
const localDockerfile = await readFileIfExists(resolve(input.wakeRoot, 'docker', 'Dockerfile'));
|
|
430
|
+
const shippedDockerfile = await readFileIfExists(resolve(input.packageRoot, 'docker', dockerfileTemplateName));
|
|
431
|
+
if (localDockerfile !== null &&
|
|
432
|
+
shippedDockerfile !== null &&
|
|
433
|
+
localDockerfile !== shippedDockerfile) {
|
|
434
|
+
drifted.push('docker/Dockerfile');
|
|
435
|
+
}
|
|
436
|
+
return drifted;
|
|
437
|
+
}
|
|
184
438
|
export async function buildRuntime(args) {
|
|
185
|
-
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ??
|
|
439
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? process.cwd());
|
|
186
440
|
const stateStore = createStateStore({ wakeRoot });
|
|
187
441
|
await stateStore.ensureWakeRoot();
|
|
188
442
|
const config = await loadWakeConfig({
|
|
@@ -351,7 +605,7 @@ function resolveSmokEntry(config, kind) {
|
|
|
351
605
|
return null;
|
|
352
606
|
}
|
|
353
607
|
async function runUi(args) {
|
|
354
|
-
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ??
|
|
608
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? process.cwd());
|
|
355
609
|
const stateStore = createStateStore({ wakeRoot });
|
|
356
610
|
await stateStore.ensureWakeRoot();
|
|
357
611
|
const config = await loadWakeConfig({
|
|
@@ -373,7 +627,7 @@ async function runUi(args) {
|
|
|
373
627
|
await new Promise(() => { });
|
|
374
628
|
}
|
|
375
629
|
async function runCorrelate(args) {
|
|
376
|
-
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ??
|
|
630
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? process.cwd());
|
|
377
631
|
const stateStore = createStateStore({ wakeRoot });
|
|
378
632
|
await stateStore.ensureWakeRoot();
|
|
379
633
|
await runCorrelateCommand({
|
|
@@ -399,18 +653,98 @@ async function runSmoke(args) {
|
|
|
399
653
|
const result = await runnerAdapter.smoke(smokeArgs);
|
|
400
654
|
console.log(JSON.stringify(result, null, 2));
|
|
401
655
|
}
|
|
656
|
+
async function runDoctor(args) {
|
|
657
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? process.cwd());
|
|
658
|
+
const stateStore = createStateStore({ wakeRoot });
|
|
659
|
+
await stateStore.ensureWakeRoot();
|
|
660
|
+
const config = await loadWakeConfig({
|
|
661
|
+
wakeRoot,
|
|
662
|
+
configFile: stateStore.paths.configFile,
|
|
663
|
+
});
|
|
664
|
+
const docker = createHostDockerCli();
|
|
665
|
+
const packageRoot = resolvePackageRoot();
|
|
666
|
+
const containerName = config.sandbox.containerName;
|
|
667
|
+
const image = config.sandbox.image;
|
|
668
|
+
const workspaceManager = createGitWorkspaceManager({ wakeRoot });
|
|
669
|
+
const deps = {
|
|
670
|
+
collectPreflightFailures: (doctorConfig) => collectStartupPreflightFailures(doctorConfig, { workspaceManager }),
|
|
671
|
+
resolveGitHubToken,
|
|
672
|
+
hasDockerfile,
|
|
673
|
+
dockerReachable: dockerDaemonReachable,
|
|
674
|
+
inspectImage: async (imageToInspect) => {
|
|
675
|
+
try {
|
|
676
|
+
return await inspectDockerImage(imageToInspect);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
wakeRoot,
|
|
683
|
+
image,
|
|
684
|
+
containerRunning: async () => (await inspectDockerContainer(containerName)) === 'running',
|
|
685
|
+
execVersionInContainer: () => execVersionInContainer(docker, containerName, config.dev?.mode),
|
|
686
|
+
installedVersion: wakeVersion,
|
|
687
|
+
diffPromptsAndDockerfile: () => diffPromptsAndDockerfile({ wakeRoot, packageRoot, devMode: config.dev?.mode }),
|
|
688
|
+
};
|
|
689
|
+
const report = await runDoctorCommand(config, deps);
|
|
690
|
+
if (report.failures.length > 0) {
|
|
691
|
+
console.log('Failures:');
|
|
692
|
+
for (const failure of report.failures) {
|
|
693
|
+
console.log(` - ${failure}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (report.notices.length > 0) {
|
|
697
|
+
console.log('Notices:');
|
|
698
|
+
for (const notice of report.notices) {
|
|
699
|
+
console.log(` - ${notice}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (report.failures.length === 0 && report.notices.length === 0) {
|
|
703
|
+
console.log('wake doctor: no issues found');
|
|
704
|
+
}
|
|
705
|
+
if (report.failures.length > 0) {
|
|
706
|
+
process.exitCode = 1;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
export class CliUsageError extends Error {
|
|
710
|
+
}
|
|
711
|
+
export function printUsage(stream) {
|
|
712
|
+
stream.write([
|
|
713
|
+
'Wake — an autonomous agent control plane for software development.',
|
|
714
|
+
'',
|
|
715
|
+
'Usage:',
|
|
716
|
+
' wake init <path> Scaffold a new Wake home directory',
|
|
717
|
+
' wake sandbox <subcommand> Build/run/manage the Docker sandbox (build, up, update, down, stop, self-update, setup, exec, logs, resume)',
|
|
718
|
+
' wake tick Run one control-plane tick',
|
|
719
|
+
' wake start Run the resident loop',
|
|
720
|
+
' wake stop Stop the sandbox container gracefully',
|
|
721
|
+
' wake smoke Smoke-test the configured runner',
|
|
722
|
+
' wake ui Run the control-plane UI server',
|
|
723
|
+
' wake correlate Manually correlate a resource to a work item',
|
|
724
|
+
' wake doctor Diagnose config/GitHub/Docker/sandbox setup problems',
|
|
725
|
+
' wake --version Print the installed Wake version',
|
|
726
|
+
' wake --help Show this message',
|
|
727
|
+
'',
|
|
728
|
+
'Getting started:',
|
|
729
|
+
' 1. wake init ./wake-home',
|
|
730
|
+
' 2. cd wake-home && wake start',
|
|
731
|
+
'',
|
|
732
|
+
'Runtime commands (tick/start/ui/smoke/correlate) auto-delegate into the sandbox',
|
|
733
|
+
'when docker/Dockerfile exists at --wake-root (i.e. after `wake sandbox build`),',
|
|
734
|
+
'defaulting --wake-root to the current directory. Pass --no-sandbox to run',
|
|
735
|
+
'directly on the host instead.',
|
|
736
|
+
'',
|
|
737
|
+
].join('\n'));
|
|
738
|
+
}
|
|
739
|
+
const runtimeCommands = new Set(['tick', 'start', 'ui', 'smoke', 'correlate']);
|
|
402
740
|
export async function dispatchMainCommand(input) {
|
|
403
|
-
const command = input.args[0] ?? '
|
|
404
|
-
if (command === '--version' || command === '-v'
|
|
741
|
+
const command = input.args[0] ?? 'help';
|
|
742
|
+
if (command === '--version' || command === '-v') {
|
|
405
743
|
console.log(wakeVersion);
|
|
406
744
|
return;
|
|
407
745
|
}
|
|
408
|
-
if (command === '
|
|
409
|
-
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
if (command === 'start') {
|
|
413
|
-
await input.runStart(input.args.slice(1));
|
|
746
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
747
|
+
printUsage(process.stdout);
|
|
414
748
|
return;
|
|
415
749
|
}
|
|
416
750
|
if (command === 'init') {
|
|
@@ -421,26 +755,147 @@ export async function dispatchMainCommand(input) {
|
|
|
421
755
|
await input.runSandbox(input.args.slice(1));
|
|
422
756
|
return;
|
|
423
757
|
}
|
|
424
|
-
if (command === '
|
|
425
|
-
await input.
|
|
758
|
+
if (command === 'sandbox-setup') {
|
|
759
|
+
await input.runSandboxSetup(input.args.slice(1));
|
|
426
760
|
return;
|
|
427
761
|
}
|
|
428
|
-
if (command === '
|
|
429
|
-
await input.
|
|
762
|
+
if (command === 'sandbox-entrypoint') {
|
|
763
|
+
await input.runSandboxEntrypoint(input.args.slice(1));
|
|
430
764
|
return;
|
|
431
765
|
}
|
|
432
|
-
if (command === '
|
|
433
|
-
await input.
|
|
766
|
+
if (command === 'stop') {
|
|
767
|
+
await input.runSandbox(['stop', ...input.args.slice(1)]);
|
|
434
768
|
return;
|
|
435
769
|
}
|
|
436
|
-
if (command === '
|
|
437
|
-
|
|
770
|
+
if (command === 'doctor') {
|
|
771
|
+
// Host-only, like init/sandbox/stop above: doctor's job is to report on
|
|
772
|
+
// sandbox reachability from the outside, so it must never auto-delegate
|
|
773
|
+
// into the sandbox the way runtimeCommands below does.
|
|
774
|
+
await input.runDoctor(input.args.slice(1));
|
|
438
775
|
return;
|
|
439
776
|
}
|
|
440
|
-
|
|
777
|
+
if (runtimeCommands.has(command)) {
|
|
778
|
+
const commandArgs = input.args.slice(1);
|
|
779
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', commandArgs) ?? process.cwd());
|
|
780
|
+
const host = commandArgs.includes('--no-sandbox');
|
|
781
|
+
if (!host && (await hasDockerfile(wakeRoot))) {
|
|
782
|
+
await input.execIntoSandbox(input.args);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const hostArgs = commandArgs.filter((arg) => arg !== '--no-sandbox');
|
|
786
|
+
if (command === 'tick') {
|
|
787
|
+
await input.runTick(hostArgs);
|
|
788
|
+
}
|
|
789
|
+
else if (command === 'start') {
|
|
790
|
+
await input.runStart(hostArgs);
|
|
791
|
+
}
|
|
792
|
+
else if (command === 'ui') {
|
|
793
|
+
await input.runUi(hostArgs);
|
|
794
|
+
}
|
|
795
|
+
else if (command === 'smoke') {
|
|
796
|
+
await input.runSmoke(hostArgs);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
await input.runCorrelate(hostArgs);
|
|
800
|
+
}
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
printUsage(process.stderr);
|
|
804
|
+
throw new CliUsageError(`Unknown command: ${input.args.join(' ')}`);
|
|
441
805
|
}
|
|
442
806
|
async function main() {
|
|
443
807
|
const args = process.argv.slice(2);
|
|
808
|
+
const runSandbox = async (commandArgs) => {
|
|
809
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', commandArgs) ?? process.cwd());
|
|
810
|
+
const stateStore = createStateStore({ wakeRoot });
|
|
811
|
+
await stateStore.ensureWakeRoot();
|
|
812
|
+
const config = await loadWakeConfig({
|
|
813
|
+
wakeRoot,
|
|
814
|
+
configFile: stateStore.paths.configFile,
|
|
815
|
+
});
|
|
816
|
+
const docker = createHostDockerCli();
|
|
817
|
+
const repoRoot = config.dev?.repoRoot;
|
|
818
|
+
const selfUpdate = commandArgs[0] === 'self-update' &&
|
|
819
|
+
config.dev?.mode === 'source' &&
|
|
820
|
+
repoRoot !== undefined &&
|
|
821
|
+
repoRoot.length > 0
|
|
822
|
+
? {
|
|
823
|
+
git: {
|
|
824
|
+
latestTag: async () => {
|
|
825
|
+
await runCommand('git', ['-C', repoRoot, 'fetch', '--tags']);
|
|
826
|
+
const output = await runCommandCapture('git', [
|
|
827
|
+
'-C',
|
|
828
|
+
repoRoot,
|
|
829
|
+
'tag',
|
|
830
|
+
'--list',
|
|
831
|
+
'v*',
|
|
832
|
+
'--sort=-v:refname',
|
|
833
|
+
]);
|
|
834
|
+
const [latest] = output.split('\n').filter((line) => line.trim().length > 0);
|
|
835
|
+
if (latest === undefined) {
|
|
836
|
+
throw new Error('No version tags found in repo');
|
|
837
|
+
}
|
|
838
|
+
return latest.trim();
|
|
839
|
+
},
|
|
840
|
+
isWorkingTreeClean: async () => {
|
|
841
|
+
const output = await runCommandCapture('git', [
|
|
842
|
+
'-C',
|
|
843
|
+
repoRoot,
|
|
844
|
+
'status',
|
|
845
|
+
'--porcelain',
|
|
846
|
+
]);
|
|
847
|
+
return output.trim().length === 0;
|
|
848
|
+
},
|
|
849
|
+
checkoutTag: async (tag) => {
|
|
850
|
+
await runCommand('git', ['-C', repoRoot, 'checkout', tag]);
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
issueReporter: {
|
|
854
|
+
createIssue: async (issue) => {
|
|
855
|
+
const remoteUrl = await runCommandCapture('git', [
|
|
856
|
+
'-C',
|
|
857
|
+
repoRoot,
|
|
858
|
+
'remote',
|
|
859
|
+
'get-url',
|
|
860
|
+
'origin',
|
|
861
|
+
]);
|
|
862
|
+
const repoSlug = parseGithubRepoSlug(remoteUrl);
|
|
863
|
+
await runCommand('gh', [
|
|
864
|
+
'issue',
|
|
865
|
+
'create',
|
|
866
|
+
'--repo',
|
|
867
|
+
repoSlug,
|
|
868
|
+
'--title',
|
|
869
|
+
issue.title,
|
|
870
|
+
'--body',
|
|
871
|
+
issue.body,
|
|
872
|
+
]);
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
readLedger: () => readSelfUpdateLedger(resolve(wakeRoot, 'self-update-ledger.json')),
|
|
876
|
+
writeLedger: (ledger) => writeSelfUpdateLedger(resolve(wakeRoot, 'self-update-ledger.json'), ledger),
|
|
877
|
+
}
|
|
878
|
+
: undefined;
|
|
879
|
+
await runSandboxCommand({
|
|
880
|
+
args: commandArgs,
|
|
881
|
+
config,
|
|
882
|
+
wakeRoot,
|
|
883
|
+
containerHomeRoot: stateStore.paths.containerHomeRoot,
|
|
884
|
+
docker,
|
|
885
|
+
packagedTemplatesRoot: resolve(resolvePackageRoot(), 'docker'),
|
|
886
|
+
stateStore,
|
|
887
|
+
sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
|
|
888
|
+
logger: {
|
|
889
|
+
info(message) {
|
|
890
|
+
console.log(message);
|
|
891
|
+
},
|
|
892
|
+
error(message) {
|
|
893
|
+
console.error(message);
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
selfUpdate,
|
|
897
|
+
});
|
|
898
|
+
};
|
|
444
899
|
await dispatchMainCommand({
|
|
445
900
|
args,
|
|
446
901
|
runInit: async (commandArgs) => {
|
|
@@ -450,7 +905,24 @@ async function main() {
|
|
|
450
905
|
repoRoot: resolvePackageRoot(),
|
|
451
906
|
});
|
|
452
907
|
},
|
|
453
|
-
runSandbox
|
|
908
|
+
runSandbox,
|
|
909
|
+
runSandboxSetup: async () => {
|
|
910
|
+
await runSandboxSetup();
|
|
911
|
+
},
|
|
912
|
+
runSandboxEntrypoint: async () => {
|
|
913
|
+
await runSandboxEntrypoint();
|
|
914
|
+
},
|
|
915
|
+
runTick,
|
|
916
|
+
runStart,
|
|
917
|
+
runSmoke,
|
|
918
|
+
runUi,
|
|
919
|
+
runCorrelate,
|
|
920
|
+
execIntoSandbox: async (commandArgs) => {
|
|
921
|
+
const withoutBypassFlag = commandArgs.filter((arg) => arg !== '--no-sandbox');
|
|
922
|
+
const wakeRootIndex = withoutBypassFlag.indexOf('--wake-root');
|
|
923
|
+
const rewritten = wakeRootIndex === -1
|
|
924
|
+
? [...withoutBypassFlag, '--wake-root', '/wake']
|
|
925
|
+
: withoutBypassFlag.map((arg, index) => (index === wakeRootIndex + 1 ? '/wake' : arg));
|
|
454
926
|
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', commandArgs) ?? process.cwd());
|
|
455
927
|
const stateStore = createStateStore({ wakeRoot });
|
|
456
928
|
await stateStore.ensureWakeRoot();
|
|
@@ -458,100 +930,18 @@ async function main() {
|
|
|
458
930
|
wakeRoot,
|
|
459
931
|
configFile: stateStore.paths.configFile,
|
|
460
932
|
});
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
// (`RUN --mount=type=cache`), which keeps `npm`/`apt` package
|
|
464
|
-
// caches warm across builds even when a layer above them changes.
|
|
465
|
-
run: (dockerArgs) => runCommand('docker', dockerArgs, { ...process.env, DOCKER_BUILDKIT: '1' }),
|
|
466
|
-
inspectImage: inspectDockerImage,
|
|
467
|
-
inspectContainer: inspectDockerContainer,
|
|
468
|
-
});
|
|
469
|
-
const repoRoot = config.dev?.repoRoot;
|
|
470
|
-
const selfUpdate = commandArgs[0] === 'self-update' && repoRoot !== undefined && repoRoot.length > 0
|
|
471
|
-
? {
|
|
472
|
-
git: {
|
|
473
|
-
latestTag: async () => {
|
|
474
|
-
await runCommand('git', ['-C', repoRoot, 'fetch', '--tags']);
|
|
475
|
-
const output = await runCommandCapture('git', [
|
|
476
|
-
'-C',
|
|
477
|
-
repoRoot,
|
|
478
|
-
'tag',
|
|
479
|
-
'--list',
|
|
480
|
-
'v*',
|
|
481
|
-
'--sort=-v:refname',
|
|
482
|
-
]);
|
|
483
|
-
const [latest] = output.split('\n').filter((line) => line.trim().length > 0);
|
|
484
|
-
if (latest === undefined) {
|
|
485
|
-
throw new Error('No version tags found in repo');
|
|
486
|
-
}
|
|
487
|
-
return latest.trim();
|
|
488
|
-
},
|
|
489
|
-
isWorkingTreeClean: async () => {
|
|
490
|
-
const output = await runCommandCapture('git', [
|
|
491
|
-
'-C',
|
|
492
|
-
repoRoot,
|
|
493
|
-
'status',
|
|
494
|
-
'--porcelain',
|
|
495
|
-
]);
|
|
496
|
-
return output.trim().length === 0;
|
|
497
|
-
},
|
|
498
|
-
checkoutTag: async (tag) => {
|
|
499
|
-
await runCommand('git', ['-C', repoRoot, 'checkout', tag]);
|
|
500
|
-
},
|
|
501
|
-
},
|
|
502
|
-
issueReporter: {
|
|
503
|
-
createIssue: async (issue) => {
|
|
504
|
-
const remoteUrl = await runCommandCapture('git', [
|
|
505
|
-
'-C',
|
|
506
|
-
repoRoot,
|
|
507
|
-
'remote',
|
|
508
|
-
'get-url',
|
|
509
|
-
'origin',
|
|
510
|
-
]);
|
|
511
|
-
const repoSlug = parseGithubRepoSlug(remoteUrl);
|
|
512
|
-
await runCommand('gh', [
|
|
513
|
-
'issue',
|
|
514
|
-
'create',
|
|
515
|
-
'--repo',
|
|
516
|
-
repoSlug,
|
|
517
|
-
'--title',
|
|
518
|
-
issue.title,
|
|
519
|
-
'--body',
|
|
520
|
-
issue.body,
|
|
521
|
-
]);
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
readLedger: () => readSelfUpdateLedger(resolve(wakeRoot, 'self-update-ledger.json')),
|
|
525
|
-
writeLedger: (ledger) => writeSelfUpdateLedger(resolve(wakeRoot, 'self-update-ledger.json'), ledger),
|
|
526
|
-
}
|
|
527
|
-
: undefined;
|
|
528
|
-
await runSandboxCommand({
|
|
529
|
-
args: commandArgs,
|
|
530
|
-
config,
|
|
531
|
-
wakeRoot,
|
|
532
|
-
containerHomeRoot: stateStore.paths.containerHomeRoot,
|
|
533
|
-
docker,
|
|
534
|
-
stateStore,
|
|
535
|
-
sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
|
|
536
|
-
logger: {
|
|
537
|
-
info(message) {
|
|
538
|
-
console.log(message);
|
|
539
|
-
},
|
|
540
|
-
error(message) {
|
|
541
|
-
console.error(message);
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
selfUpdate,
|
|
545
|
-
});
|
|
933
|
+
const wakeInvocation = config.dev?.mode === 'source' ? ['node', '/app/dist/src/main.js'] : ['wake'];
|
|
934
|
+
await runSandbox(['exec', '--', ...wakeInvocation, ...rewritten]);
|
|
546
935
|
},
|
|
547
|
-
|
|
548
|
-
runStart,
|
|
549
|
-
runSmoke,
|
|
550
|
-
runUi,
|
|
551
|
-
runCorrelate,
|
|
936
|
+
runDoctor,
|
|
552
937
|
});
|
|
553
938
|
}
|
|
554
939
|
main().catch((error) => {
|
|
555
|
-
|
|
940
|
+
if (error instanceof CliUsageError) {
|
|
941
|
+
console.error(error.message);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
console.error(error);
|
|
945
|
+
}
|
|
556
946
|
process.exitCode = 1;
|
|
557
947
|
});
|