@ship-cli/opencode 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/package.json +27 -10
- package/src/compaction.ts +78 -0
- package/src/plugin.ts +2529 -504
- package/src/services.ts +885 -0
- package/src/utils.ts +180 -0
- package/LICENSE +0 -21
package/src/services.ts
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core services for the Ship OpenCode Plugin.
|
|
3
|
+
* Separated from plugin.ts for testability (avoids @opencode-ai/plugin import issues in tests).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Data from "effect/Data";
|
|
8
|
+
import * as Context from "effect/Context";
|
|
9
|
+
import { extractJson, type ShipTask } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Errors
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export class ShipCommandError extends Data.TaggedError("ShipCommandError")<{
|
|
16
|
+
readonly command: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
}> {}
|
|
19
|
+
|
|
20
|
+
export class JsonParseError extends Data.TaggedError("JsonParseError")<{
|
|
21
|
+
readonly raw: string;
|
|
22
|
+
readonly cause: unknown;
|
|
23
|
+
}> {}
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Types
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
export interface ShipStatus {
|
|
30
|
+
configured: boolean;
|
|
31
|
+
teamId?: string;
|
|
32
|
+
teamKey?: string;
|
|
33
|
+
projectId?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ShipSubtask {
|
|
37
|
+
id: string;
|
|
38
|
+
identifier: string;
|
|
39
|
+
title: string;
|
|
40
|
+
state: string;
|
|
41
|
+
stateType: string;
|
|
42
|
+
isDone: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ShipMilestone {
|
|
46
|
+
id: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description?: string | null;
|
|
50
|
+
targetDate?: string | null;
|
|
51
|
+
projectId: string;
|
|
52
|
+
sortOrder: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StackChange {
|
|
56
|
+
changeId: string;
|
|
57
|
+
commitId: string;
|
|
58
|
+
description: string;
|
|
59
|
+
bookmarks: string[];
|
|
60
|
+
isEmpty: boolean;
|
|
61
|
+
isWorkingCopy: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface StackStatus {
|
|
65
|
+
isRepo: boolean;
|
|
66
|
+
change?: {
|
|
67
|
+
changeId: string;
|
|
68
|
+
commitId: string;
|
|
69
|
+
description: string;
|
|
70
|
+
bookmarks: string[];
|
|
71
|
+
isEmpty: boolean;
|
|
72
|
+
};
|
|
73
|
+
error?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface StackCreateResult {
|
|
77
|
+
created: boolean;
|
|
78
|
+
changeId?: string;
|
|
79
|
+
bookmark?: string;
|
|
80
|
+
workspace?: {
|
|
81
|
+
name: string;
|
|
82
|
+
path: string;
|
|
83
|
+
created: boolean;
|
|
84
|
+
};
|
|
85
|
+
error?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface StackDescribeResult {
|
|
89
|
+
updated: boolean;
|
|
90
|
+
changeId?: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
error?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface AbandonedMergedChange {
|
|
96
|
+
changeId: string;
|
|
97
|
+
bookmark?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface StackSyncResult {
|
|
101
|
+
fetched: boolean;
|
|
102
|
+
rebased: boolean;
|
|
103
|
+
trunkChangeId?: string;
|
|
104
|
+
stackSize?: number;
|
|
105
|
+
conflicted?: boolean;
|
|
106
|
+
abandonedMergedChanges?: AbandonedMergedChange[];
|
|
107
|
+
stackFullyMerged?: boolean;
|
|
108
|
+
cleanedUpWorkspace?: string;
|
|
109
|
+
error?: { tag: string; message: string };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface StackRestackResult {
|
|
113
|
+
restacked: boolean;
|
|
114
|
+
stackSize?: number;
|
|
115
|
+
trunkChangeId?: string;
|
|
116
|
+
conflicted?: boolean;
|
|
117
|
+
error?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface StackSubmitResult {
|
|
121
|
+
pushed: boolean;
|
|
122
|
+
bookmark?: string;
|
|
123
|
+
baseBranch?: string;
|
|
124
|
+
pr?: {
|
|
125
|
+
url: string;
|
|
126
|
+
number: number;
|
|
127
|
+
status: "created" | "updated" | "exists";
|
|
128
|
+
};
|
|
129
|
+
error?: string;
|
|
130
|
+
subscribed?: {
|
|
131
|
+
sessionId: string;
|
|
132
|
+
prNumbers: number[];
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface StackSquashResult {
|
|
137
|
+
squashed: boolean;
|
|
138
|
+
intoChangeId?: string;
|
|
139
|
+
description?: string;
|
|
140
|
+
error?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface StackAbandonResult {
|
|
144
|
+
abandoned: boolean;
|
|
145
|
+
changeId?: string;
|
|
146
|
+
newWorkingCopy?: string;
|
|
147
|
+
error?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface StackNavigateResult {
|
|
151
|
+
moved: boolean;
|
|
152
|
+
from?: {
|
|
153
|
+
changeId: string;
|
|
154
|
+
description: string;
|
|
155
|
+
};
|
|
156
|
+
to?: {
|
|
157
|
+
changeId: string;
|
|
158
|
+
description: string;
|
|
159
|
+
};
|
|
160
|
+
error?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface StackUndoResult {
|
|
164
|
+
undone: boolean;
|
|
165
|
+
operation?: string;
|
|
166
|
+
error?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface StackUpdateStaleResult {
|
|
170
|
+
updated: boolean;
|
|
171
|
+
changeId?: string;
|
|
172
|
+
error?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface StackBookmarkResult {
|
|
176
|
+
success: boolean;
|
|
177
|
+
action: "created" | "moved";
|
|
178
|
+
bookmark: string;
|
|
179
|
+
changeId?: string;
|
|
180
|
+
error?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface WorkspaceOutput {
|
|
184
|
+
name: string;
|
|
185
|
+
path: string;
|
|
186
|
+
changeId: string;
|
|
187
|
+
description: string;
|
|
188
|
+
isDefault: boolean;
|
|
189
|
+
stackName: string | null;
|
|
190
|
+
taskId: string | null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface RemoveWorkspaceResult {
|
|
194
|
+
removed: boolean;
|
|
195
|
+
name: string;
|
|
196
|
+
filesDeleted?: boolean;
|
|
197
|
+
error?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface WebhookDaemonStatus {
|
|
201
|
+
running: boolean;
|
|
202
|
+
pid?: number;
|
|
203
|
+
repo?: string;
|
|
204
|
+
connectedToGitHub?: boolean;
|
|
205
|
+
subscriptions?: Array<{ sessionId: string; prNumbers: number[] }>;
|
|
206
|
+
uptime?: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface WebhookSubscribeResult {
|
|
210
|
+
subscribed: boolean;
|
|
211
|
+
sessionId?: string;
|
|
212
|
+
prNumbers?: number[];
|
|
213
|
+
error?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface WebhookUnsubscribeResult {
|
|
217
|
+
unsubscribed: boolean;
|
|
218
|
+
sessionId?: string;
|
|
219
|
+
prNumbers?: number[];
|
|
220
|
+
error?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface WebhookCleanupResult {
|
|
224
|
+
success: boolean;
|
|
225
|
+
removedSessions: string[];
|
|
226
|
+
remainingSessions?: number;
|
|
227
|
+
error?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// PR Review types
|
|
231
|
+
// Note: This type mirrors ReviewOutput from CLI's review.ts
|
|
232
|
+
// Kept separate for plugin isolation (plugin doesn't depend on CLI package)
|
|
233
|
+
export interface PrReviewOutput {
|
|
234
|
+
prNumber: number;
|
|
235
|
+
prTitle?: string;
|
|
236
|
+
prUrl?: string;
|
|
237
|
+
reviews: Array<{
|
|
238
|
+
id: number;
|
|
239
|
+
author: string;
|
|
240
|
+
state: string;
|
|
241
|
+
body: string;
|
|
242
|
+
submittedAt: string;
|
|
243
|
+
}>;
|
|
244
|
+
codeComments: Array<{
|
|
245
|
+
id: number;
|
|
246
|
+
path: string;
|
|
247
|
+
line: number | null;
|
|
248
|
+
body: string;
|
|
249
|
+
author: string;
|
|
250
|
+
createdAt: string;
|
|
251
|
+
inReplyToId: number | null;
|
|
252
|
+
diffHunk?: string;
|
|
253
|
+
}>;
|
|
254
|
+
conversationComments: Array<{
|
|
255
|
+
id: number;
|
|
256
|
+
body: string;
|
|
257
|
+
author: string;
|
|
258
|
+
createdAt: string;
|
|
259
|
+
}>;
|
|
260
|
+
commentsByFile: Record<
|
|
261
|
+
string,
|
|
262
|
+
Array<{
|
|
263
|
+
line: number | null;
|
|
264
|
+
author: string;
|
|
265
|
+
body: string;
|
|
266
|
+
id: number;
|
|
267
|
+
diffHunk?: string;
|
|
268
|
+
}>
|
|
269
|
+
>;
|
|
270
|
+
error?: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Shell Service
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
277
|
+
export interface ShellService {
|
|
278
|
+
readonly run: (args: string[], cwd?: string) => Effect.Effect<string, ShipCommandError>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const ShellService = Context.GenericTag<ShellService>("ShellService");
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create a shell service that extracts JSON from CLI output.
|
|
285
|
+
* The actual shell execution is handled by the BunShell in the plugin.
|
|
286
|
+
*/
|
|
287
|
+
export const createShellService = (
|
|
288
|
+
execute: (args: string[], cwd?: string) => Effect.Effect<string, ShipCommandError>,
|
|
289
|
+
): ShellService => ({
|
|
290
|
+
run: (args: string[], cwd?: string) =>
|
|
291
|
+
execute(args, cwd).pipe(
|
|
292
|
+
Effect.map((output) => (args.includes("--json") ? extractJson(output) : output)),
|
|
293
|
+
),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// JSON Parsing
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Parse JSON with type assertion.
|
|
302
|
+
*/
|
|
303
|
+
export const parseJson = <T>(raw: string): Effect.Effect<T, JsonParseError> =>
|
|
304
|
+
Effect.try({
|
|
305
|
+
try: () => {
|
|
306
|
+
const parsed = JSON.parse(raw);
|
|
307
|
+
if (parsed === null || (typeof parsed !== "object" && !Array.isArray(parsed))) {
|
|
308
|
+
throw new Error(`Expected object or array, got ${typeof parsed}`);
|
|
309
|
+
}
|
|
310
|
+
return parsed as T;
|
|
311
|
+
},
|
|
312
|
+
catch: (cause) => new JsonParseError({ raw: raw.slice(0, 500), cause }),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// =============================================================================
|
|
316
|
+
// Ship Service
|
|
317
|
+
// =============================================================================
|
|
318
|
+
|
|
319
|
+
export interface ShipService {
|
|
320
|
+
readonly checkConfigured: () => Effect.Effect<ShipStatus, ShipCommandError | JsonParseError>;
|
|
321
|
+
readonly getReadyTasks: () => Effect.Effect<ShipTask[], ShipCommandError | JsonParseError>;
|
|
322
|
+
readonly getBlockedTasks: () => Effect.Effect<ShipTask[], ShipCommandError | JsonParseError>;
|
|
323
|
+
readonly listTasks: (filter?: {
|
|
324
|
+
status?: string;
|
|
325
|
+
priority?: string;
|
|
326
|
+
mine?: boolean;
|
|
327
|
+
}) => Effect.Effect<ShipTask[], ShipCommandError | JsonParseError>;
|
|
328
|
+
readonly getTask: (taskId: string) => Effect.Effect<ShipTask, ShipCommandError | JsonParseError>;
|
|
329
|
+
readonly startTask: (taskId: string, sessionId?: string) => Effect.Effect<void, ShipCommandError>;
|
|
330
|
+
readonly completeTask: (taskId: string) => Effect.Effect<void, ShipCommandError>;
|
|
331
|
+
readonly createTask: (input: {
|
|
332
|
+
title: string;
|
|
333
|
+
description?: string;
|
|
334
|
+
priority?: string;
|
|
335
|
+
parentId?: string;
|
|
336
|
+
}) => Effect.Effect<ShipTask, ShipCommandError | JsonParseError>;
|
|
337
|
+
readonly updateTask: (
|
|
338
|
+
taskId: string,
|
|
339
|
+
input: {
|
|
340
|
+
title?: string;
|
|
341
|
+
description?: string;
|
|
342
|
+
priority?: string;
|
|
343
|
+
status?: string;
|
|
344
|
+
parentId?: string;
|
|
345
|
+
},
|
|
346
|
+
) => Effect.Effect<ShipTask, ShipCommandError | JsonParseError>;
|
|
347
|
+
readonly addBlocker: (blocker: string, blocked: string) => Effect.Effect<void, ShipCommandError>;
|
|
348
|
+
readonly removeBlocker: (
|
|
349
|
+
blocker: string,
|
|
350
|
+
blocked: string,
|
|
351
|
+
) => Effect.Effect<void, ShipCommandError>;
|
|
352
|
+
readonly relateTask: (
|
|
353
|
+
taskId: string,
|
|
354
|
+
relatedTaskId: string,
|
|
355
|
+
) => Effect.Effect<void, ShipCommandError>;
|
|
356
|
+
readonly getStackLog: (
|
|
357
|
+
workdir?: string,
|
|
358
|
+
) => Effect.Effect<StackChange[], ShipCommandError | JsonParseError>;
|
|
359
|
+
readonly getStackStatus: (
|
|
360
|
+
workdir?: string,
|
|
361
|
+
) => Effect.Effect<StackStatus, ShipCommandError | JsonParseError>;
|
|
362
|
+
readonly createStackChange: (input: {
|
|
363
|
+
message?: string;
|
|
364
|
+
bookmark?: string;
|
|
365
|
+
noWorkspace?: boolean;
|
|
366
|
+
taskId?: string;
|
|
367
|
+
workdir?: string;
|
|
368
|
+
}) => Effect.Effect<StackCreateResult, ShipCommandError | JsonParseError>;
|
|
369
|
+
readonly describeStackChange: (
|
|
370
|
+
message: string,
|
|
371
|
+
workdir?: string,
|
|
372
|
+
) => Effect.Effect<StackDescribeResult, ShipCommandError | JsonParseError>;
|
|
373
|
+
readonly syncStack: (
|
|
374
|
+
workdir?: string,
|
|
375
|
+
) => Effect.Effect<StackSyncResult, ShipCommandError | JsonParseError>;
|
|
376
|
+
readonly restackStack: (
|
|
377
|
+
workdir?: string,
|
|
378
|
+
) => Effect.Effect<StackRestackResult, ShipCommandError | JsonParseError>;
|
|
379
|
+
readonly submitStack: (input: {
|
|
380
|
+
draft?: boolean;
|
|
381
|
+
title?: string;
|
|
382
|
+
body?: string;
|
|
383
|
+
subscribe?: string;
|
|
384
|
+
workdir?: string;
|
|
385
|
+
}) => Effect.Effect<StackSubmitResult, ShipCommandError | JsonParseError>;
|
|
386
|
+
readonly squashStack: (
|
|
387
|
+
message: string,
|
|
388
|
+
workdir?: string,
|
|
389
|
+
) => Effect.Effect<StackSquashResult, ShipCommandError | JsonParseError>;
|
|
390
|
+
readonly abandonStack: (
|
|
391
|
+
changeId?: string,
|
|
392
|
+
workdir?: string,
|
|
393
|
+
) => Effect.Effect<StackAbandonResult, ShipCommandError | JsonParseError>;
|
|
394
|
+
readonly stackUp: (
|
|
395
|
+
workdir?: string,
|
|
396
|
+
) => Effect.Effect<StackNavigateResult, ShipCommandError | JsonParseError>;
|
|
397
|
+
readonly stackDown: (
|
|
398
|
+
workdir?: string,
|
|
399
|
+
) => Effect.Effect<StackNavigateResult, ShipCommandError | JsonParseError>;
|
|
400
|
+
readonly stackUndo: (
|
|
401
|
+
workdir?: string,
|
|
402
|
+
) => Effect.Effect<StackUndoResult, ShipCommandError | JsonParseError>;
|
|
403
|
+
readonly stackUpdateStale: (
|
|
404
|
+
workdir?: string,
|
|
405
|
+
) => Effect.Effect<StackUpdateStaleResult, ShipCommandError | JsonParseError>;
|
|
406
|
+
readonly bookmarkStack: (
|
|
407
|
+
name: string,
|
|
408
|
+
move?: boolean,
|
|
409
|
+
workdir?: string,
|
|
410
|
+
) => Effect.Effect<StackBookmarkResult, ShipCommandError | JsonParseError>;
|
|
411
|
+
readonly getDaemonStatus: () => Effect.Effect<
|
|
412
|
+
WebhookDaemonStatus,
|
|
413
|
+
ShipCommandError | JsonParseError
|
|
414
|
+
>;
|
|
415
|
+
readonly subscribeToPRs: (
|
|
416
|
+
sessionId: string,
|
|
417
|
+
prNumbers: number[],
|
|
418
|
+
serverUrl?: string,
|
|
419
|
+
) => Effect.Effect<WebhookSubscribeResult, ShipCommandError | JsonParseError>;
|
|
420
|
+
readonly unsubscribeFromPRs: (
|
|
421
|
+
sessionId: string,
|
|
422
|
+
prNumbers: number[],
|
|
423
|
+
serverUrl?: string,
|
|
424
|
+
) => Effect.Effect<WebhookUnsubscribeResult, ShipCommandError | JsonParseError>;
|
|
425
|
+
readonly cleanupStaleSubscriptions: () => Effect.Effect<
|
|
426
|
+
WebhookCleanupResult,
|
|
427
|
+
ShipCommandError | JsonParseError
|
|
428
|
+
>;
|
|
429
|
+
readonly listWorkspaces: (
|
|
430
|
+
workdir?: string,
|
|
431
|
+
) => Effect.Effect<WorkspaceOutput[], ShipCommandError | JsonParseError>;
|
|
432
|
+
readonly removeWorkspace: (
|
|
433
|
+
name: string,
|
|
434
|
+
deleteFiles?: boolean,
|
|
435
|
+
workdir?: string,
|
|
436
|
+
) => Effect.Effect<RemoveWorkspaceResult, ShipCommandError | JsonParseError>;
|
|
437
|
+
readonly listMilestones: () => Effect.Effect<ShipMilestone[], ShipCommandError | JsonParseError>;
|
|
438
|
+
readonly getMilestone: (
|
|
439
|
+
milestoneId: string,
|
|
440
|
+
) => Effect.Effect<ShipMilestone, ShipCommandError | JsonParseError>;
|
|
441
|
+
readonly createMilestone: (input: {
|
|
442
|
+
name: string;
|
|
443
|
+
description?: string;
|
|
444
|
+
targetDate?: string;
|
|
445
|
+
}) => Effect.Effect<ShipMilestone, ShipCommandError | JsonParseError>;
|
|
446
|
+
readonly updateMilestone: (
|
|
447
|
+
milestoneId: string,
|
|
448
|
+
input: { name?: string; description?: string; targetDate?: string },
|
|
449
|
+
) => Effect.Effect<ShipMilestone, ShipCommandError | JsonParseError>;
|
|
450
|
+
readonly deleteMilestone: (milestoneId: string) => Effect.Effect<void, ShipCommandError>;
|
|
451
|
+
readonly setTaskMilestone: (
|
|
452
|
+
taskId: string,
|
|
453
|
+
milestoneId: string,
|
|
454
|
+
) => Effect.Effect<ShipTask, ShipCommandError | JsonParseError>;
|
|
455
|
+
readonly unsetTaskMilestone: (
|
|
456
|
+
taskId: string,
|
|
457
|
+
) => Effect.Effect<ShipTask, ShipCommandError | JsonParseError>;
|
|
458
|
+
// PR review operations
|
|
459
|
+
readonly getPrReviews: (
|
|
460
|
+
prNumber?: number,
|
|
461
|
+
unresolved?: boolean,
|
|
462
|
+
workdir?: string,
|
|
463
|
+
) => Effect.Effect<PrReviewOutput, ShipCommandError | JsonParseError>;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export const ShipServiceTag = Context.GenericTag<ShipService>("ShipService");
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Create the ShipService implementation.
|
|
470
|
+
* This is an Effect that requires ShellService and produces ShipService.
|
|
471
|
+
*/
|
|
472
|
+
export const makeShipService = Effect.gen(function* () {
|
|
473
|
+
const shell = yield* ShellService;
|
|
474
|
+
|
|
475
|
+
const checkConfigured = () =>
|
|
476
|
+
Effect.gen(function* () {
|
|
477
|
+
const output = yield* shell.run(["status", "--json"]);
|
|
478
|
+
return yield* parseJson<ShipStatus>(output);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const getReadyTasks = () =>
|
|
482
|
+
Effect.gen(function* () {
|
|
483
|
+
const output = yield* shell.run(["task", "ready", "--json"]);
|
|
484
|
+
return yield* parseJson<ShipTask[]>(output);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const getBlockedTasks = () =>
|
|
488
|
+
Effect.gen(function* () {
|
|
489
|
+
const output = yield* shell.run(["task", "blocked", "--json"]);
|
|
490
|
+
return yield* parseJson<ShipTask[]>(output);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const listTasks = (filter?: { status?: string; priority?: string; mine?: boolean }) =>
|
|
494
|
+
Effect.gen(function* () {
|
|
495
|
+
const args = ["task", "list", "--json"];
|
|
496
|
+
if (filter?.status) args.push("--status", filter.status);
|
|
497
|
+
if (filter?.priority) args.push("--priority", filter.priority);
|
|
498
|
+
if (filter?.mine) args.push("--mine");
|
|
499
|
+
|
|
500
|
+
const output = yield* shell.run(args);
|
|
501
|
+
return yield* parseJson<ShipTask[]>(output);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const getTask = (taskId: string) =>
|
|
505
|
+
Effect.gen(function* () {
|
|
506
|
+
const output = yield* shell.run(["task", "show", "--json", taskId]);
|
|
507
|
+
return yield* parseJson<ShipTask>(output);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const startTask = (taskId: string, sessionId?: string) => {
|
|
511
|
+
const args = ["task", "start"];
|
|
512
|
+
if (sessionId) {
|
|
513
|
+
args.push("--session", sessionId);
|
|
514
|
+
}
|
|
515
|
+
args.push(taskId);
|
|
516
|
+
return shell.run(args).pipe(Effect.asVoid);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const completeTask = (taskId: string) => shell.run(["task", "done", taskId]).pipe(Effect.asVoid);
|
|
520
|
+
|
|
521
|
+
const createTask = (input: {
|
|
522
|
+
title: string;
|
|
523
|
+
description?: string;
|
|
524
|
+
priority?: string;
|
|
525
|
+
parentId?: string;
|
|
526
|
+
}) =>
|
|
527
|
+
Effect.gen(function* () {
|
|
528
|
+
const args = ["task", "create", "--json"];
|
|
529
|
+
if (input.description) args.push("--description", input.description);
|
|
530
|
+
if (input.priority) args.push("--priority", input.priority);
|
|
531
|
+
if (input.parentId) args.push("--parent", input.parentId);
|
|
532
|
+
args.push(input.title);
|
|
533
|
+
|
|
534
|
+
const output = yield* shell.run(args);
|
|
535
|
+
const response = yield* parseJson<{ task: ShipTask }>(output);
|
|
536
|
+
return response.task;
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const updateTask = (
|
|
540
|
+
taskId: string,
|
|
541
|
+
input: {
|
|
542
|
+
title?: string;
|
|
543
|
+
description?: string;
|
|
544
|
+
priority?: string;
|
|
545
|
+
status?: string;
|
|
546
|
+
parentId?: string;
|
|
547
|
+
},
|
|
548
|
+
) =>
|
|
549
|
+
Effect.gen(function* () {
|
|
550
|
+
const args = ["task", "update", "--json"];
|
|
551
|
+
if (input.title) args.push("--title", input.title);
|
|
552
|
+
if (input.description) args.push("--description", input.description);
|
|
553
|
+
if (input.priority) args.push("--priority", input.priority);
|
|
554
|
+
if (input.status) args.push("--status", input.status);
|
|
555
|
+
if (input.parentId !== undefined) args.push("--parent", input.parentId);
|
|
556
|
+
args.push(taskId);
|
|
557
|
+
|
|
558
|
+
const output = yield* shell.run(args);
|
|
559
|
+
const response = yield* parseJson<{ task: ShipTask }>(output);
|
|
560
|
+
return response.task;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const addBlocker = (blocker: string, blocked: string) =>
|
|
564
|
+
shell.run(["task", "block", blocker, blocked]).pipe(Effect.asVoid);
|
|
565
|
+
|
|
566
|
+
const removeBlocker = (blocker: string, blocked: string) =>
|
|
567
|
+
shell.run(["task", "unblock", blocker, blocked]).pipe(Effect.asVoid);
|
|
568
|
+
|
|
569
|
+
const relateTask = (taskId: string, relatedTaskId: string) =>
|
|
570
|
+
shell.run(["task", "relate", taskId, relatedTaskId]).pipe(Effect.asVoid);
|
|
571
|
+
|
|
572
|
+
const getStackLog = (workdir?: string) =>
|
|
573
|
+
Effect.gen(function* () {
|
|
574
|
+
const output = yield* shell.run(["stack", "log", "--json"], workdir);
|
|
575
|
+
return yield* parseJson<StackChange[]>(output);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const getStackStatus = (workdir?: string) =>
|
|
579
|
+
Effect.gen(function* () {
|
|
580
|
+
const output = yield* shell.run(["stack", "status", "--json"], workdir);
|
|
581
|
+
return yield* parseJson<StackStatus>(output);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const createStackChange = (input: {
|
|
585
|
+
message?: string;
|
|
586
|
+
bookmark?: string;
|
|
587
|
+
noWorkspace?: boolean;
|
|
588
|
+
taskId?: string;
|
|
589
|
+
workdir?: string;
|
|
590
|
+
}) =>
|
|
591
|
+
Effect.gen(function* () {
|
|
592
|
+
const args = ["stack", "create", "--json"];
|
|
593
|
+
if (input.message) args.push("--message", input.message);
|
|
594
|
+
if (input.bookmark) args.push("--bookmark", input.bookmark);
|
|
595
|
+
if (input.noWorkspace) args.push("--no-workspace");
|
|
596
|
+
if (input.taskId) args.push("--task-id", input.taskId);
|
|
597
|
+
const output = yield* shell.run(args, input.workdir);
|
|
598
|
+
return yield* parseJson<StackCreateResult>(output);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const describeStackChange = (message: string, workdir?: string) =>
|
|
602
|
+
Effect.gen(function* () {
|
|
603
|
+
const output = yield* shell.run(
|
|
604
|
+
["stack", "describe", "--json", "--message", message],
|
|
605
|
+
workdir,
|
|
606
|
+
);
|
|
607
|
+
return yield* parseJson<StackDescribeResult>(output);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const syncStack = (workdir?: string) =>
|
|
611
|
+
Effect.gen(function* () {
|
|
612
|
+
const output = yield* shell.run(["stack", "sync", "--json"], workdir);
|
|
613
|
+
return yield* parseJson<StackSyncResult>(output);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const restackStack = (workdir?: string) =>
|
|
617
|
+
Effect.gen(function* () {
|
|
618
|
+
const output = yield* shell.run(["stack", "restack", "--json"], workdir);
|
|
619
|
+
return yield* parseJson<StackRestackResult>(output);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const submitStack = (input: {
|
|
623
|
+
draft?: boolean;
|
|
624
|
+
title?: string;
|
|
625
|
+
body?: string;
|
|
626
|
+
subscribe?: string;
|
|
627
|
+
workdir?: string;
|
|
628
|
+
}) =>
|
|
629
|
+
Effect.gen(function* () {
|
|
630
|
+
const args = ["stack", "submit", "--json"];
|
|
631
|
+
if (input.draft) args.push("--draft");
|
|
632
|
+
if (input.title) args.push("--title", input.title);
|
|
633
|
+
if (input.body) args.push("--body", input.body);
|
|
634
|
+
if (input.subscribe) args.push("--subscribe", input.subscribe);
|
|
635
|
+
const output = yield* shell.run(args, input.workdir);
|
|
636
|
+
return yield* parseJson<StackSubmitResult>(output);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const squashStack = (message: string, workdir?: string) =>
|
|
640
|
+
Effect.gen(function* () {
|
|
641
|
+
const output = yield* shell.run(["stack", "squash", "--json", "-m", message], workdir);
|
|
642
|
+
return yield* parseJson<StackSquashResult>(output);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const abandonStack = (changeId?: string, workdir?: string) =>
|
|
646
|
+
Effect.gen(function* () {
|
|
647
|
+
const args = ["stack", "abandon", "--json"];
|
|
648
|
+
if (changeId) args.push(changeId);
|
|
649
|
+
const output = yield* shell.run(args, workdir);
|
|
650
|
+
return yield* parseJson<StackAbandonResult>(output);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const stackUp = (workdir?: string) =>
|
|
654
|
+
Effect.gen(function* () {
|
|
655
|
+
const output = yield* shell.run(["stack", "up", "--json"], workdir);
|
|
656
|
+
return yield* parseJson<StackNavigateResult>(output);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const stackDown = (workdir?: string) =>
|
|
660
|
+
Effect.gen(function* () {
|
|
661
|
+
const output = yield* shell.run(["stack", "down", "--json"], workdir);
|
|
662
|
+
return yield* parseJson<StackNavigateResult>(output);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const stackUndo = (workdir?: string) =>
|
|
666
|
+
Effect.gen(function* () {
|
|
667
|
+
const output = yield* shell.run(["stack", "undo", "--json"], workdir);
|
|
668
|
+
return yield* parseJson<StackUndoResult>(output);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const stackUpdateStale = (workdir?: string) =>
|
|
672
|
+
Effect.gen(function* () {
|
|
673
|
+
const output = yield* shell.run(["stack", "update-stale", "--json"], workdir);
|
|
674
|
+
return yield* parseJson<StackUpdateStaleResult>(output);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const bookmarkStack = (name: string, move?: boolean, workdir?: string) =>
|
|
678
|
+
Effect.gen(function* () {
|
|
679
|
+
const args = ["stack", "bookmark", "--json"];
|
|
680
|
+
if (move) args.push("--move");
|
|
681
|
+
args.push(name);
|
|
682
|
+
const output = yield* shell.run(args, workdir);
|
|
683
|
+
return yield* parseJson<StackBookmarkResult>(output);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const getDaemonStatus = (): Effect.Effect<
|
|
687
|
+
WebhookDaemonStatus,
|
|
688
|
+
ShipCommandError | JsonParseError
|
|
689
|
+
> =>
|
|
690
|
+
Effect.gen(function* () {
|
|
691
|
+
const output = yield* shell
|
|
692
|
+
.run(["webhook", "status", "--json"])
|
|
693
|
+
.pipe(Effect.catchAll(() => Effect.succeed('{"running":false}')));
|
|
694
|
+
return yield* parseJson<WebhookDaemonStatus>(output);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const subscribeToPRs = (
|
|
698
|
+
sessionId: string,
|
|
699
|
+
prNumbers: number[],
|
|
700
|
+
serverUrl?: string,
|
|
701
|
+
): Effect.Effect<WebhookSubscribeResult, ShipCommandError | JsonParseError> =>
|
|
702
|
+
Effect.gen(function* () {
|
|
703
|
+
const prNumbersStr = prNumbers.join(",");
|
|
704
|
+
const args = ["webhook", "subscribe", "--json", "--session", sessionId];
|
|
705
|
+
if (serverUrl) {
|
|
706
|
+
args.push("--server-url", serverUrl);
|
|
707
|
+
}
|
|
708
|
+
args.push(prNumbersStr);
|
|
709
|
+
const output = yield* shell.run(args);
|
|
710
|
+
return yield* parseJson<WebhookSubscribeResult>(output);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const unsubscribeFromPRs = (
|
|
714
|
+
sessionId: string,
|
|
715
|
+
prNumbers: number[],
|
|
716
|
+
serverUrl?: string,
|
|
717
|
+
): Effect.Effect<WebhookUnsubscribeResult, ShipCommandError | JsonParseError> =>
|
|
718
|
+
Effect.gen(function* () {
|
|
719
|
+
const prNumbersStr = prNumbers.join(",");
|
|
720
|
+
const args = ["webhook", "unsubscribe", "--json", "--session", sessionId];
|
|
721
|
+
if (serverUrl) {
|
|
722
|
+
args.push("--server-url", serverUrl);
|
|
723
|
+
}
|
|
724
|
+
args.push(prNumbersStr);
|
|
725
|
+
const output = yield* shell.run(args);
|
|
726
|
+
return yield* parseJson<WebhookUnsubscribeResult>(output);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const cleanupStaleSubscriptions = (): Effect.Effect<
|
|
730
|
+
WebhookCleanupResult,
|
|
731
|
+
ShipCommandError | JsonParseError
|
|
732
|
+
> =>
|
|
733
|
+
Effect.gen(function* () {
|
|
734
|
+
const output = yield* shell.run(["webhook", "cleanup", "--json"]);
|
|
735
|
+
return yield* parseJson<WebhookCleanupResult>(output);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const listWorkspaces = (
|
|
739
|
+
workdir?: string,
|
|
740
|
+
): Effect.Effect<WorkspaceOutput[], ShipCommandError | JsonParseError> =>
|
|
741
|
+
Effect.gen(function* () {
|
|
742
|
+
const output = yield* shell.run(["stack", "workspaces", "--json"], workdir);
|
|
743
|
+
return yield* parseJson<WorkspaceOutput[]>(output);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const removeWorkspace = (
|
|
747
|
+
name: string,
|
|
748
|
+
deleteFiles?: boolean,
|
|
749
|
+
workdir?: string,
|
|
750
|
+
): Effect.Effect<RemoveWorkspaceResult, ShipCommandError | JsonParseError> =>
|
|
751
|
+
Effect.gen(function* () {
|
|
752
|
+
const args = ["stack", "remove-workspace", "--json"];
|
|
753
|
+
if (deleteFiles) args.push("--delete");
|
|
754
|
+
args.push(name);
|
|
755
|
+
const output = yield* shell.run(args, workdir);
|
|
756
|
+
return yield* parseJson<RemoveWorkspaceResult>(output);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
const listMilestones = (): Effect.Effect<ShipMilestone[], ShipCommandError | JsonParseError> =>
|
|
760
|
+
Effect.gen(function* () {
|
|
761
|
+
const output = yield* shell.run(["milestone", "list", "--json"]);
|
|
762
|
+
return yield* parseJson<ShipMilestone[]>(output);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const getMilestone = (
|
|
766
|
+
milestoneId: string,
|
|
767
|
+
): Effect.Effect<ShipMilestone, ShipCommandError | JsonParseError> =>
|
|
768
|
+
Effect.gen(function* () {
|
|
769
|
+
const output = yield* shell.run(["milestone", "show", "--json", milestoneId]);
|
|
770
|
+
return yield* parseJson<ShipMilestone>(output);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const createMilestone = (input: {
|
|
774
|
+
name: string;
|
|
775
|
+
description?: string;
|
|
776
|
+
targetDate?: string;
|
|
777
|
+
}): Effect.Effect<ShipMilestone, ShipCommandError | JsonParseError> =>
|
|
778
|
+
Effect.gen(function* () {
|
|
779
|
+
const args = ["milestone", "create", "--json"];
|
|
780
|
+
if (input.description) args.push("--description", input.description);
|
|
781
|
+
if (input.targetDate) args.push("--target-date", input.targetDate);
|
|
782
|
+
args.push(input.name);
|
|
783
|
+
|
|
784
|
+
const output = yield* shell.run(args);
|
|
785
|
+
const response = yield* parseJson<{ milestone: ShipMilestone }>(output);
|
|
786
|
+
return response.milestone;
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
const updateMilestone = (
|
|
790
|
+
milestoneId: string,
|
|
791
|
+
input: { name?: string; description?: string; targetDate?: string },
|
|
792
|
+
): Effect.Effect<ShipMilestone, ShipCommandError | JsonParseError> =>
|
|
793
|
+
Effect.gen(function* () {
|
|
794
|
+
const args = ["milestone", "update", "--json"];
|
|
795
|
+
if (input.name) args.push("--name", input.name);
|
|
796
|
+
if (input.description) args.push("--description", input.description);
|
|
797
|
+
if (input.targetDate) args.push("--target-date", input.targetDate);
|
|
798
|
+
args.push(milestoneId);
|
|
799
|
+
|
|
800
|
+
const output = yield* shell.run(args);
|
|
801
|
+
const response = yield* parseJson<{ milestone: ShipMilestone }>(output);
|
|
802
|
+
return response.milestone;
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const deleteMilestone = (milestoneId: string): Effect.Effect<void, ShipCommandError> =>
|
|
806
|
+
shell.run(["milestone", "delete", milestoneId]).pipe(Effect.asVoid);
|
|
807
|
+
|
|
808
|
+
const setTaskMilestone = (
|
|
809
|
+
taskId: string,
|
|
810
|
+
milestoneId: string,
|
|
811
|
+
): Effect.Effect<ShipTask, ShipCommandError | JsonParseError> =>
|
|
812
|
+
Effect.gen(function* () {
|
|
813
|
+
const args = ["task", "update", "--json", "--milestone", milestoneId, taskId];
|
|
814
|
+
const output = yield* shell.run(args);
|
|
815
|
+
const response = yield* parseJson<{ task: ShipTask }>(output);
|
|
816
|
+
return response.task;
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const unsetTaskMilestone = (
|
|
820
|
+
taskId: string,
|
|
821
|
+
): Effect.Effect<ShipTask, ShipCommandError | JsonParseError> =>
|
|
822
|
+
Effect.gen(function* () {
|
|
823
|
+
const args = ["task", "update", "--json", "--milestone", "", taskId];
|
|
824
|
+
const output = yield* shell.run(args);
|
|
825
|
+
const response = yield* parseJson<{ task: ShipTask }>(output);
|
|
826
|
+
return response.task;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// PR reviews operations
|
|
830
|
+
const getPrReviews = (
|
|
831
|
+
prNumber?: number,
|
|
832
|
+
unresolved?: boolean,
|
|
833
|
+
workdir?: string,
|
|
834
|
+
): Effect.Effect<PrReviewOutput, ShipCommandError | JsonParseError> =>
|
|
835
|
+
Effect.gen(function* () {
|
|
836
|
+
const args = ["pr", "reviews", "--json"];
|
|
837
|
+
if (unresolved) args.push("--unresolved");
|
|
838
|
+
if (prNumber !== undefined) args.push(String(prNumber));
|
|
839
|
+
const output = yield* shell.run(args, workdir);
|
|
840
|
+
return yield* parseJson<PrReviewOutput>(output);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
checkConfigured,
|
|
845
|
+
getReadyTasks,
|
|
846
|
+
getBlockedTasks,
|
|
847
|
+
listTasks,
|
|
848
|
+
getTask,
|
|
849
|
+
startTask,
|
|
850
|
+
completeTask,
|
|
851
|
+
createTask,
|
|
852
|
+
updateTask,
|
|
853
|
+
addBlocker,
|
|
854
|
+
removeBlocker,
|
|
855
|
+
relateTask,
|
|
856
|
+
getStackLog,
|
|
857
|
+
getStackStatus,
|
|
858
|
+
createStackChange,
|
|
859
|
+
describeStackChange,
|
|
860
|
+
syncStack,
|
|
861
|
+
restackStack,
|
|
862
|
+
submitStack,
|
|
863
|
+
squashStack,
|
|
864
|
+
abandonStack,
|
|
865
|
+
stackUp,
|
|
866
|
+
stackDown,
|
|
867
|
+
stackUndo,
|
|
868
|
+
stackUpdateStale,
|
|
869
|
+
bookmarkStack,
|
|
870
|
+
getDaemonStatus,
|
|
871
|
+
subscribeToPRs,
|
|
872
|
+
unsubscribeFromPRs,
|
|
873
|
+
cleanupStaleSubscriptions,
|
|
874
|
+
listWorkspaces,
|
|
875
|
+
removeWorkspace,
|
|
876
|
+
listMilestones,
|
|
877
|
+
getMilestone,
|
|
878
|
+
createMilestone,
|
|
879
|
+
updateMilestone,
|
|
880
|
+
deleteMilestone,
|
|
881
|
+
setTaskMilestone,
|
|
882
|
+
unsetTaskMilestone,
|
|
883
|
+
getPrReviews,
|
|
884
|
+
} satisfies ShipService;
|
|
885
|
+
});
|