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/LICENSE +21 -0
- package/NOTICE +25 -0
- package/README.md +189 -2
- package/bin/daemon.js +2212 -0
- package/bin/do-it.js +1208 -0
- package/package.json +45 -4
- package/skill/SKILL.md +263 -0
- package/web/assets/index-6QftMBqY.css +2 -0
- package/web/assets/index-Bey8I5te.js +130 -0
- package/web/index.html +13 -0
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) });
|