claude-do-it 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/do-it.js ADDED
@@ -0,0 +1,1208 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/cli/src/main.ts
5
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
6
+
7
+ // packages/client/src/paths.ts
8
+ import { Config, Effect, Option, Path } from "effect";
9
+ var defaultHome = Effect.gen(function* () {
10
+ const path = yield* Path.Path;
11
+ const configured = yield* Config.option(Config.String("DO_IT_HOME"));
12
+ if (Option.isSome(configured))
13
+ return configured.value;
14
+ return path.join(yield* Config.String("HOME"), ".do-it");
15
+ }).pipe(Effect.orDie);
16
+ function paths(home) {
17
+ return Effect.gen(function* () {
18
+ const path = yield* Path.Path;
19
+ const root = path.resolve(home ?? (yield* defaultHome));
20
+ const lock = path.join(root, "daemon.lock");
21
+ return {
22
+ home: root,
23
+ daemon: path.join(root, "daemon.json"),
24
+ token: path.join(root, "token"),
25
+ lock,
26
+ lockPid: path.join(lock, "pid"),
27
+ log: path.join(root, "daemon.log"),
28
+ versions: path.join(root, "versions.json")
29
+ };
30
+ });
31
+ }
32
+ // packages/client/src/info.ts
33
+ import { Data, Effect as Effect2, FileSystem } from "effect";
34
+ class DaemonUnavailable extends Data.TaggedError("DaemonUnavailable") {
35
+ }
36
+ function readDaemonInfo(home) {
37
+ return Effect2.gen(function* () {
38
+ const fs = yield* FileSystem.FileSystem;
39
+ const p = yield* paths(home);
40
+ if ((yield* fs.stat(p.daemon)).size > 4096n || (yield* fs.stat(p.token)).size > 128n)
41
+ return null;
42
+ const text = yield* fs.readFileString(p.daemon);
43
+ const token = (yield* fs.readFileString(p.token)).trim();
44
+ const record = yield* Effect2.try(() => JSON.parse(text));
45
+ if (!record || !Number.isSafeInteger(record.pid) || record.pid <= 0 || !Number.isInteger(record.port) || record.port < 1 || record.port > 65535 || typeof record.appVersion !== "string" || !Number.isFinite(record.startedAt) || !/^[a-f0-9]{64}$/.test(token))
46
+ return null;
47
+ const port = record.port;
48
+ return {
49
+ home: p.home,
50
+ pid: record.pid,
51
+ port,
52
+ appVersion: record.appVersion,
53
+ startedAt: record.startedAt,
54
+ token,
55
+ baseUrl: `http://127.0.0.1:${port}`
56
+ };
57
+ }).pipe(Effect2.catch(() => Effect2.succeed(null)));
58
+ }
59
+ function alive(pid) {
60
+ try {
61
+ process.kill(pid, 0);
62
+ return true;
63
+ } catch (error) {
64
+ return error.code === "EPERM";
65
+ }
66
+ }
67
+ var probeHealth = (info) => Effect2.tryPromise({
68
+ try: async (signal) => {
69
+ const response = await fetch(`${info.baseUrl}/health`, { signal: AbortSignal.any([signal, AbortSignal.timeout(500)]) });
70
+ if (!response.ok)
71
+ return null;
72
+ const body = await response.json();
73
+ return body.ok === true && body.pid === info.pid && typeof body.appVersion === "string" ? body : null;
74
+ },
75
+ catch: () => new DaemonUnavailable({ message: "The daemon did not answer its health check" })
76
+ }).pipe(Effect2.catch(() => Effect2.succeed(null)));
77
+ // packages/protocol/src/schema.ts
78
+ import { Schema } from "effect";
79
+ var MAX_TEXT_LENGTH = 2 * 1024 * 1024;
80
+ var MAX_TASK_TEXT_LENGTH = 4096;
81
+ var Text = Schema.String.pipe(Schema.check(Schema.isMaxLength(MAX_TEXT_LENGTH)));
82
+ var TaskText = Schema.String.pipe(Schema.check(Schema.isMaxLength(MAX_TASK_TEXT_LENGTH)));
83
+ var Path3 = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => value.length > 0 && value.startsWith("/") ? undefined : "an absolute, non-empty path is required"), Schema.isMaxLength(MAX_TEXT_LENGTH)));
84
+ var Version = Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0)));
85
+ var User = Schema.TaggedStruct("User", {});
86
+ var Agent = Schema.TaggedStruct("Agent", {
87
+ name: Schema.String.pipe(Schema.check(Schema.isBetweenLength(1, 64), Schema.isPattern(/^[A-Za-z0-9._-]+$/)))
88
+ });
89
+ var File = Schema.TaggedStruct("File", {});
90
+ var Author = Schema.Union([User, Agent, File]);
91
+ var TaskState = Schema.Literals(["open", "done", "cancelled"]);
92
+ var Todo = Schema.Struct({
93
+ number: Schema.Number,
94
+ line: Schema.Number,
95
+ state: TaskState,
96
+ text: TaskText,
97
+ indent: Schema.Number,
98
+ section: Schema.NullOr(Text)
99
+ });
100
+ var TodoChange = Schema.Struct({
101
+ kind: Schema.Literals([
102
+ "added",
103
+ "removed",
104
+ "checked",
105
+ "unchecked",
106
+ "cancelled",
107
+ "uncancelled",
108
+ "edited"
109
+ ]),
110
+ number: Schema.Number,
111
+ text: TaskText,
112
+ line: Schema.Number,
113
+ oldLine: Schema.Number
114
+ });
115
+ var Section = Schema.Struct({
116
+ id: Text,
117
+ title: Text,
118
+ level: Schema.Literals([1, 2, 3]),
119
+ line: Schema.Number
120
+ });
121
+ var Others = Schema.Struct({
122
+ since: Version,
123
+ diff: Schema.NullOr(Text)
124
+ });
125
+ var Replace = Schema.TaggedStruct("Replace", {
126
+ old: Text,
127
+ new: Text
128
+ });
129
+ var Append = Schema.TaggedStruct("Append", {
130
+ text: Text
131
+ });
132
+ var TodoAdd = Schema.TaggedStruct("TodoAdd", {
133
+ text: TaskText
134
+ });
135
+ var TodoSet = Schema.TaggedStruct("TodoSet", {
136
+ target: Schema.Union([Schema.Number, Text]),
137
+ state: TaskState
138
+ });
139
+ var TodoStart = Schema.TaggedStruct("TodoStart", {
140
+ target: Schema.Union([Schema.Number, Text])
141
+ });
142
+ var EditOp = Schema.Union([Replace, Append, TodoAdd, TodoSet, TodoStart]);
143
+ var SectionId = Schema.optional(Text);
144
+ var StatusPayload = Schema.Struct({});
145
+ var StatusDocument = Schema.Struct({
146
+ path: Path3,
147
+ version: Version,
148
+ watchers: Schema.Number
149
+ });
150
+ var StatusSuccess = Schema.Struct({
151
+ pid: Schema.Number,
152
+ appVersion: Text,
153
+ port: Schema.Number,
154
+ startedAt: Schema.Number,
155
+ documents: Schema.Array(StatusDocument)
156
+ });
157
+ var StopPayload = Schema.Struct({});
158
+ var StopSuccess = Schema.Struct({});
159
+ var OpenPayload = Schema.Struct({
160
+ path: Path3,
161
+ create: Schema.Boolean
162
+ });
163
+ var OpenSuccess = Schema.Struct({
164
+ path: Path3,
165
+ version: Version,
166
+ url: Text,
167
+ created: Schema.Boolean
168
+ });
169
+ var ReadPayload = Schema.Struct({
170
+ path: Path3,
171
+ section: SectionId
172
+ });
173
+ var ReadSuccess = Schema.Struct({
174
+ version: Version,
175
+ text: Text
176
+ });
177
+ var SectionsPayload = Schema.Struct({ path: Path3 });
178
+ var SectionsSuccess = Schema.Struct({
179
+ version: Version,
180
+ sections: Schema.Array(Section)
181
+ });
182
+ var TodosPayload = Schema.Struct({
183
+ path: Path3,
184
+ section: SectionId
185
+ });
186
+ var TodosSuccess = Schema.Struct({
187
+ version: Version,
188
+ todos: Schema.Array(Todo)
189
+ });
190
+ var EditPayload = Schema.Struct({
191
+ path: Path3,
192
+ author: Author,
193
+ version: Version,
194
+ section: SectionId,
195
+ op: EditOp
196
+ });
197
+ var EditSuccess = Schema.Struct({
198
+ version: Version,
199
+ changed: Schema.Boolean,
200
+ section: Schema.NullOr(Text),
201
+ others: Schema.NullOr(Others)
202
+ });
203
+ var SyncPayload = Schema.Struct({
204
+ path: Path3,
205
+ base: Version,
206
+ text: Text
207
+ });
208
+ var SyncSuccess = Schema.Struct({
209
+ version: Version,
210
+ text: Text
211
+ });
212
+ var DiffPayload = Schema.Struct({
213
+ path: Path3,
214
+ since: Version,
215
+ wait: Schema.Number.pipe(Schema.check(Schema.isBetween({ minimum: 0, maximum: 600 })))
216
+ });
217
+ var DiffSuccess = Schema.Struct({
218
+ from: Version,
219
+ to: Version,
220
+ by: Schema.Array(Author),
221
+ diff: Schema.NullOr(Text),
222
+ todos: Schema.Array(TodoChange)
223
+ });
224
+ var WatchPayload = Schema.Struct({
225
+ path: Path3,
226
+ since: Schema.optional(Version),
227
+ section: SectionId,
228
+ text: Schema.Boolean
229
+ });
230
+ var Watching = Schema.TaggedStruct("Watching", {
231
+ version: Version,
232
+ text: Schema.optionalKey(Text)
233
+ });
234
+ var Change = Schema.TaggedStruct("Change", {
235
+ from: Version,
236
+ to: Version,
237
+ by: Schema.Array(Author),
238
+ diff: Schema.NullOr(Text),
239
+ todos: Schema.Array(TodoChange),
240
+ text: Schema.optionalKey(Text)
241
+ });
242
+ var Resync = Schema.TaggedStruct("Resync", {
243
+ version: Version,
244
+ text: Schema.optionalKey(Text)
245
+ });
246
+ var Deleted = Schema.TaggedStruct("Deleted", {});
247
+ var WatchEvent = Schema.Union([Watching, Change, Resync, Deleted]);
248
+ // packages/protocol/src/errors.ts
249
+ import { Schema as Schema2 } from "effect";
250
+ class VersionMismatch extends Schema2.TaggedError()("VersionMismatch", {
251
+ latest: Version,
252
+ since: Version,
253
+ diff: Schema2.NullOr(Text),
254
+ forgotten: Schema2.Boolean
255
+ }) {
256
+ }
257
+ var RefusedCode = Schema2.Literals(["not-found", "ambiguous", "too-large", "invalid"]);
258
+
259
+ class Refused extends Schema2.TaggedError()("Refused", {
260
+ code: RefusedCode,
261
+ message: Text
262
+ }) {
263
+ }
264
+
265
+ class Forgotten extends Schema2.TaggedError()("Forgotten", {
266
+ latest: Version
267
+ }) {
268
+ }
269
+
270
+ class NothingYet extends Schema2.TaggedError()("NothingYet", {}) {
271
+ }
272
+
273
+ class DocumentGone extends Schema2.TaggedError()("DocumentGone", {
274
+ path: Path3
275
+ }) {
276
+ }
277
+
278
+ class Unauthorized extends Schema2.TaggedError()("Unauthorized", {}) {
279
+ }
280
+ var RpcFailure = Schema2.Union([
281
+ VersionMismatch,
282
+ Refused,
283
+ Forgotten,
284
+ NothingYet,
285
+ DocumentGone,
286
+ Unauthorized
287
+ ]);
288
+ // packages/protocol/src/rpcs.ts
289
+ import { Rpc, RpcGroup } from "effect/rpc";
290
+ var statusRpc = Rpc.make("status", {
291
+ payload: StatusPayload,
292
+ success: StatusSuccess,
293
+ error: RpcFailure
294
+ });
295
+ var stopRpc = Rpc.make("stop", {
296
+ payload: StopPayload,
297
+ success: StopSuccess,
298
+ error: RpcFailure
299
+ });
300
+ var openRpc = Rpc.make("open", {
301
+ payload: OpenPayload,
302
+ success: OpenSuccess,
303
+ error: RpcFailure
304
+ });
305
+ var readRpc = Rpc.make("read", {
306
+ payload: ReadPayload,
307
+ success: ReadSuccess,
308
+ error: RpcFailure
309
+ });
310
+ var sectionsRpc = Rpc.make("sections", {
311
+ payload: SectionsPayload,
312
+ success: SectionsSuccess,
313
+ error: RpcFailure
314
+ });
315
+ var todosRpc = Rpc.make("todos", {
316
+ payload: TodosPayload,
317
+ success: TodosSuccess,
318
+ error: RpcFailure
319
+ });
320
+ var editRpc = Rpc.make("edit", {
321
+ payload: EditPayload,
322
+ success: EditSuccess,
323
+ error: RpcFailure
324
+ });
325
+ var syncRpc = Rpc.make("sync", {
326
+ payload: SyncPayload,
327
+ success: SyncSuccess,
328
+ error: RpcFailure
329
+ });
330
+ var diffRpc = Rpc.make("diff", {
331
+ payload: DiffPayload,
332
+ success: DiffSuccess,
333
+ error: RpcFailure
334
+ });
335
+ var watchRpc = Rpc.make("watch", {
336
+ payload: WatchPayload,
337
+ success: WatchEvent,
338
+ error: RpcFailure,
339
+ stream: true
340
+ });
341
+ var DoItRpcs = RpcGroup.make(statusRpc, stopRpc, openRpc, readRpc, sectionsRpc, todosRpc, editRpc, syncRpc, diffRpc, watchRpc);
342
+ // packages/protocol/src/format.ts
343
+ var describeAuthor = (author) => {
344
+ switch (author._tag) {
345
+ case "User":
346
+ return "the user";
347
+ case "Agent":
348
+ return author.name;
349
+ case "File":
350
+ return "file";
351
+ }
352
+ };
353
+ var formatTodoChange = (change) => `${change.kind} ${change.number}. ${change.text}`;
354
+ var formatVersionHeader = (from, to, by) => {
355
+ const version = from === to || to === from + 1 ? `Version ${to}` : `Versions ${from + 1}-${to}`;
356
+ return by.length === 0 ? version : `${version} by ${by.map(describeAuthor).join(", ")}`;
357
+ };
358
+ var exitCodeFor = (error) => {
359
+ switch (error._tag) {
360
+ case "Refused":
361
+ return error.code === "invalid" ? 2 : 1;
362
+ case "NothingYet":
363
+ return 4;
364
+ case "DocumentGone":
365
+ return 9;
366
+ case "Unauthorized":
367
+ return 3;
368
+ case "VersionMismatch":
369
+ case "Forgotten":
370
+ return 1;
371
+ }
372
+ };
373
+ // packages/protocol/src/index.ts
374
+ var APP_VERSION = "0.1.1";
375
+
376
+ // packages/client/src/lifecycle.ts
377
+ import { Effect as Effect4, FileSystem as FileSystem2, Option as Option2, Path as Path4, Semaphore } from "effect";
378
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
379
+
380
+ // packages/client/src/rpc.ts
381
+ import { Context, Effect as Effect3, Layer } from "effect";
382
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
383
+ import { RpcClient as Rpc2, RpcSerialization } from "effect/rpc";
384
+
385
+ class RpcClient extends Context.Service()("@do-it/client/RpcClient") {
386
+ }
387
+ function RpcClientLive(info) {
388
+ const http = Layer.effect(HttpClient.HttpClient, Effect3.map(HttpClient.HttpClient, HttpClient.filterStatusOk)).pipe(Layer.provide(FetchHttpClient.layer));
389
+ const protocol = Rpc2.layerProtocolHttp({
390
+ url: `${info.baseUrl}/rpc`,
391
+ transformClient: (client) => HttpClient.mapRequest(client, (request) => request.pipe(HttpClientRequest.setUrl(`${info.baseUrl}/rpc`), HttpClientRequest.setHeader("Authorization", `Bearer ${info.token}`)))
392
+ }).pipe(Layer.provide([http, RpcSerialization.layerNdjson]));
393
+ return Layer.effect(RpcClient, Rpc2.make(DoItRpcs)).pipe(Layer.provide(protocol));
394
+ }
395
+ function withClient(info, f) {
396
+ return Effect3.flatMap(RpcClient, f).pipe(Effect3.provide(RpcClientLive(info)));
397
+ }
398
+
399
+ // packages/client/src/lifecycle.ts
400
+ var unavailable = (cause) => cause instanceof DaemonUnavailable ? cause : new DaemonUnavailable({ message: cause instanceof Error ? cause.message : String(cause) });
401
+ var attempt = (f) => Effect4.try({ try: f, catch: unavailable });
402
+ function unless(reason, fallback) {
403
+ return (effect) => Effect4.catch(effect, (error) => error.reason._tag === reason ? Effect4.succeed(fallback) : Effect4.fail(error));
404
+ }
405
+ var lockTurn = Semaphore.makeUnsafe(1);
406
+ var daemonEntry = new URL("./daemon.js", import.meta.url);
407
+ var daemonScript = 'exec "$0" "$1" >>"$2" 2>&1';
408
+ var stopDaemon = Effect4.fn("stopDaemon")(function* (home) {
409
+ const info = yield* readDaemonInfo(home);
410
+ if (!info || !alive(info.pid))
411
+ return false;
412
+ const stopped = yield* withClient(info, (client) => client.stop({})).pipe(Effect4.timeout("500 millis"), Effect4.map(() => true), Effect4.catch(() => Effect4.succeed(false)));
413
+ if (!stopped) {
414
+ const signalled = yield* attempt(() => {
415
+ if (info.pid !== process.pid && alive(info.pid))
416
+ process.kill(info.pid, "SIGTERM");
417
+ }).pipe(Effect4.result);
418
+ if (signalled._tag === "Failure")
419
+ return false;
420
+ }
421
+ const deadline = Date.now() + 5000;
422
+ while (Date.now() < deadline) {
423
+ if (!(yield* probeHealth(info)))
424
+ return true;
425
+ yield* Effect4.sleep("50 millis");
426
+ }
427
+ return false;
428
+ });
429
+ var ensureDaemon = Effect4.fn("ensureDaemon")(function* (options = {}) {
430
+ const fs = yield* FileSystem2.FileSystem;
431
+ const path = yield* Path4.Path;
432
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
433
+ const p = yield* paths(options.home ?? options.env?.DO_IT_HOME);
434
+ const compatible = Effect4.gen(function* () {
435
+ const info = yield* readDaemonInfo(p.home);
436
+ if (!info || !alive(info.pid))
437
+ return null;
438
+ const health = yield* probeHealth(info);
439
+ return health && health.appVersion === APP_VERSION && info.appVersion === APP_VERSION ? info : null;
440
+ });
441
+ const existing = yield* compatible;
442
+ if (existing)
443
+ return existing;
444
+ if (options.spawn === false)
445
+ return yield* Effect4.fail(new DaemonUnavailable({ message: "No compatible daemon is running" }));
446
+ yield* fs.makeDirectory(p.home, { recursive: true, mode: 448 }).pipe(Effect4.andThen(fs.chmod(p.home, 448)), Effect4.mapError(unavailable));
447
+ const lockOnce = Effect4.gen(function* () {
448
+ const created = yield* fs.makeDirectory(p.lock, { mode: 448 }).pipe(Effect4.as(true), unless("AlreadyExists", false));
449
+ if (created) {
450
+ yield* fs.writeFileString(p.lockPid, String(process.pid), { mode: 384 });
451
+ return Option2.getOrNull((yield* fs.stat(p.lock)).ino);
452
+ }
453
+ const stat = yield* fs.stat(p.lock).pipe(unless("NotFound", undefined));
454
+ if (!stat)
455
+ return;
456
+ const text = yield* fs.readFileString(p.lockPid).pipe(unless("NotFound", undefined));
457
+ const pid = text === undefined ? undefined : Number(text);
458
+ const modified = Option2.match(stat.mtime, { onNone: () => 0, onSome: (date) => date.getTime() });
459
+ const abandoned = pid !== undefined && Number.isSafeInteger(pid) && pid > 0 ? !alive(pid) : Date.now() - modified > 1e4;
460
+ if (abandoned)
461
+ yield* fs.remove(p.lock, { recursive: true, force: true });
462
+ return;
463
+ }).pipe(lockTurn.withPermit, Effect4.mapError(unavailable));
464
+ let inode;
465
+ const lockDeadline = Date.now() + 12000;
466
+ while (inode === undefined) {
467
+ inode = yield* lockOnce;
468
+ if (inode !== undefined)
469
+ break;
470
+ if (Date.now() > lockDeadline)
471
+ return yield* Effect4.fail(new DaemonUnavailable({ message: "Timed out waiting for the daemon startup lock" }));
472
+ yield* Effect4.sleep("50 millis");
473
+ }
474
+ const held = inode;
475
+ const release = fs.stat(p.lock).pipe(Effect4.flatMap((stat) => Option2.getOrNull(stat.ino) === held ? fs.remove(p.lock, { recursive: true, force: true }) : Effect4.void), lockTurn.withPermit, Effect4.ignore);
476
+ const start = Effect4.gen(function* () {
477
+ const found = yield* compatible;
478
+ if (found)
479
+ return found;
480
+ const old = yield* readDaemonInfo(p.home);
481
+ if (old && alive(old.pid) && !(yield* stopDaemon(p.home))) {
482
+ return yield* Effect4.fail(new DaemonUnavailable({ message: "The previous daemon could not be stopped" }));
483
+ }
484
+ const size = yield* fs.stat(p.log).pipe(Effect4.map((stat) => stat.size), unless("NotFound", 0n), Effect4.mapError(unavailable));
485
+ if (size >= 1024n * 1024n)
486
+ yield* fs.rename(p.log, `${p.log}.old`).pipe(Effect4.mapError(unavailable));
487
+ yield* fs.writeFileString(p.log, "", { flag: "a", mode: 384 }).pipe(Effect4.mapError(unavailable));
488
+ const main = yield* path.fromFileUrl(daemonEntry).pipe(Effect4.mapError(unavailable));
489
+ const env = { ...options.env, DO_IT_HOME: p.home };
490
+ if (options.port !== undefined)
491
+ env.DO_IT_PORT = String(options.port);
492
+ const command = ChildProcess.make("/bin/sh", ["-c", daemonScript, process.execPath, main, p.log], {
493
+ detached: true,
494
+ stdin: "ignore",
495
+ stdout: "ignore",
496
+ stderr: "ignore",
497
+ env,
498
+ extendEnv: true
499
+ });
500
+ const child = yield* spawner.spawn(command).pipe(Effect4.mapError(unavailable));
501
+ yield* child.unref.pipe(Effect4.mapError(unavailable));
502
+ const wait = Effect4.gen(function* () {
503
+ const deadline = Date.now() + 5000;
504
+ while (Date.now() < deadline) {
505
+ const info = yield* compatible;
506
+ if (info)
507
+ return info;
508
+ if (!(yield* child.isRunning.pipe(Effect4.mapError(unavailable))))
509
+ break;
510
+ yield* Effect4.sleep("50 millis");
511
+ }
512
+ return yield* Effect4.fail(new DaemonUnavailable({ message: `The daemon did not start, see ${p.log}` }));
513
+ });
514
+ return yield* wait.pipe(Effect4.onError(() => child.kill({ forceKillAfter: "1 second" }).pipe(Effect4.ignore)));
515
+ });
516
+ return yield* Effect4.scoped(start).pipe(Effect4.ensuring(release));
517
+ });
518
+ // packages/cli/src/main.ts
519
+ import { Cause, Duration, Effect as Effect10, Exit, FiberHandle, Stdio as Stdio2, Stream as Stream2 } from "effect";
520
+ import { CliConfig as CliConfig2, CliOutput as CliOutput2, Command as Command2 } from "effect/cli";
521
+
522
+ // packages/cli/src/browser.ts
523
+ import { Config as Config2, Effect as Effect5, Option as Option3 } from "effect";
524
+ import { ChildProcess as ChildProcess2, ChildProcessSpawner as ChildProcessSpawner2 } from "effect/process";
525
+ var openBrowser = (url) => Effect5.gen(function* () {
526
+ const configured = Option3.getOrUndefined(yield* Config2.option(Config2.String("DO_IT_BROWSER")));
527
+ if (configured?.toLowerCase() === "true")
528
+ return;
529
+ const command = configured || (process.platform === "darwin" ? "open" : "xdg-open");
530
+ const spawner = yield* ChildProcessSpawner2.ChildProcessSpawner;
531
+ const opener = ChildProcess2.make("/bin/sh", ["-c", `${command} "$1"`, "sh", url], {
532
+ detached: true,
533
+ stdin: "ignore",
534
+ stdout: "ignore",
535
+ stderr: "ignore"
536
+ });
537
+ yield* Effect5.scoped(Effect5.flatMap(spawner.spawn(opener), (child) => child.unref));
538
+ }).pipe(Effect5.ignore);
539
+
540
+ // packages/cli/src/output.ts
541
+ import { Console } from "effect";
542
+ function writeLine(text = "") {
543
+ return Console.log(text.endsWith(`
544
+ `) ? text.slice(0, -1) : text);
545
+ }
546
+ function writeJson(value) {
547
+ return writeLine(JSON.stringify(value));
548
+ }
549
+ function diffText(header, diff) {
550
+ return diff ? `${header}
551
+ ${diff}` : header;
552
+ }
553
+ function editReply(result) {
554
+ if (!result.changed)
555
+ return { text: `Unchanged. Version: ${result.version}`, json: { kind: "unchanged", version: result.version, section: result.section, others: result.others, diff: result.others?.diff ?? null } };
556
+ if (result.section && result.others) {
557
+ const header = `Updated section "${result.section}". Version: ${result.version}. Other sections changed since ${result.others.since}:`;
558
+ return { text: diffText(header, result.others.diff), json: { kind: "updated-section", version: result.version, section: result.section, others: result.others, diff: result.others.diff } };
559
+ }
560
+ return { text: `Updated. Version: ${result.version}`, json: { kind: "updated", version: result.version, section: result.section, others: result.others, diff: result.others?.diff ?? null } };
561
+ }
562
+ function field(error, key) {
563
+ return typeof error === "object" && error !== null && key in error ? error[key] : undefined;
564
+ }
565
+ function mismatchReply(error, requested) {
566
+ const latest = typeof field(error, "latest") === "number" ? field(error, "latest") : requested;
567
+ const forgotten = field(error, "forgotten") === true || field(error, "_tag") === "Forgotten";
568
+ if (forgotten) {
569
+ const text = `Version mismatch, nothing changed. Latest version: ${latest}. Version ${requested} is no longer kept: run do-it read`;
570
+ return { text, json: { kind: "version-mismatch", latest, since: requested, diff: null, forgotten: true } };
571
+ }
572
+ const since = typeof field(error, "since") === "number" ? field(error, "since") : requested;
573
+ const diff = typeof field(error, "diff") === "string" ? field(error, "diff") : null;
574
+ const header = `Version mismatch, nothing changed. Latest version: ${latest}. Changes since ${since}:`;
575
+ return { text: diffText(header, diff), json: { kind: "version-mismatch", latest, since, diff, forgotten: false } };
576
+ }
577
+ function changeReply(change, todosOnly) {
578
+ return todosOnly ? change.todos.map(formatTodoChange).join(`
579
+ `) : diffText(formatVersionHeader(change.from, change.to, change.by), change.diff);
580
+ }
581
+ function changeJson(change) {
582
+ return { event: "change", from: change.from, to: change.to, by: change.by, diff: change.diff, todos: change.todos };
583
+ }
584
+ function todosText(changes) {
585
+ return changes.map(formatTodoChange).join(`
586
+ `);
587
+ }
588
+ function rpcErrorCode(error) {
589
+ const tag = field(error, "_tag");
590
+ return typeof tag === "string" ? tag : "RpcClientError";
591
+ }
592
+ function isRpcFailure(error) {
593
+ const tag = field(error, "_tag");
594
+ return typeof tag === "string" && ["VersionMismatch", "Refused", "Forgotten", "NothingYet", "DocumentGone", "Unauthorized"].includes(tag);
595
+ }
596
+ function rpcExitCode(error) {
597
+ return isRpcFailure(error) ? exitCodeFor(error) : 3;
598
+ }
599
+ function errorMessage(error) {
600
+ const errors = field(error, "errors");
601
+ if (Array.isArray(errors) && errors.length > 0)
602
+ return errors.map(errorMessage).join(`
603
+ `);
604
+ const message = field(error, "message");
605
+ return typeof message === "string" ? message : String(error);
606
+ }
607
+ function errorTag(error) {
608
+ const tag = field(error, "_tag");
609
+ return typeof tag === "string" ? tag : undefined;
610
+ }
611
+ function errorFields(error) {
612
+ if (typeof error !== "object" || error === null)
613
+ return {};
614
+ return Object.fromEntries(Object.entries(error).filter(([key]) => key !== "_tag" && key !== "message" && key !== "code"));
615
+ }
616
+
617
+ // packages/cli/src/parser.ts
618
+ import { Config as Config3, Effect as Effect7, Option as Option4, Path as Path5, Schema as Schema3 } from "effect";
619
+ import { Argument, CliConfig, CliError, CliOutput, Command, Flag, GlobalFlag } from "effect/cli";
620
+ var cliFormatter = {
621
+ ...CliOutput.defaultFormatter({ colors: false }),
622
+ formatVersion: (name, version) => `${name} ${version}`
623
+ };
624
+ var cliPresenter = CliOutput.layerPresenter({
625
+ present: (event) => event._tag === "Help" && event.reason === "Requested" || event._tag === "Version" ? CliOutput.defaultPresenter.present(event) : Effect7.void
626
+ });
627
+ var subcommands = new Set(["open", "read", "sections", "edit", "append", "todo", "diff", "watch", "status", "stop"]);
628
+ var valueFlags = new Set(["--old", "--new", "--section", "--version", "--since", "--wait", "--settle", "--as"]);
629
+ function normalizeArgv(input) {
630
+ if (input[0] === "--skill")
631
+ return normalizeArgv(["skill", "show", ...input.slice(1)]);
632
+ const result = [];
633
+ let options = true;
634
+ for (let index = 0;index < input.length; index += 1) {
635
+ const value = input[index];
636
+ if (options && value === "--") {
637
+ options = false;
638
+ result.push(value);
639
+ continue;
640
+ }
641
+ if (options && valueFlags.has(value) && index + 1 < input.length && input[index + 1] !== "--") {
642
+ result.push(`${value}=${input[index + 1]}`);
643
+ index += 1;
644
+ continue;
645
+ }
646
+ result.push(value);
647
+ }
648
+ return result;
649
+ }
650
+ function hasNonGlobalArgument(input, end) {
651
+ let skipAsValue = false;
652
+ for (const [index, value] of input.entries()) {
653
+ if (index >= end)
654
+ break;
655
+ if (skipAsValue) {
656
+ skipAsValue = false;
657
+ continue;
658
+ }
659
+ if (value === "--as") {
660
+ skipAsValue = true;
661
+ continue;
662
+ }
663
+ if (value === "--json" || value.startsWith("--as=") || value === "--help" || value === "-h")
664
+ continue;
665
+ if (value.startsWith("-"))
666
+ continue;
667
+ return true;
668
+ }
669
+ return false;
670
+ }
671
+ function cliConfigFor(input) {
672
+ const versionIndex = input.findIndex((value) => value === "--version" || value === "-v");
673
+ const rootVersion = versionIndex >= 0 && !hasNonGlobalArgument(input, versionIndex);
674
+ return CliConfig.make({ builtIns: rootVersion || !input.some((value) => subcommands.has(value)) ? GlobalFlag.BuiltIns : GlobalFlag.BuiltIns.filter((builtIn) => builtIn !== GlobalFlag.Version) });
675
+ }
676
+ var PositiveInt = Schema3.Int.pipe(Schema3.check(Schema3.isGreaterThan(0)));
677
+ var Seconds = Schema3.Finite.pipe(Schema3.check(Schema3.isBetween({ minimum: 0, maximum: 600 })));
678
+ var AgentName = Schema3.String.pipe(Schema3.check(Schema3.isBetweenLength(1, 64), Schema3.isPattern(/^[A-Za-z0-9._-]+$/)));
679
+ var section = Flag.String("section").pipe(Flag.optional);
680
+ var version = Flag.Int("version").pipe(Flag.withSchema(PositiveInt));
681
+ var since = Flag.Int("since").pipe(Flag.withSchema(PositiveInt));
682
+ var optionalSince = Flag.Int("since").pipe(Flag.withSchema(PositiveInt), Flag.optional);
683
+ var wait = Flag.Finite("wait").pipe(Flag.withDefault(0), Flag.withSchema(Seconds));
684
+ var settle = Flag.Finite("settle").pipe(Flag.withSchema(Seconds), Flag.optional);
685
+ var optionValue = (value) => Option4.isSome(value) ? value.value : undefined;
686
+ var pathValue = (value, optional = false) => {
687
+ const displayPath = optional ? value ?? "TODO.md" : value;
688
+ return { displayPath, path: displayPath };
689
+ };
690
+ var defaultAgent = Config3.option(Config3.String("DO_IT_AGENT")).pipe(Effect7.map((name) => Option4.getOrElse(name, () => "").trim() || "agent"), Effect7.orDie);
691
+ var commonFlags = {
692
+ json: Flag.Boolean("json").pipe(Flag.withDefault(false)),
693
+ as: Flag.String("as").pipe(Flag.withDefault(defaultAgent), Flag.withSchema(AgentName))
694
+ };
695
+ function withCommon(root, input, run, make) {
696
+ return Effect7.gen(function* () {
697
+ const globals = yield* root;
698
+ const path = yield* Path5.Path;
699
+ const command = make(input, globals);
700
+ yield* run("path" in command ? { ...command, path: path.resolve(command.path) } : command);
701
+ });
702
+ }
703
+ function makeCli(run) {
704
+ const root = Command.make("do-it").pipe(Command.withDescription("Commands: open, read, sections, edit, append, todo add, todo start, todo check, todo uncheck, todo cancel, todo uncancel, todo list, diff, watch, status, stop, skill install, skill path, skill show (or --skill)"), Command.withSharedFlags(commonFlags));
705
+ const open = Command.make("open", {
706
+ path: Argument.String("path").pipe(Argument.optional),
707
+ noBrowser: Flag.Boolean("no-browser").pipe(Flag.withDefault(false))
708
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "open", options: common, ...pathValue(optionValue(config.path), true), noBrowser: config.noBrowser })));
709
+ const read = Command.make("read", {
710
+ path: Argument.String("path"),
711
+ section
712
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "read", options: common, ...pathValue(config.path), section: optionValue(config.section) })));
713
+ const sections = Command.make("sections", {
714
+ path: Argument.String("path")
715
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "sections", options: common, ...pathValue(config.path) })));
716
+ const edit = Command.make("edit", {
717
+ path: Argument.String("path"),
718
+ version,
719
+ section,
720
+ old: Flag.String("old"),
721
+ next: Flag.String("new")
722
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "edit", options: common, ...pathValue(config.path), version: config.version, section: optionValue(config.section), old: config.old, next: config.next })));
723
+ const append = Command.make("append", {
724
+ path: Argument.String("path"),
725
+ version,
726
+ section,
727
+ text: Argument.String("text")
728
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "append", options: common, ...pathValue(config.path), version: config.version, section: optionValue(config.section), text: config.text })));
729
+ const todoAdd = Command.make("add", {
730
+ path: Argument.String("path"),
731
+ version,
732
+ section,
733
+ text: Argument.String("text")
734
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "todo-add", options: common, ...pathValue(config.path), version: config.version, section: optionValue(config.section), text: config.text })));
735
+ const todoSet = (name, state) => Command.make(name, {
736
+ path: Argument.String("path"),
737
+ version,
738
+ section,
739
+ target: Argument.String("target")
740
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "todo-set", options: common, ...pathValue(config.path), version: config.version, section: optionValue(config.section), target: config.target, state })));
741
+ const todoStart = Command.make("start", {
742
+ path: Argument.String("path"),
743
+ version,
744
+ section,
745
+ target: Argument.String("target")
746
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "todo-start", options: common, ...pathValue(config.path), version: config.version, section: optionValue(config.section), target: config.target })));
747
+ const todoList = Command.make("list", {
748
+ path: Argument.String("path"),
749
+ section
750
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "todo-list", options: common, ...pathValue(config.path), section: optionValue(config.section) })));
751
+ const todo = Command.make("todo").pipe(Command.withSubcommands([
752
+ todoAdd,
753
+ todoStart,
754
+ todoSet("check", "done"),
755
+ todoSet("uncheck", "open"),
756
+ todoSet("cancel", "cancelled"),
757
+ todoSet("uncancel", "open"),
758
+ todoList
759
+ ]));
760
+ const diff = Command.make("diff", {
761
+ path: Argument.String("path"),
762
+ since,
763
+ wait
764
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "diff", options: common, ...pathValue(config.path), since: config.since, wait: config.wait })));
765
+ const watch = Command.make("watch", {
766
+ path: Argument.String("path"),
767
+ optionalSince,
768
+ section,
769
+ todos: Flag.Boolean("todos").pipe(Flag.withDefault(false)),
770
+ settle,
771
+ quiet: Flag.Boolean("quiet").pipe(Flag.withDefault(false))
772
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "watch", options: common, ...pathValue(config.path), since: optionValue(config.optionalSince), section: optionValue(config.section), todos: config.todos, settle: optionValue(config.settle), quiet: config.quiet })));
773
+ const status = Command.make("status", {}, (input) => withCommon(root, input, run, (_config, common) => ({ kind: "status", options: common })));
774
+ const stop = Command.make("stop", {}, (input) => withCommon(root, input, run, (_config, common) => ({ kind: "stop", options: common })));
775
+ const skillInstall = Command.make("install", {
776
+ dir: Flag.String("dir").pipe(Flag.optional),
777
+ force: Flag.Boolean("force").pipe(Flag.withDefault(false))
778
+ }, (input) => withCommon(root, input, run, (config, common) => ({ kind: "skill-install", options: common, dir: optionValue(config.dir), force: config.force })));
779
+ const skillPath = Command.make("path", {}, (input) => withCommon(root, input, run, (_config, common) => ({ kind: "skill-path", options: common })));
780
+ const skillShow = Command.make("show", {}, (input) => withCommon(root, input, run, (_config, common) => ({ kind: "skill-show", options: common })));
781
+ const skill = Command.make("skill").pipe(Command.withDescription("Install the /do-it skill for Claude Code, print it, or print where this CLI keeps it"), Command.withSubcommands([skillInstall, skillPath, skillShow]));
782
+ return root.pipe(Command.withSubcommands([open, read, sections, edit, append, todo, diff, watch, status, stop, skill]));
783
+ }
784
+
785
+ // packages/cli/src/skill.ts
786
+ import { Config as Config4, Effect as Effect8, FileSystem as FileSystem3, Option as Option5, Path as Path6 } from "effect";
787
+ var skillRoot = new URL("../skill", import.meta.url);
788
+ var MARKER = ".do-it-version";
789
+ var skillSource = Effect8.gen(function* () {
790
+ const path = yield* Path6.Path;
791
+ return yield* path.fromFileUrl(skillRoot);
792
+ }).pipe(Effect8.orDie);
793
+ var readSkill = Effect8.gen(function* () {
794
+ const fs = yield* FileSystem3.FileSystem;
795
+ const path = yield* Path6.Path;
796
+ const file = path.join(yield* skillSource, "SKILL.md");
797
+ return { path: file, content: yield* fs.readFileString(file) };
798
+ });
799
+ var defaultSkillDir = Effect8.gen(function* () {
800
+ const path = yield* Path6.Path;
801
+ const configured = yield* Config4.option(Config4.String("CLAUDE_CONFIG_DIR"));
802
+ const base = Option5.isSome(configured) ? configured.value : path.join(yield* Config4.String("HOME"), ".claude");
803
+ return path.join(base, "skills", "do-it");
804
+ }).pipe(Effect8.orDie);
805
+ var installSkill = Effect8.fn("installSkill")(function* (dir, force) {
806
+ const fs = yield* FileSystem3.FileSystem;
807
+ const path = yield* Path6.Path;
808
+ const source = yield* skillSource;
809
+ const target = path.resolve(dir ?? (yield* defaultSkillDir));
810
+ const existing = yield* fs.lstat(target).pipe(Effect8.option);
811
+ if (Option5.isSome(existing)) {
812
+ const same = yield* fs.realPath(target).pipe(Effect8.map((real) => real === source), Effect8.orElseSucceed(() => false));
813
+ if (same)
814
+ return { kind: "linked", path: target };
815
+ const ours = existing.value.type === "Directory" && (yield* fs.exists(path.join(target, MARKER)));
816
+ if (!ours && !force)
817
+ return { kind: "occupied", path: target };
818
+ yield* fs.remove(target, { recursive: true });
819
+ }
820
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true });
821
+ yield* fs.copy(source, target);
822
+ yield* fs.writeFileString(path.join(target, MARKER), `${APP_VERSION}
823
+ `);
824
+ return { kind: "installed", path: target, replaced: Option5.isSome(existing) };
825
+ });
826
+
827
+ // packages/cli/src/stdin.ts
828
+ import { Effect as Effect9, Stdio, Stream } from "effect";
829
+ var MAX_STDIN = 2 * 1024 * 1024;
830
+ var readStdin = Effect9.gen(function* () {
831
+ const stdio = yield* Stdio.Stdio;
832
+ const chunks = [];
833
+ let total = 0;
834
+ yield* Stream.runForEach(stdio.stdin, (chunk) => {
835
+ total += chunk.byteLength;
836
+ if (total > MAX_STDIN)
837
+ return Effect9.fail(new Error("Standard input exceeds 2 MiB"));
838
+ chunks.push(chunk);
839
+ return Effect9.void;
840
+ });
841
+ const bytes = new Uint8Array(total);
842
+ let offset = 0;
843
+ for (const chunk of chunks) {
844
+ bytes.set(chunk, offset);
845
+ offset += chunk.byteLength;
846
+ }
847
+ return new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes);
848
+ });
849
+ function resolveText(text) {
850
+ return text === "-" ? readStdin : Effect9.succeed(text);
851
+ }
852
+
853
+ // packages/cli/src/main.ts
854
+ function outputText(options, text, json) {
855
+ return options.json ? writeJson(json) : writeLine(text);
856
+ }
857
+ function sameAgent(by, name) {
858
+ return by.length > 0 && by.every((author) => author._tag === "Agent" && author.name === name);
859
+ }
860
+ function authorText(by) {
861
+ return by.map((author) => author._tag === "User" ? "the user" : author._tag === "File" ? "file" : author.name).join(", ");
862
+ }
863
+ function rpc(info, f) {
864
+ return withClient(info, f);
865
+ }
866
+ function openCommand(command) {
867
+ return Effect10.gen(function* () {
868
+ const info = yield* ensureDaemon();
869
+ const result = yield* rpc(info, (client) => client.open({ path: command.path, create: true }));
870
+ if (!command.noBrowser)
871
+ yield* openBrowser(result.url);
872
+ const path = command.options.json ? result.path : command.displayPath;
873
+ yield* outputText(command.options, `Opened ${path} at version ${result.version}: ${result.url}`, { path, version: result.version, url: result.url, created: result.created });
874
+ return 0;
875
+ });
876
+ }
877
+ function readCommand(command) {
878
+ return Effect10.gen(function* () {
879
+ const info = yield* ensureDaemon();
880
+ const result = yield* rpc(info, (client) => client.read({ path: command.path, ...command.section === undefined ? {} : { section: command.section } }));
881
+ if (command.options.json)
882
+ yield* writeJson({ version: result.version, text: result.text });
883
+ else
884
+ yield* writeLine(`Version: ${result.version}
885
+ ${result.text}`.replace(/\n$/, ""));
886
+ return 0;
887
+ });
888
+ }
889
+ function sectionsCommand(command) {
890
+ return Effect10.gen(function* () {
891
+ const info = yield* ensureDaemon();
892
+ const result = yield* rpc(info, (client) => client.sections({ path: command.path }));
893
+ const text = [`Version: ${result.version}`, ...result.sections.map((section) => `${section.id} (${section.level}) ${section.title}`)].join(`
894
+ `);
895
+ yield* outputText(command.options, text, { version: result.version, sections: result.sections });
896
+ return 0;
897
+ });
898
+ }
899
+ function operation(command, text) {
900
+ if (command.kind === "edit")
901
+ return { _tag: "Replace", old: command.old, new: command.next };
902
+ if (command.kind === "append")
903
+ return { _tag: "Append", text: text ?? command.text };
904
+ if (command.kind === "todo-add")
905
+ return { _tag: "TodoAdd", text: text ?? command.text };
906
+ if (command.kind === "todo-start") {
907
+ const target = /^[+]?\d+$/.test(command.target.trim()) ? Number(command.target.trim()) : command.target;
908
+ return { _tag: "TodoStart", target };
909
+ }
910
+ const target = /^[+]?\d+$/.test(command.target.trim()) ? Number(command.target.trim()) : command.target;
911
+ return { _tag: "TodoSet", target, state: command.state };
912
+ }
913
+ function editCommand(command) {
914
+ return Effect10.gen(function* () {
915
+ const info = yield* ensureDaemon();
916
+ const text = command.kind === "append" || command.kind === "todo-add" ? yield* resolveText(command.text) : undefined;
917
+ const result = yield* rpc(info, (client) => client.edit({
918
+ path: command.path,
919
+ author: { _tag: "Agent", name: command.options.as },
920
+ version: command.version,
921
+ ...command.section === undefined ? {} : { section: command.section },
922
+ op: operation(command, text)
923
+ }));
924
+ const reply = editReply(result);
925
+ yield* outputText(command.options, reply.text, reply.json);
926
+ return 0;
927
+ });
928
+ }
929
+ function todoListCommand(command) {
930
+ return Effect10.gen(function* () {
931
+ const info = yield* ensureDaemon();
932
+ const result = yield* rpc(info, (client) => client.todos({ path: command.path, ...command.section === undefined ? {} : { section: command.section } }));
933
+ const lines = result.todos.map((todo) => `${todo.number}. [${todo.state === "done" ? "x" : todo.state === "cancelled" ? "-" : " "}] ${todo.text}`);
934
+ yield* outputText(command.options, [`Version: ${result.version}`, ...lines].join(`
935
+ `), { version: result.version, todos: result.todos });
936
+ return 0;
937
+ });
938
+ }
939
+ function diffCommand(command) {
940
+ return Effect10.gen(function* () {
941
+ const info = yield* ensureDaemon();
942
+ const result = yield* rpc(info, (client) => client.diff({ path: command.path, since: command.since, wait: command.wait }));
943
+ const text = diffText(formatVersionHeader(result.from, result.to, result.by), result.diff);
944
+ yield* outputText(command.options, text, { from: result.from, to: result.to, by: result.by, diff: result.diff, todos: result.todos });
945
+ return 0;
946
+ });
947
+ }
948
+ function statusCommand(command) {
949
+ return Effect10.gen(function* () {
950
+ const info = yield* ensureDaemon({ spawn: false });
951
+ const result = yield* rpc(info, (client) => client.status({}));
952
+ const upSeconds = Math.floor(Math.max(0, Date.now() - result.startedAt) / 1000);
953
+ const minutes = Math.floor(upSeconds / 60);
954
+ if (command.options.json)
955
+ yield* writeJson({ pid: result.pid, port: result.port, startedAt: result.startedAt, appVersion: result.appVersion, documents: result.documents, upSeconds });
956
+ else
957
+ yield* writeLine(`Daemon pid ${result.pid} on 127.0.0.1:${result.port}, up ${minutes}m
958
+ ${result.documents.map((document) => ` ${document.path} version ${document.version} ${document.watchers} watchers`).join(`
959
+ `)}`);
960
+ return 0;
961
+ });
962
+ }
963
+ function stopCommand(command) {
964
+ return Effect10.gen(function* () {
965
+ const stopped = yield* stopDaemon();
966
+ yield* outputText(command.options, stopped ? "Stopped" : "Not running", { stopped });
967
+ return 0;
968
+ });
969
+ }
970
+ function skillInstallCommand(command) {
971
+ return Effect10.gen(function* () {
972
+ const result = yield* installSkill(command.dir, command.force);
973
+ if (result.kind === "occupied") {
974
+ yield* outputText(command.options, `${result.path} already exists and was not installed by do-it. Pass --force to replace it.`, { error: { code: "Refused", message: "The skill directory already exists", path: result.path } });
975
+ return 1;
976
+ }
977
+ const text = result.kind === "linked" ? `${result.path} already links to this skill.` : `${result.replaced ? "Updated" : "Installed"} the do-it skill in ${result.path}. In Claude Code, run /do-it to start.`;
978
+ yield* outputText(command.options, text, { kind: result.kind, path: result.path });
979
+ return 0;
980
+ });
981
+ }
982
+ function skillPathCommand(command) {
983
+ return Effect10.gen(function* () {
984
+ const path = yield* skillSource;
985
+ yield* outputText(command.options, path, { path });
986
+ return 0;
987
+ });
988
+ }
989
+ function skillShowCommand(command) {
990
+ return Effect10.gen(function* () {
991
+ const skill = yield* readSkill;
992
+ yield* outputText(command.options, skill.content, skill);
993
+ return 0;
994
+ });
995
+ }
996
+ function initialWatchOutput(command, version) {
997
+ if (command.quiet)
998
+ return Effect10.void;
999
+ return outputText(command.options, `Watching ${command.displayPath} at version ${version}`, { event: "watching", version });
1000
+ }
1001
+ function restartOutput(command, version) {
1002
+ return outputText(command.options, `Daemon restarted, now version ${version}`, { event: "restarted", version });
1003
+ }
1004
+ function resyncOutput(command, version) {
1005
+ return outputText(command.options, `Missed changes, now at version ${version}: run do-it read`, { event: "resync", version });
1006
+ }
1007
+ function deletedOutput(command) {
1008
+ return outputText(command.options, `Deleted ${command.displayPath}`, { event: "deleted" });
1009
+ }
1010
+ function watchCommand(command) {
1011
+ return Effect10.scoped(Effect10.gen(function* () {
1012
+ let since = command.since;
1013
+ let pid;
1014
+ let first = true;
1015
+ let announce = false;
1016
+ let backoff = 250;
1017
+ let deleted = false;
1018
+ let lastFailure;
1019
+ const timer = yield* FiberHandle.make();
1020
+ let pending = false;
1021
+ let settleVersion = 0;
1022
+ let settleBy = [];
1023
+ const settled = (delay) => Effect10.sleep(delay).pipe(Effect10.andThen(Effect10.uninterruptible(Effect10.suspend(() => {
1024
+ const version = settleVersion;
1025
+ const by = settleBy;
1026
+ pending = false;
1027
+ settleBy = [];
1028
+ const byText = by.join(", ");
1029
+ return outputText(command.options, `Settled at version ${version}${byText ? ` by ${byText}` : ""}`, { event: "settled", version, by });
1030
+ }))));
1031
+ const schedule = (version, by) => Effect10.suspend(() => {
1032
+ if (command.settle === undefined)
1033
+ return Effect10.void;
1034
+ const authors = authorText(by).split(", ").filter(Boolean);
1035
+ settleVersion = version;
1036
+ settleBy = [...new Set(pending ? [...settleBy, ...authors] : authors)];
1037
+ pending = true;
1038
+ return FiberHandle.run(timer, settled(Duration.millis(command.settle * 1000))).pipe(Effect10.asVoid);
1039
+ });
1040
+ while (!deleted) {
1041
+ const daemon = yield* Effect10.exit(ensureDaemon());
1042
+ if (Exit.isFailure(daemon)) {
1043
+ if (Cause.hasInterruptsOnly(daemon.cause))
1044
+ break;
1045
+ const failure = Cause.squash(daemon.cause);
1046
+ const tag = errorTag(failure);
1047
+ if (tag !== "DaemonUnavailable") {
1048
+ const message = errorMessage(failure);
1049
+ if (message !== lastFailure)
1050
+ yield* outputText(command.options, `Watch failed: ${message}, retrying`, { event: "error", message });
1051
+ lastFailure = message;
1052
+ } else
1053
+ lastFailure = undefined;
1054
+ yield* Effect10.sleep(`${backoff} millis`);
1055
+ backoff = Math.min(5000, backoff * 2);
1056
+ continue;
1057
+ }
1058
+ const info = daemon.value;
1059
+ if (pid !== undefined && pid !== info.pid)
1060
+ announce = true;
1061
+ pid = info.pid;
1062
+ const payload = { path: command.path, ...since === undefined ? {} : { since }, ...command.section === undefined ? {} : { section: command.section }, text: false };
1063
+ const exit = yield* Effect10.exit(rpc(info, (client) => client.watch(payload).pipe(Stream2.runForEach((event) => Effect10.gen(function* () {
1064
+ lastFailure = undefined;
1065
+ if (event._tag === "Watching") {
1066
+ backoff = 250;
1067
+ if (first) {
1068
+ yield* initialWatchOutput(command, event.version);
1069
+ first = false;
1070
+ } else if (announce) {
1071
+ yield* restartOutput(command, event.version);
1072
+ announce = false;
1073
+ }
1074
+ since = event.version;
1075
+ return;
1076
+ }
1077
+ if (event._tag === "Resync") {
1078
+ yield* resyncOutput(command, event.version);
1079
+ if (command.settle !== undefined)
1080
+ yield* schedule(event.version, []);
1081
+ since = event.version;
1082
+ announce = false;
1083
+ return;
1084
+ }
1085
+ if (event._tag === "Deleted") {
1086
+ yield* deletedOutput(command);
1087
+ deleted = true;
1088
+ return;
1089
+ }
1090
+ since = event.to;
1091
+ if (sameAgent(event.by, command.options.as))
1092
+ return;
1093
+ if (command.settle !== undefined) {
1094
+ yield* schedule(event.to, event.by);
1095
+ return;
1096
+ }
1097
+ if (command.todos) {
1098
+ if (!event.todos.length)
1099
+ return;
1100
+ if (command.options.json)
1101
+ yield* writeJson(changeJson(event));
1102
+ else
1103
+ yield* writeLine(todosText(event.todos));
1104
+ } else if (command.options.json)
1105
+ yield* writeJson(changeJson(event));
1106
+ else
1107
+ yield* writeLine(changeReply(event, false));
1108
+ })))));
1109
+ if (deleted)
1110
+ break;
1111
+ if (Exit.isFailure(exit)) {
1112
+ if (Cause.hasInterruptsOnly(exit.cause))
1113
+ break;
1114
+ const failure = Cause.squash(exit.cause);
1115
+ const tag = errorTag(failure);
1116
+ if (tag === "DocumentGone" || tag === "Refused")
1117
+ return yield* errorResult(command, failure);
1118
+ if (tag !== "DaemonUnavailable" && tag !== "RpcClientError") {
1119
+ const message = errorMessage(failure);
1120
+ if (message !== lastFailure)
1121
+ yield* outputText(command.options, `Watch failed: ${message}, retrying`, { event: "error", message });
1122
+ lastFailure = message;
1123
+ } else
1124
+ lastFailure = undefined;
1125
+ } else
1126
+ lastFailure = undefined;
1127
+ yield* Effect10.sleep(`${backoff} millis`);
1128
+ backoff = Math.min(5000, backoff * 2);
1129
+ }
1130
+ return deleted ? 9 : 0;
1131
+ }));
1132
+ }
1133
+ function commandEffect(command) {
1134
+ switch (command.kind) {
1135
+ case "open":
1136
+ return openCommand(command);
1137
+ case "read":
1138
+ return readCommand(command);
1139
+ case "sections":
1140
+ return sectionsCommand(command);
1141
+ case "edit":
1142
+ case "append":
1143
+ case "todo-add":
1144
+ case "todo-set":
1145
+ case "todo-start":
1146
+ return editCommand(command);
1147
+ case "todo-list":
1148
+ return todoListCommand(command);
1149
+ case "diff":
1150
+ return diffCommand(command);
1151
+ case "watch":
1152
+ return watchCommand(command);
1153
+ case "status":
1154
+ return statusCommand(command);
1155
+ case "stop":
1156
+ return stopCommand(command);
1157
+ case "skill-install":
1158
+ return skillInstallCommand(command);
1159
+ case "skill-path":
1160
+ return skillPathCommand(command);
1161
+ case "skill-show":
1162
+ return skillShowCommand(command);
1163
+ }
1164
+ }
1165
+ function errorResult(command, error) {
1166
+ return Effect10.gen(function* () {
1167
+ const requested = ("version" in command ? command.version : ("since" in command) ? command.since : 0) ?? 0;
1168
+ const tag = errorTag(error);
1169
+ if (command.kind === "status" && tag === "DaemonUnavailable") {
1170
+ yield* outputText(command.options, "Not running", { error: { code: "DaemonUnavailable", message: "Not running" } });
1171
+ return 3;
1172
+ }
1173
+ if (tag === "VersionMismatch" || tag === "Forgotten") {
1174
+ const reply = mismatchReply(error, requested);
1175
+ const { kind: _kind, ...fields } = reply.json;
1176
+ yield* outputText(command.options, reply.text, { error: { code: tag, message: reply.text, ...fields } });
1177
+ return 1;
1178
+ }
1179
+ const code = rpcExitCode(error);
1180
+ const message = tag === "NothingYet" && command.kind === "diff" ? `No change since version ${command.since} in ${command.wait} ${command.wait === 1 ? "second" : "seconds"}` : errorMessage(error);
1181
+ const fields = errorFields(error);
1182
+ yield* outputText(command.options, message, { error: { code: rpcErrorCode(error), message, ...fields } });
1183
+ return code;
1184
+ });
1185
+ }
1186
+ function program() {
1187
+ let commandExit = 0;
1188
+ const cli = makeCli((command) => Effect10.gen(function* () {
1189
+ const result = yield* Effect10.exit(commandEffect(command));
1190
+ commandExit = Exit.isSuccess(result) ? result.value : yield* errorResult(command, Cause.squash(result.cause));
1191
+ }));
1192
+ return Effect10.gen(function* () {
1193
+ const args = yield* (yield* Stdio2.Stdio).args;
1194
+ const input = normalizeArgv(args);
1195
+ const result = yield* Effect10.exit(Command2.runWith(cli, { version: APP_VERSION, renderErrors: true })(input).pipe(Effect10.provide(CliConfig2.layer(cliConfigFor(input))), Effect10.provide(CliOutput2.layer(cliFormatter)), Effect10.provide(cliPresenter)));
1196
+ if (Exit.isFailure(result)) {
1197
+ const message = errorMessage(Cause.squash(result.cause));
1198
+ if (args.includes("--json"))
1199
+ yield* writeJson({ error: { code: "invalid", message } });
1200
+ else
1201
+ yield* writeLine(message);
1202
+ return 2;
1203
+ }
1204
+ return commandExit;
1205
+ }).pipe(Effect10.provide(BunServices.layer));
1206
+ }
1207
+ var main = program();
1208
+ BunRuntime.runMain(main, { disableErrorReporting: true, teardown: (exit, done) => done(Exit.isSuccess(exit) ? Number(exit.value) : 1) });