@atolis-hq/wake 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/src/adapters/claude/claude-runner.js +388 -0
- package/dist/src/adapters/claude/prompt-templates.js +57 -0
- package/dist/src/adapters/codex/codex-runner.js +391 -0
- package/dist/src/adapters/cursor/cursor-runner.js +352 -0
- package/dist/src/adapters/docker/docker-cli.js +97 -0
- package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
- package/dist/src/adapters/fake/fake-resource-index.js +21 -0
- package/dist/src/adapters/fake/fake-runner.js +22 -0
- package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
- package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
- package/dist/src/adapters/fs/resource-index.js +84 -0
- package/dist/src/adapters/fs/self-update-ledger.js +17 -0
- package/dist/src/adapters/fs/state-store.js +364 -0
- package/dist/src/adapters/git/git-workspace-manager.js +168 -0
- package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
- package/dist/src/adapters/github/github-auth.js +16 -0
- package/dist/src/adapters/github/github-client.js +100 -0
- package/dist/src/adapters/github/github-issues-work-source.js +410 -0
- package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
- package/dist/src/adapters/http/ui-assets.js +302 -0
- package/dist/src/adapters/http/ui-data.js +389 -0
- package/dist/src/adapters/http/ui-server.js +151 -0
- package/dist/src/adapters/runner/cli-command.js +43 -0
- package/dist/src/adapters/runner/prompt-templates.js +76 -0
- package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
- package/dist/src/adapters/runner/runner-registry.js +141 -0
- package/dist/src/adapters/runner/stage-prompt.js +256 -0
- package/dist/src/adapters/runner/transcripts.js +25 -0
- package/dist/src/cli/correlate-command.js +62 -0
- package/dist/src/cli/init-command.js +11 -0
- package/dist/src/cli/locks-command.js +19 -0
- package/dist/src/cli/sandbox-command.js +191 -0
- package/dist/src/cli/sandbox-logging.js +30 -0
- package/dist/src/cli/sandbox-resume.js +77 -0
- package/dist/src/cli/scaffold-assets.js +173 -0
- package/dist/src/cli/self-update-command.js +158 -0
- package/dist/src/cli/startup-preflight.js +127 -0
- package/dist/src/cli/stop-command.js +42 -0
- package/dist/src/cli/ui-command.js +27 -0
- package/dist/src/config/defaults.js +11 -0
- package/dist/src/config/load-config.js +26 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/control-plane.js +127 -0
- package/dist/src/core/lifecycle-service.js +8 -0
- package/dist/src/core/policy-engine.js +200 -0
- package/dist/src/core/projection-updater.js +438 -0
- package/dist/src/core/quota-backoff.js +69 -0
- package/dist/src/core/sink-router.js +85 -0
- package/dist/src/core/tick-runner.js +1347 -0
- package/dist/src/domain/branch-naming.js +14 -0
- package/dist/src/domain/resource-uri.js +38 -0
- package/dist/src/domain/runner-routing.js +108 -0
- package/dist/src/domain/schema.js +685 -0
- package/dist/src/domain/sources.js +16 -0
- package/dist/src/domain/stages.js +39 -0
- package/dist/src/domain/types.js +1 -0
- package/dist/src/domain/workflows.js +83 -0
- package/dist/src/lib/clock.js +5 -0
- package/dist/src/lib/event-log.js +21 -0
- package/dist/src/lib/json-file.js +23 -0
- package/dist/src/lib/lock.js +118 -0
- package/dist/src/lib/paths.js +44 -0
- package/dist/src/lib/work-id.js +19 -0
- package/dist/src/main.js +523 -0
- package/dist/src/version.js +1 -0
- package/docker/Dockerfile +54 -0
- package/docker/entrypoint.sh +75 -0
- package/docker/log-command.sh +107 -0
- package/docker/setup.sh +72 -0
- package/package.json +52 -0
- package/prompts/implement.md +49 -0
- package/prompts/refine.md +44 -0
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createDockerCli } from './adapters/docker/docker-cli.js';
|
|
6
|
+
import { createFileBackedFakeTicketingSystem } from './adapters/fake/fake-ticketing-system.js';
|
|
7
|
+
import { createFakeWorkspaceManager } from './adapters/fake/fake-workspace-manager.js';
|
|
8
|
+
import { createGitWorkspaceManager } from './adapters/git/git-workspace-manager.js';
|
|
9
|
+
import { createRunnerCliAdapter } from './adapters/runner/runner-cli-adapter.js';
|
|
10
|
+
import { createRegistryRunner, runnerKindForOverride, } from './adapters/runner/runner-registry.js';
|
|
11
|
+
import { createResourceIndex } from './adapters/fs/resource-index.js';
|
|
12
|
+
import { createStateStore } from './adapters/fs/state-store.js';
|
|
13
|
+
import { readSelfUpdateLedger, writeSelfUpdateLedger, } from './adapters/fs/self-update-ledger.js';
|
|
14
|
+
import { resolveGitHubToken } from './adapters/github/github-auth.js';
|
|
15
|
+
import { createGitHubArtifactVerifier } from './adapters/github/github-artifact-verifier.js';
|
|
16
|
+
import { createGitHubClient } from './adapters/github/github-client.js';
|
|
17
|
+
import { createGitHubIssuesWorkSource } from './adapters/github/github-issues-work-source.js';
|
|
18
|
+
import { createGitHubPullRequestActivitySource } from './adapters/github/github-pull-request-activity-source.js';
|
|
19
|
+
import { runCorrelateCommand } from './cli/correlate-command.js';
|
|
20
|
+
import { runInitCommand } from './cli/init-command.js';
|
|
21
|
+
import { runSandboxCommand } from './cli/sandbox-command.js';
|
|
22
|
+
import { runStartupPreflight } from './cli/startup-preflight.js';
|
|
23
|
+
import { runUiCommand } from './cli/ui-command.js';
|
|
24
|
+
import { loadWakeConfig } from './config/load-config.js';
|
|
25
|
+
import { createControlPlane } from './core/control-plane.js';
|
|
26
|
+
import { createOutboundSinkRouter, createWorkSourceFanIn } from './core/sink-router.js';
|
|
27
|
+
import { createTickRunner } from './core/tick-runner.js';
|
|
28
|
+
import { systemClock } from './lib/clock.js';
|
|
29
|
+
import { configuredTicketSource } from './domain/sources.js';
|
|
30
|
+
import { wakeVersion } from './version.js';
|
|
31
|
+
function commandArgsBeforeTerminator(args) {
|
|
32
|
+
const terminatorIndex = args.indexOf('--');
|
|
33
|
+
if (terminatorIndex === -1) {
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
return args.slice(0, terminatorIndex);
|
|
37
|
+
}
|
|
38
|
+
export function readFlagBeforeCommandTerminator(name, args) {
|
|
39
|
+
const scopedArgs = commandArgsBeforeTerminator(args);
|
|
40
|
+
const index = scopedArgs.indexOf(name);
|
|
41
|
+
if (index === -1) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return scopedArgs[index + 1];
|
|
45
|
+
}
|
|
46
|
+
function hasFlag(name, args) {
|
|
47
|
+
return commandArgsBeforeTerminator(args).includes(name);
|
|
48
|
+
}
|
|
49
|
+
function resolvePackageRoot() {
|
|
50
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
51
|
+
}
|
|
52
|
+
function routesOnlyToFake(config) {
|
|
53
|
+
return Object.values(config.tiers).every((candidates) => {
|
|
54
|
+
const first = candidates[0];
|
|
55
|
+
return first !== undefined && config.runners[first]?.kind === 'fake';
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function formatTickFailureDetails(runRecord) {
|
|
59
|
+
if (runRecord === null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const summary = runRecord.summary?.trim();
|
|
63
|
+
const exitCode = runRecord.metadata !== undefined && typeof runRecord.metadata.exitCode === 'number'
|
|
64
|
+
? runRecord.metadata.exitCode
|
|
65
|
+
: undefined;
|
|
66
|
+
const stderr = runRecord.metadata !== undefined && typeof runRecord.metadata.stderr === 'string'
|
|
67
|
+
? runRecord.metadata.stderr.trim()
|
|
68
|
+
: '';
|
|
69
|
+
if (summary === undefined && exitCode === undefined && stderr.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const lines = ['Tick failure details:', `runId: ${runRecord.runId}`];
|
|
73
|
+
if (exitCode !== undefined) {
|
|
74
|
+
lines.push(`exitCode: ${exitCode}`);
|
|
75
|
+
}
|
|
76
|
+
if (summary !== undefined) {
|
|
77
|
+
lines.push('', 'Summary:', summary);
|
|
78
|
+
}
|
|
79
|
+
if (stderr.length > 0) {
|
|
80
|
+
lines.push('', 'stderr:', stderr);
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
async function runCommand(command, args, env = process.env) {
|
|
85
|
+
await new Promise((resolveRun, reject) => {
|
|
86
|
+
const child = spawn(command, args, {
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
env,
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
});
|
|
91
|
+
child.on('error', reject);
|
|
92
|
+
child.on('close', (exitCode) => {
|
|
93
|
+
if (exitCode === 0) {
|
|
94
|
+
resolveRun();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${exitCode ?? 1}`));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function parseGithubRepoSlug(remoteUrl) {
|
|
102
|
+
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(remoteUrl.trim());
|
|
103
|
+
if (match === null) {
|
|
104
|
+
throw new Error(`Could not parse a GitHub owner/repo from origin remote: ${remoteUrl}`);
|
|
105
|
+
}
|
|
106
|
+
return match[1];
|
|
107
|
+
}
|
|
108
|
+
async function runCommandCapture(command, args) {
|
|
109
|
+
return await new Promise((resolveRun, reject) => {
|
|
110
|
+
const child = spawn(command, args, {
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
env: process.env,
|
|
113
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
114
|
+
});
|
|
115
|
+
let stdout = '';
|
|
116
|
+
child.stdout.on('data', (chunk) => {
|
|
117
|
+
stdout += chunk.toString();
|
|
118
|
+
});
|
|
119
|
+
child.on('error', reject);
|
|
120
|
+
child.on('close', (exitCode) => {
|
|
121
|
+
if (exitCode === 0) {
|
|
122
|
+
resolveRun(stdout);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${exitCode ?? 1}`));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async function inspectDockerImage(image) {
|
|
130
|
+
return await new Promise((resolveInspect, reject) => {
|
|
131
|
+
const child = spawn('docker', ['image', 'inspect', image], {
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
env: process.env,
|
|
134
|
+
stdio: 'ignore',
|
|
135
|
+
});
|
|
136
|
+
child.on('error', reject);
|
|
137
|
+
child.on('close', (exitCode) => {
|
|
138
|
+
resolveInspect(exitCode === 0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function inspectDockerContainer(containerName) {
|
|
143
|
+
return await new Promise((resolveInspect, reject) => {
|
|
144
|
+
const child = spawn('docker', ['container', 'inspect', '-f', '{{.State.Running}}', containerName], {
|
|
145
|
+
cwd: process.cwd(),
|
|
146
|
+
env: process.env,
|
|
147
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
148
|
+
});
|
|
149
|
+
let stdout = '';
|
|
150
|
+
child.stdout.on('data', (chunk) => {
|
|
151
|
+
stdout += chunk.toString();
|
|
152
|
+
});
|
|
153
|
+
child.on('error', reject);
|
|
154
|
+
child.on('close', (exitCode) => {
|
|
155
|
+
if (exitCode !== 0) {
|
|
156
|
+
resolveInspect(null);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
resolveInspect(stdout.trim() === 'true' ? 'running' : 'stopped');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export async function buildRuntime(args) {
|
|
164
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? resolve(process.cwd(), '.wake'));
|
|
165
|
+
const stateStore = createStateStore({ wakeRoot });
|
|
166
|
+
await stateStore.ensureWakeRoot();
|
|
167
|
+
const config = await loadWakeConfig({
|
|
168
|
+
wakeRoot,
|
|
169
|
+
configFile: stateStore.paths.configFile,
|
|
170
|
+
});
|
|
171
|
+
await stateStore.writeConfig(config);
|
|
172
|
+
const resourceIndex = createResourceIndex({ paths: stateStore.paths });
|
|
173
|
+
const prTrackingEnabled = config.sources.github.enabled && config.sources.github.pullRequests.enabled;
|
|
174
|
+
// Resolved once and shared: resolveGitHubToken shells out to `gh auth
|
|
175
|
+
// token`, so building a separate client per consumer would triple that
|
|
176
|
+
// subprocess spawn on every startup for no benefit — all three consumers
|
|
177
|
+
// talk to the same GitHub account under the same config.
|
|
178
|
+
const githubClient = config.sources.github.enabled
|
|
179
|
+
? createGitHubClient(await resolveGitHubToken())
|
|
180
|
+
: undefined;
|
|
181
|
+
const artifactVerifier = prTrackingEnabled && githubClient !== undefined
|
|
182
|
+
? createGitHubArtifactVerifier({ client: githubClient })
|
|
183
|
+
: undefined;
|
|
184
|
+
const ticketingSystem = githubClient !== undefined
|
|
185
|
+
? createGitHubIssuesWorkSource({
|
|
186
|
+
client: githubClient,
|
|
187
|
+
stateStore,
|
|
188
|
+
config,
|
|
189
|
+
resourceIndex,
|
|
190
|
+
now: () => systemClock.now(),
|
|
191
|
+
})
|
|
192
|
+
: await createFileBackedFakeTicketingSystem({
|
|
193
|
+
fixturePath: stateStore.paths.issueFixtureFile,
|
|
194
|
+
now: () => systemClock.now(),
|
|
195
|
+
});
|
|
196
|
+
const sourceName = configuredTicketSource(config);
|
|
197
|
+
const sinkName = sourceName;
|
|
198
|
+
const pullRequestActivitySource = prTrackingEnabled && githubClient !== undefined
|
|
199
|
+
? createGitHubPullRequestActivitySource({
|
|
200
|
+
client: githubClient,
|
|
201
|
+
stateStore,
|
|
202
|
+
config,
|
|
203
|
+
resourceIndex,
|
|
204
|
+
now: () => systemClock.now(),
|
|
205
|
+
})
|
|
206
|
+
: null;
|
|
207
|
+
const workSource = createWorkSourceFanIn([
|
|
208
|
+
{
|
|
209
|
+
source: sourceName,
|
|
210
|
+
pollEvents: ticketingSystem.pollEvents,
|
|
211
|
+
},
|
|
212
|
+
...(pullRequestActivitySource === null
|
|
213
|
+
? []
|
|
214
|
+
: [{ source: 'github-pr', pollEvents: pullRequestActivitySource.pollEvents }]),
|
|
215
|
+
]);
|
|
216
|
+
const outboundSink = createOutboundSinkRouter({
|
|
217
|
+
sinks: [
|
|
218
|
+
{
|
|
219
|
+
sink: sinkName,
|
|
220
|
+
deliverIntent: ticketingSystem.deliverIntent,
|
|
221
|
+
},
|
|
222
|
+
...(pullRequestActivitySource === null
|
|
223
|
+
? []
|
|
224
|
+
: [{ sink: 'github-pr', deliverIntent: pullRequestActivitySource.deliverIntent }]),
|
|
225
|
+
],
|
|
226
|
+
config,
|
|
227
|
+
});
|
|
228
|
+
const runnerOverride = readFlagBeforeCommandTerminator('--runner', args);
|
|
229
|
+
if (runnerOverride !== undefined &&
|
|
230
|
+
runnerOverride !== 'fake' &&
|
|
231
|
+
config.runners[runnerOverride] === undefined) {
|
|
232
|
+
throw new Error(`Unsupported runner override: ${runnerOverride}`);
|
|
233
|
+
}
|
|
234
|
+
const runner = createRegistryRunner({
|
|
235
|
+
config,
|
|
236
|
+
cwd: process.cwd(),
|
|
237
|
+
...(runnerOverride === undefined ? {} : { override: runnerOverride }),
|
|
238
|
+
});
|
|
239
|
+
const workspaceManager = (runnerOverride !== undefined
|
|
240
|
+
? runnerKindForOverride(config, runnerOverride) === 'fake'
|
|
241
|
+
: routesOnlyToFake(config))
|
|
242
|
+
? createFakeWorkspaceManager(stateStore.paths.workspaceRoot)
|
|
243
|
+
: createGitWorkspaceManager({ wakeRoot });
|
|
244
|
+
const tickRunner = createTickRunner({
|
|
245
|
+
clock: systemClock,
|
|
246
|
+
config,
|
|
247
|
+
stateStore,
|
|
248
|
+
workSource,
|
|
249
|
+
outboundSink,
|
|
250
|
+
runner,
|
|
251
|
+
workspaceManager,
|
|
252
|
+
resourceIndex,
|
|
253
|
+
...(artifactVerifier === undefined ? {} : { artifactVerifier }),
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
config,
|
|
257
|
+
runnerAdapter: null,
|
|
258
|
+
runner,
|
|
259
|
+
stateStore,
|
|
260
|
+
tickRunner,
|
|
261
|
+
workspaceManager,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function runTick(args) {
|
|
265
|
+
const runtime = await buildRuntime(args);
|
|
266
|
+
const outcome = await runtime.tickRunner.runTick();
|
|
267
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
268
|
+
if (outcome.status !== 'processed' || outcome.sentinel !== 'FAILED' || outcome.runId === undefined) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const runRecord = await runtime.stateStore.readRunRecord(outcome.runId);
|
|
272
|
+
const details = formatTickFailureDetails(runRecord);
|
|
273
|
+
if (details !== null) {
|
|
274
|
+
console.error(details);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function runStart(args) {
|
|
278
|
+
const runtime = await buildRuntime(args);
|
|
279
|
+
const runnerOverride = readFlagBeforeCommandTerminator('--runner', args);
|
|
280
|
+
await runStartupPreflight(runtime.config, {
|
|
281
|
+
...(runnerOverride === undefined ? {} : { runnerOverride }),
|
|
282
|
+
workspaceManager: runtime.workspaceManager,
|
|
283
|
+
});
|
|
284
|
+
const controlPlane = createControlPlane({
|
|
285
|
+
tickRunner: runtime.tickRunner,
|
|
286
|
+
intervalMs: runtime.config.scheduler.intervalMs,
|
|
287
|
+
maxIntervalMs: runtime.config.scheduler.maxIntervalMs,
|
|
288
|
+
isPaused: () => runtime.stateStore.isPaused(),
|
|
289
|
+
logger: {
|
|
290
|
+
info(message) {
|
|
291
|
+
console.log(message);
|
|
292
|
+
},
|
|
293
|
+
error(message) {
|
|
294
|
+
console.error(message);
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
sleep(ms) {
|
|
298
|
+
return new Promise((resolveSleep) => {
|
|
299
|
+
setTimeout(resolveSleep, ms);
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
const stop = () => controlPlane.stop();
|
|
304
|
+
process.on('SIGINT', stop);
|
|
305
|
+
process.on('SIGTERM', stop);
|
|
306
|
+
await controlPlane.start();
|
|
307
|
+
}
|
|
308
|
+
function resolveSmokEntry(config, kind) {
|
|
309
|
+
for (const entry of Object.values(config.runners)) {
|
|
310
|
+
if (entry.kind === 'fake') {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (kind === undefined || entry.kind === kind) {
|
|
314
|
+
return entry;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
async function runUi(args) {
|
|
320
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? resolve(process.cwd(), '.wake'));
|
|
321
|
+
const stateStore = createStateStore({ wakeRoot });
|
|
322
|
+
await stateStore.ensureWakeRoot();
|
|
323
|
+
const config = await loadWakeConfig({
|
|
324
|
+
wakeRoot,
|
|
325
|
+
configFile: stateStore.paths.configFile,
|
|
326
|
+
});
|
|
327
|
+
const server = await runUiCommand({
|
|
328
|
+
args,
|
|
329
|
+
stateStore,
|
|
330
|
+
resourceIndex: createResourceIndex({ paths: stateStore.paths }),
|
|
331
|
+
config,
|
|
332
|
+
readFlag: readFlagBeforeCommandTerminator,
|
|
333
|
+
});
|
|
334
|
+
const stop = () => {
|
|
335
|
+
void server.close().then(() => process.exit(0));
|
|
336
|
+
};
|
|
337
|
+
process.on('SIGINT', stop);
|
|
338
|
+
process.on('SIGTERM', stop);
|
|
339
|
+
await new Promise(() => { });
|
|
340
|
+
}
|
|
341
|
+
async function runCorrelate(args) {
|
|
342
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? resolve(process.cwd(), '.wake'));
|
|
343
|
+
const stateStore = createStateStore({ wakeRoot });
|
|
344
|
+
await stateStore.ensureWakeRoot();
|
|
345
|
+
await runCorrelateCommand({
|
|
346
|
+
args,
|
|
347
|
+
stateStore,
|
|
348
|
+
resourceIndex: createResourceIndex({ paths: stateStore.paths }),
|
|
349
|
+
clock: systemClock,
|
|
350
|
+
readFlag: readFlagBeforeCommandTerminator,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async function runSmoke(args) {
|
|
354
|
+
const runtime = await buildRuntime(args);
|
|
355
|
+
const explicitKind = args[0] === 'claude' || args[0] === 'codex' || args[0] === 'cursor' ? args[0] : undefined;
|
|
356
|
+
const smokeArgs = explicitKind === undefined ? args : args.slice(1);
|
|
357
|
+
const entry = resolveSmokEntry(runtime.config, explicitKind);
|
|
358
|
+
if (entry === null) {
|
|
359
|
+
throw new Error('Smoke tests require a real runner entry (`claude`, `codex`, or `cursor`) in config.runners.');
|
|
360
|
+
}
|
|
361
|
+
const runnerAdapter = createRunnerCliAdapter({
|
|
362
|
+
entry,
|
|
363
|
+
cwd: process.cwd(),
|
|
364
|
+
});
|
|
365
|
+
const result = await runnerAdapter.smoke(smokeArgs);
|
|
366
|
+
console.log(JSON.stringify(result, null, 2));
|
|
367
|
+
}
|
|
368
|
+
export async function dispatchMainCommand(input) {
|
|
369
|
+
const command = input.args[0] ?? 'tick';
|
|
370
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
371
|
+
console.log(wakeVersion);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (command === 'tick') {
|
|
375
|
+
await input.runTick(input.args.slice(1));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (command === 'start') {
|
|
379
|
+
await input.runStart(input.args.slice(1));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (command === 'init') {
|
|
383
|
+
await input.runInit(input.args.slice(1));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (command === 'sandbox') {
|
|
387
|
+
await input.runSandbox(input.args.slice(1));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (command === 'stop') {
|
|
391
|
+
await input.runSandbox(['stop', ...input.args.slice(1)]);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (command === 'smoke') {
|
|
395
|
+
await input.runSmoke(input.args.slice(1));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (command === 'ui') {
|
|
399
|
+
await input.runUi(input.args.slice(1));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (command === 'correlate') {
|
|
403
|
+
await input.runCorrelate(input.args.slice(1));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
throw new Error(`Unknown command: ${input.args.join(' ')}`);
|
|
407
|
+
}
|
|
408
|
+
async function main() {
|
|
409
|
+
const args = process.argv.slice(2);
|
|
410
|
+
await dispatchMainCommand({
|
|
411
|
+
args,
|
|
412
|
+
runInit: async (commandArgs) => {
|
|
413
|
+
await runInitCommand({
|
|
414
|
+
cwd: process.cwd(),
|
|
415
|
+
args: commandArgs,
|
|
416
|
+
repoRoot: resolvePackageRoot(),
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
runSandbox: async (commandArgs) => {
|
|
420
|
+
const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', commandArgs) ?? process.cwd());
|
|
421
|
+
const stateStore = createStateStore({ wakeRoot });
|
|
422
|
+
await stateStore.ensureWakeRoot();
|
|
423
|
+
const config = await loadWakeConfig({
|
|
424
|
+
wakeRoot,
|
|
425
|
+
configFile: stateStore.paths.configFile,
|
|
426
|
+
});
|
|
427
|
+
const docker = createDockerCli({
|
|
428
|
+
// BuildKit is required for the Dockerfile's cache-mount syntax
|
|
429
|
+
// (`RUN --mount=type=cache`), which keeps `npm`/`apt` package
|
|
430
|
+
// caches warm across builds even when a layer above them changes.
|
|
431
|
+
run: (dockerArgs) => runCommand('docker', dockerArgs, { ...process.env, DOCKER_BUILDKIT: '1' }),
|
|
432
|
+
inspectImage: inspectDockerImage,
|
|
433
|
+
inspectContainer: inspectDockerContainer,
|
|
434
|
+
});
|
|
435
|
+
const repoRoot = config.dev?.repoRoot;
|
|
436
|
+
const selfUpdate = commandArgs[0] === 'self-update' && repoRoot !== undefined && repoRoot.length > 0
|
|
437
|
+
? {
|
|
438
|
+
git: {
|
|
439
|
+
latestTag: async () => {
|
|
440
|
+
await runCommand('git', ['-C', repoRoot, 'fetch', '--tags']);
|
|
441
|
+
const output = await runCommandCapture('git', [
|
|
442
|
+
'-C',
|
|
443
|
+
repoRoot,
|
|
444
|
+
'tag',
|
|
445
|
+
'--list',
|
|
446
|
+
'v*',
|
|
447
|
+
'--sort=-v:refname',
|
|
448
|
+
]);
|
|
449
|
+
const [latest] = output.split('\n').filter((line) => line.trim().length > 0);
|
|
450
|
+
if (latest === undefined) {
|
|
451
|
+
throw new Error('No version tags found in repo');
|
|
452
|
+
}
|
|
453
|
+
return latest.trim();
|
|
454
|
+
},
|
|
455
|
+
isWorkingTreeClean: async () => {
|
|
456
|
+
const output = await runCommandCapture('git', [
|
|
457
|
+
'-C',
|
|
458
|
+
repoRoot,
|
|
459
|
+
'status',
|
|
460
|
+
'--porcelain',
|
|
461
|
+
]);
|
|
462
|
+
return output.trim().length === 0;
|
|
463
|
+
},
|
|
464
|
+
checkoutTag: async (tag) => {
|
|
465
|
+
await runCommand('git', ['-C', repoRoot, 'checkout', tag]);
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
issueReporter: {
|
|
469
|
+
createIssue: async (issue) => {
|
|
470
|
+
const remoteUrl = await runCommandCapture('git', [
|
|
471
|
+
'-C',
|
|
472
|
+
repoRoot,
|
|
473
|
+
'remote',
|
|
474
|
+
'get-url',
|
|
475
|
+
'origin',
|
|
476
|
+
]);
|
|
477
|
+
const repoSlug = parseGithubRepoSlug(remoteUrl);
|
|
478
|
+
await runCommand('gh', [
|
|
479
|
+
'issue',
|
|
480
|
+
'create',
|
|
481
|
+
'--repo',
|
|
482
|
+
repoSlug,
|
|
483
|
+
'--title',
|
|
484
|
+
issue.title,
|
|
485
|
+
'--body',
|
|
486
|
+
issue.body,
|
|
487
|
+
]);
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
readLedger: () => readSelfUpdateLedger(resolve(wakeRoot, 'self-update-ledger.json')),
|
|
491
|
+
writeLedger: (ledger) => writeSelfUpdateLedger(resolve(wakeRoot, 'self-update-ledger.json'), ledger),
|
|
492
|
+
}
|
|
493
|
+
: undefined;
|
|
494
|
+
await runSandboxCommand({
|
|
495
|
+
args: commandArgs,
|
|
496
|
+
config,
|
|
497
|
+
wakeRoot,
|
|
498
|
+
containerHomeRoot: stateStore.paths.containerHomeRoot,
|
|
499
|
+
docker,
|
|
500
|
+
stateStore,
|
|
501
|
+
sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
|
|
502
|
+
logger: {
|
|
503
|
+
info(message) {
|
|
504
|
+
console.log(message);
|
|
505
|
+
},
|
|
506
|
+
error(message) {
|
|
507
|
+
console.error(message);
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
selfUpdate,
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
runTick,
|
|
514
|
+
runStart,
|
|
515
|
+
runSmoke,
|
|
516
|
+
runUi,
|
|
517
|
+
runCorrelate,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
main().catch((error) => {
|
|
521
|
+
console.error(error);
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const wakeVersion = "v0.0.108";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM node:20-bookworm-slim
|
|
3
|
+
|
|
4
|
+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
5
|
+
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
|
6
|
+
apt-get update \
|
|
7
|
+
&& apt-get install -y --no-install-recommends git openssh-client ca-certificates curl gnupg \
|
|
8
|
+
&& mkdir -p /etc/apt/keyrings \
|
|
9
|
+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
10
|
+
| gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
11
|
+
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
12
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
13
|
+
> /etc/apt/sources.list.d/github-cli.list \
|
|
14
|
+
&& curl -fsSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
|
|
15
|
+
| gpg --dearmor -o /etc/apt/keyrings/ngrok-archive-keyring.gpg \
|
|
16
|
+
&& chmod go+r /etc/apt/keyrings/ngrok-archive-keyring.gpg \
|
|
17
|
+
&& echo "deb [signed-by=/etc/apt/keyrings/ngrok-archive-keyring.gpg] https://ngrok-agent.s3.amazonaws.com buster main" \
|
|
18
|
+
> /etc/apt/sources.list.d/ngrok.list \
|
|
19
|
+
&& apt-get update \
|
|
20
|
+
&& apt-get install -y --no-install-recommends gh ngrok
|
|
21
|
+
|
|
22
|
+
RUN --mount=type=cache,target=/root/.npm \
|
|
23
|
+
npm install -g @anthropic-ai/claude-code @openai/codex
|
|
24
|
+
|
|
25
|
+
RUN useradd --create-home --shell /bin/bash wake \
|
|
26
|
+
&& mkdir -p /home/wake/.codex-runtime \
|
|
27
|
+
&& mkdir -p /home/wake/.cursor \
|
|
28
|
+
&& chown -R wake:wake /home/wake/.codex-runtime \
|
|
29
|
+
&& chown -R wake:wake /home/wake/.cursor
|
|
30
|
+
|
|
31
|
+
ENV CODEX_HOME=/home/wake/.codex-runtime
|
|
32
|
+
ENV PATH=/home/wake/.local/bin:$PATH
|
|
33
|
+
|
|
34
|
+
RUN curl https://cursor.com/install -fsS | HOME=/home/wake bash \
|
|
35
|
+
&& printf '#!/bin/bash\n[ "$1" = "agent" ] && shift\nexec ~/.local/bin/agent "$@"\n' \
|
|
36
|
+
> /home/wake/.local/bin/cursor \
|
|
37
|
+
&& chmod +x /home/wake/.local/bin/cursor \
|
|
38
|
+
&& chown -R wake:wake /home/wake/.local
|
|
39
|
+
|
|
40
|
+
WORKDIR /app
|
|
41
|
+
COPY package*.json ./
|
|
42
|
+
RUN --mount=type=cache,target=/root/.npm \
|
|
43
|
+
if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
|
44
|
+
|
|
45
|
+
COPY . .
|
|
46
|
+
ARG WAKE_BUILD_TAG
|
|
47
|
+
RUN WAKE_BUILD_TAG="$WAKE_BUILD_TAG" npm run build && chmod +x docker/entrypoint.sh
|
|
48
|
+
|
|
49
|
+
USER wake
|
|
50
|
+
WORKDIR /home/wake
|
|
51
|
+
|
|
52
|
+
EXPOSE 4317
|
|
53
|
+
|
|
54
|
+
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
mkdir -p /wake/logs
|
|
5
|
+
|
|
6
|
+
write_ngrok_public_url() {
|
|
7
|
+
local output_file="/wake/control-plane-ui-url"
|
|
8
|
+
|
|
9
|
+
rm -f "${output_file}"
|
|
10
|
+
for _ in $(seq 1 30); do
|
|
11
|
+
local public_url
|
|
12
|
+
public_url="$(
|
|
13
|
+
node -e '
|
|
14
|
+
const http = require("node:http");
|
|
15
|
+
const req = http.get("http://127.0.0.1:4040/api/tunnels", (res) => {
|
|
16
|
+
let body = "";
|
|
17
|
+
res.setEncoding("utf8");
|
|
18
|
+
res.on("data", (chunk) => { body += chunk; });
|
|
19
|
+
res.on("end", () => {
|
|
20
|
+
try {
|
|
21
|
+
const tunnels = JSON.parse(body).tunnels ?? [];
|
|
22
|
+
const tunnel = tunnels.find((entry) => typeof entry.public_url === "string" && entry.public_url.startsWith("https://"))
|
|
23
|
+
?? tunnels.find((entry) => typeof entry.public_url === "string");
|
|
24
|
+
if (tunnel !== undefined) {
|
|
25
|
+
process.stdout.write(tunnel.public_url);
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on("error", () => {});
|
|
31
|
+
req.setTimeout(1000, () => req.destroy());
|
|
32
|
+
' || true
|
|
33
|
+
)"
|
|
34
|
+
|
|
35
|
+
if [ -n "${public_url}" ]; then
|
|
36
|
+
printf '%s\n' "${public_url}" > "${output_file}"
|
|
37
|
+
echo "wake ui: ngrok tunnel available at ${public_url}"
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
sleep 1
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
echo "wake ui: ngrok tunnel started but public URL was not discovered; see /wake/logs/ngrok.log"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if [ "${WAKE_UI_ENABLED:-false}" = "true" ]; then
|
|
48
|
+
echo "wake ui: starting on 0.0.0.0:${WAKE_UI_PORT:-4317}"
|
|
49
|
+
node /app/dist/src/main.js ui \
|
|
50
|
+
--wake-root /wake \
|
|
51
|
+
--host 0.0.0.0 \
|
|
52
|
+
--port "${WAKE_UI_PORT:-4317}" \
|
|
53
|
+
${WAKE_UI_TOKEN:+--token "${WAKE_UI_TOKEN}"} \
|
|
54
|
+
>> /wake/logs/ui.log 2>&1 &
|
|
55
|
+
|
|
56
|
+
if [ "${WAKE_UI_TUNNEL_ENABLED:-false}" = "true" ]; then
|
|
57
|
+
if [ -n "${NGROK_AUTHTOKEN:-}" ]; then
|
|
58
|
+
ngrok config add-authtoken "${NGROK_AUTHTOKEN}" >> /wake/logs/ngrok.log 2>&1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo "wake ui: starting ngrok tunnel for 127.0.0.1:${WAKE_UI_PORT:-4317}"
|
|
62
|
+
ngrok http "127.0.0.1:${WAKE_UI_PORT:-4317}" --log=stdout >> /wake/logs/ngrok.log 2>&1 &
|
|
63
|
+
write_ngrok_public_url &
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [ "${WAKE_START_ENABLED:-false}" = "true" ]; then
|
|
68
|
+
echo "wake start: starting resident loop"
|
|
69
|
+
node /app/dist/src/main.js start \
|
|
70
|
+
--wake-root /wake \
|
|
71
|
+
>> /wake/logs/start.log 2>&1 &
|
|
72
|
+
echo "$!" > /wake/logs/start.pid
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
exec sleep infinity
|