codewake 0.0.1 → 0.0.2
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 +10 -5
- package/lib/actions.js +165 -0
- package/lib/agents.js +21 -9
- package/lib/browser.js +347 -0
- package/lib/cli.js +98 -49
- package/lib/jobs.js +57 -6
- package/lib/registry.js +23 -0
- package/lib/sessions.js +104 -0
- package/lib/tui.js +19 -4
- package/lib/wizard.js +45 -135
- package/package.json +2 -2
package/lib/wizard.js
CHANGED
|
@@ -1,43 +1,46 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { buildCommand, commandPreview, configPath, detectAgents, loadAgents } from './agents.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
findPendingForSession,
|
|
8
|
-
listJobs,
|
|
9
|
-
scheduleJob,
|
|
10
|
-
} from './jobs.js';
|
|
11
|
-
import { listSessions } from './sessions.js';
|
|
12
|
-
import { describeRunAt, parseClockTime, parseDuration, timeAgo } from './time.js';
|
|
3
|
+
import { askWhen, browseJobs, renderJobsTable } from './browser.js';
|
|
4
|
+
import { findPendingForSession, listJobs, scheduleJob } from './jobs.js';
|
|
5
|
+
import { listSessions, resolveSessionPlacement } from './sessions.js';
|
|
6
|
+
import { describeRunAt, timeAgo } from './time.js';
|
|
13
7
|
import { color, confirm, input, select } from './tui.js';
|
|
14
8
|
|
|
9
|
+
// Re-exported so existing importers (tests, cli) keep one entry point.
|
|
10
|
+
export { renderJobsTable, shortenPath } from './browser.js';
|
|
11
|
+
|
|
15
12
|
const DEFAULT_RESUME_PROMPT = 'continue';
|
|
13
|
+
const EXIT_WINDOW_MS = 5000;
|
|
16
14
|
|
|
17
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Interactive console. `io` lets tests inject stdin/stdout. There is no
|
|
17
|
+
* Exit item: pressing a cancel key (ctrl+c, ctrl+d, esc, q) at the menu
|
|
18
|
+
* twice within 5 seconds leaves — the first press explains that.
|
|
19
|
+
*/
|
|
18
20
|
export async function runWizard({ version = '', io = {}, env = process.env } = {}) {
|
|
19
21
|
const out = io.stdout ?? process.stdout;
|
|
20
22
|
out.write(`\n${color.bold('codewake')}${version ? color.dim(` v${version}`) : ''} ${color.dim('— schedule your coding agents to pick up where they left off')}\n\n`);
|
|
23
|
+
let lastCancel = { key: null, at: 0 };
|
|
21
24
|
for (;;) {
|
|
22
25
|
const action = await select('What do you want to do?', [
|
|
23
|
-
{ label: 'Schedule
|
|
24
|
-
{ label: '
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
{ label: 'Schedule / wake an agent', value: 'schedule', hint: 'pick agent → session → prompt → time' },
|
|
27
|
+
{ label: 'Waked agents', value: 'browse', hint: 'browse jobs: open, retry, cancel, sweep' },
|
|
28
|
+
], { ...io, cancelInfo: true });
|
|
29
|
+
if (action?.cancelled) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (lastCancel.key === action.key && now - lastCancel.at <= EXIT_WINDOW_MS) {
|
|
32
|
+
out.write(color.dim('Bye!\n'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
lastCancel = { key: action.key, at: now };
|
|
36
|
+
out.write(color.dim(`Press ${action.key} again within 5s to exit.\n`));
|
|
37
|
+
continue;
|
|
32
38
|
}
|
|
39
|
+
lastCancel = { key: null, at: 0 };
|
|
33
40
|
if (action === 'schedule') await scheduleFlow({ io, env, out });
|
|
34
|
-
else if (action === '
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const removed = clearJobs(env);
|
|
38
|
-
out.write(removed.length
|
|
39
|
-
? `${color.green('✔')} Removed ${removed.length} finished job${removed.length === 1 ? '' : 's'}.\n\n`
|
|
40
|
-
: color.dim('Nothing to clean up.\n\n'));
|
|
41
|
+
else if (action === 'browse' && await browseJobs({ io, env }) === 'exit') {
|
|
42
|
+
out.write(color.dim('Bye!\n'));
|
|
43
|
+
return;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -91,6 +94,21 @@ async function scheduleFlow({ io, env, out }) {
|
|
|
91
94
|
out.write(color.dim(` ${agent.name} only supports starting new sessions from here.\n`));
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
// Agents like Claude Code can only resume a session from the project it
|
|
98
|
+
// belongs to; correct the working directory now instead of failing later.
|
|
99
|
+
let project = process.cwd();
|
|
100
|
+
if (mode === 'resume' && sessionId) {
|
|
101
|
+
let placement;
|
|
102
|
+
try {
|
|
103
|
+
placement = resolveSessionPlacement(agent, sessionId, { project, home: env.HOME || homedir() });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
out.write(`${color.red('✘')} ${error.message}\n\n`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (placement.note) out.write(`${color.yellow('!')} ${placement.note}\n`);
|
|
109
|
+
project = placement.project;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
// Two jobs resuming the same session would race each other — refuse early.
|
|
95
113
|
const duplicate = findPendingForSession(agent.id, sessionId, env);
|
|
96
114
|
if (duplicate) {
|
|
@@ -119,7 +137,6 @@ async function scheduleFlow({ io, env, out }) {
|
|
|
119
137
|
if (runAt === null) return;
|
|
120
138
|
|
|
121
139
|
const args = buildCommand(agent, { mode, sessionId, prompt, skipPermissions });
|
|
122
|
-
const project = process.cwd();
|
|
123
140
|
|
|
124
141
|
out.write(`\n${color.bold('Summary')}\n`);
|
|
125
142
|
out.write(` ${color.dim('Agent')} ${agent.name}\n`);
|
|
@@ -146,41 +163,6 @@ async function scheduleFlow({ io, env, out }) {
|
|
|
146
163
|
out.write(color.dim(` Manage it with "codewake jobs", "codewake logs ${job.id}" or "codewake cancel ${job.id}".\n\n`));
|
|
147
164
|
}
|
|
148
165
|
|
|
149
|
-
async function askWhen(io) {
|
|
150
|
-
const kind = await select('When should it launch?', [
|
|
151
|
-
{ label: 'Right now', value: 'now' },
|
|
152
|
-
{ label: 'After a delay', value: 'in', hint: 'e.g. 2h 30m' },
|
|
153
|
-
{ label: 'At a specific time', value: 'at', hint: 'e.g. 14:30' },
|
|
154
|
-
], io);
|
|
155
|
-
if (!kind) return null;
|
|
156
|
-
if (kind === 'now') return new Date();
|
|
157
|
-
if (kind === 'in') {
|
|
158
|
-
const text = await input('Delay', {
|
|
159
|
-
required: true,
|
|
160
|
-
validate: (value) => tryError(() => parseDuration(value)),
|
|
161
|
-
...io,
|
|
162
|
-
});
|
|
163
|
-
if (text === null) return null;
|
|
164
|
-
return new Date(Date.now() + parseDuration(text) * 1000);
|
|
165
|
-
}
|
|
166
|
-
const text = await input('Time (24h)', {
|
|
167
|
-
required: true,
|
|
168
|
-
validate: (value) => tryError(() => parseClockTime(value)),
|
|
169
|
-
...io,
|
|
170
|
-
});
|
|
171
|
-
if (text === null) return null;
|
|
172
|
-
return parseClockTime(text);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function tryError(fn) {
|
|
176
|
-
try {
|
|
177
|
-
fn();
|
|
178
|
-
return null;
|
|
179
|
-
} catch (error) {
|
|
180
|
-
return error.message;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
166
|
export function viewJobs(out, env = process.env) {
|
|
185
167
|
const jobs = listJobs(env);
|
|
186
168
|
if (!jobs.length) {
|
|
@@ -190,75 +172,3 @@ export function viewJobs(out, env = process.env) {
|
|
|
190
172
|
out.write(`${renderJobsTable(jobs, Date.now(), { width: out.columns })}\n\n`);
|
|
191
173
|
}
|
|
192
174
|
|
|
193
|
-
/** Shorten a path for display: home becomes ~, long paths keep their tail. */
|
|
194
|
-
export function shortenPath(path, max, home = homedir()) {
|
|
195
|
-
let short = path;
|
|
196
|
-
if (home && short.startsWith(home)) short = `~${short.slice(home.length)}`;
|
|
197
|
-
if (short.length > max) short = `…${short.slice(short.length - max + 1)}`;
|
|
198
|
-
return short;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const truncate = (text, max) =>
|
|
202
|
-
(text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text);
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Fixed-width jobs table that always fits `width` columns: the PROJECT
|
|
206
|
-
* column absorbs the remaining space (shortened from the left), and is
|
|
207
|
-
* dropped entirely when the terminal is too narrow for it.
|
|
208
|
-
*/
|
|
209
|
-
export function renderJobsTable(jobs, now = Date.now(), { width = process.stdout.columns || 120 } = {}) {
|
|
210
|
-
const statusColor = { pending: color.cyan, running: color.yellow, done: color.green, failed: color.red, cancelled: color.dim, overdue: color.red };
|
|
211
|
-
const rows = jobs.map((job) => [
|
|
212
|
-
job.id,
|
|
213
|
-
truncate(job.agentName, 20),
|
|
214
|
-
displayStatus(job, now),
|
|
215
|
-
truncate(job.status === 'pending' ? describeRunAt(job.runAt, new Date(now)) : new Date(job.runAt).toLocaleString(), 28),
|
|
216
|
-
job.mode === 'resume' ? truncate(job.sessionId ?? '', 12) : 'new session',
|
|
217
|
-
job.project,
|
|
218
|
-
]);
|
|
219
|
-
let header = ['ID', 'AGENT', 'STATUS', 'LAUNCH', 'SESSION', 'PROJECT'];
|
|
220
|
-
const columnWidth = (column) =>
|
|
221
|
-
Math.max(header[column].length, ...rows.map((row) => String(row[column]).length));
|
|
222
|
-
const gap = 2;
|
|
223
|
-
const fixedWidth = header.slice(0, -1).reduce((sum, _title, column) => sum + columnWidth(column) + gap, 0);
|
|
224
|
-
const projectSpace = width - fixedWidth;
|
|
225
|
-
if (projectSpace >= 'PROJECT'.length) {
|
|
226
|
-
for (const row of rows) row[5] = shortenPath(row[5], projectSpace);
|
|
227
|
-
} else {
|
|
228
|
-
header = header.slice(0, 5);
|
|
229
|
-
for (const row of rows) row.length = 5;
|
|
230
|
-
}
|
|
231
|
-
const widths = header.map((_title, column) => columnWidth(column));
|
|
232
|
-
const line = (cells, decorate) => cells
|
|
233
|
-
.map((cell, column) => {
|
|
234
|
-
const text = String(cell).padEnd(widths[column]);
|
|
235
|
-
return decorate && column === 2 ? (statusColor[cell] ?? color.dim)(text) : text;
|
|
236
|
-
})
|
|
237
|
-
.join(' '.repeat(gap))
|
|
238
|
-
.trimEnd();
|
|
239
|
-
return [color.dim(line(header)), ...rows.map((row) => line(row, true))].join('\n');
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function cancelFlow({ io, env, out }) {
|
|
243
|
-
const pending = listJobs(env).filter((job) => job.status === 'pending');
|
|
244
|
-
if (!pending.length) {
|
|
245
|
-
out.write(color.dim('No pending jobs to cancel.\n\n'));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const jobId = await select('Which job do you want to cancel?', pending.map((job) => ({
|
|
249
|
-
label: `${job.id} · ${job.agentName}`,
|
|
250
|
-
value: job.id,
|
|
251
|
-
hint: describeRunAt(job.runAt),
|
|
252
|
-
})), io);
|
|
253
|
-
if (!jobId) return;
|
|
254
|
-
const sure = await confirm(`Cancel ${jobId}?`, { def: true, ...io });
|
|
255
|
-
if (!sure) return;
|
|
256
|
-
try {
|
|
257
|
-
const { warning } = cancelJob(jobId, env);
|
|
258
|
-
out.write(`${color.green('✔')} Cancelled ${jobId}.\n`);
|
|
259
|
-
if (warning) out.write(`${color.yellow('!')} ${warning}\n`);
|
|
260
|
-
out.write('\n');
|
|
261
|
-
} catch (error) {
|
|
262
|
-
out.write(`${color.red('✘')} ${error.message}\n\n`);
|
|
263
|
-
}
|
|
264
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codewake",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Interactive CLI to schedule your coding agents (Claude Code, Codex, Copilot, Gemini, Cursor, Aider, ...) to wake up and pick up where they left off.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"test": "node --test",
|
|
49
49
|
"update:version": "npm version patch --no-git-tag-version",
|
|
50
50
|
"build": "node -e \"\"",
|
|
51
|
-
"publish:npm": "pnpm run test && pnpm run
|
|
51
|
+
"publish:npm": "pnpm run test && pnpm run build && pnpm publish --access public",
|
|
52
52
|
"git:tag": "git commit -am \"v$npm_package_version\" && git push origin main && git tag v$npm_package_version && git push origin v$npm_package_version"
|
|
53
53
|
}
|
|
54
54
|
}
|