@ship-cli/core 0.0.1 → 0.0.3
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/dist/bin.js +9 -34
- package/package.json +1 -1
- package/src/adapters/driven/linear/IssueRepositoryLive.ts +38 -4
- package/src/adapters/driven/linear/TeamRepositoryLive.ts +2 -1
- package/src/adapters/driving/cli/commands/prime.ts +44 -35
- package/src/adapters/driving/cli/commands/relate.ts +56 -0
- package/src/adapters/driving/cli/commands/start.ts +3 -1
- package/src/adapters/driving/cli/commands/team.ts +9 -8
- package/src/adapters/driving/cli/commands/update.ts +125 -0
- package/src/adapters/driving/cli/main.ts +6 -0
- package/src/ports/IssueRepository.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/bin.js
CHANGED
|
@@ -33924,7 +33924,9 @@ var startCommand = make58(
|
|
|
33924
33924
|
if (json2) {
|
|
33925
33925
|
yield* log3(JSON.stringify({ status: "already_in_progress", task: taskId }));
|
|
33926
33926
|
} else {
|
|
33927
|
-
yield* log3(
|
|
33927
|
+
yield* log3(
|
|
33928
|
+
`Task ${task.identifier} is already in progress (${task.state.name}).`
|
|
33929
|
+
);
|
|
33928
33930
|
}
|
|
33929
33931
|
return;
|
|
33930
33932
|
}
|
|
@@ -34239,28 +34241,6 @@ var blockedCommand = make58(
|
|
|
34239
34241
|
);
|
|
34240
34242
|
|
|
34241
34243
|
// src/adapters/driving/cli/commands/prime.ts
|
|
34242
|
-
var AGENT_GUIDANCE = `## Agent Restrictions
|
|
34243
|
-
|
|
34244
|
-
1. **Never create issues without user confirmation**
|
|
34245
|
-
2. **Check blockers before starting work** - blocked tasks should be surfaced
|
|
34246
|
-
3. **Small, focused tasks only** - if a task seems too large, suggest splitting
|
|
34247
|
-
4. **Always update status** - in_progress when starting, done when complete
|
|
34248
|
-
5. **Stack blocking tasks** - work on blockers first, stack changes appropriately
|
|
34249
|
-
6. **Use conventional commits** - format: type(TASK-ID): description
|
|
34250
|
-
|
|
34251
|
-
## CLI Commands
|
|
34252
|
-
|
|
34253
|
-
- \`ship ready --json\` - List tasks with no blockers
|
|
34254
|
-
- \`ship show <id> --json\` - Show task details
|
|
34255
|
-
- \`ship start <id>\` - Begin work (sets status to in_progress)
|
|
34256
|
-
- \`ship done <id> --reason "msg"\` - Complete task
|
|
34257
|
-
- \`ship blocked --json\` - Show blocked tasks
|
|
34258
|
-
- \`ship block <blocker> <blocked>\` - Create blocking relationship
|
|
34259
|
-
- \`ship unblock <blocker> <blocked>\` - Remove blocking relationship
|
|
34260
|
-
- \`ship list --json\` - List all tasks
|
|
34261
|
-
- \`ship create "title" -p priority -t type\` - Create new task
|
|
34262
|
-
|
|
34263
|
-
Always use \`--json\` flag for programmatic output.`;
|
|
34264
34244
|
var formatTaskCompact = (task) => {
|
|
34265
34245
|
const priority = task.priority === "urgent" ? "!" : task.priority === "high" ? "^" : "";
|
|
34266
34246
|
return `${priority}${task.identifier}: ${task.title} [${task.state.name}]`;
|
|
@@ -34286,20 +34266,19 @@ var primeCommand = make58(
|
|
|
34286
34266
|
const allTasks = yield* issueRepo.listTasks(cfg.linear.teamId, filter11);
|
|
34287
34267
|
const inProgressTasks = allTasks.filter((t2) => t2.state.type === "started");
|
|
34288
34268
|
const lines3 = [];
|
|
34289
|
-
lines3.push("<ship-context>");
|
|
34290
34269
|
lines3.push(`Team: ${cfg.linear.teamKey}`);
|
|
34291
34270
|
if (isSome2(cfg.linear.projectId)) {
|
|
34292
34271
|
lines3.push(`Project: ${cfg.linear.projectId.value}`);
|
|
34293
34272
|
}
|
|
34294
|
-
lines3.push("");
|
|
34295
34273
|
if (inProgressTasks.length > 0) {
|
|
34274
|
+
lines3.push("");
|
|
34296
34275
|
lines3.push("## In Progress");
|
|
34297
34276
|
for (const task of inProgressTasks) {
|
|
34298
34277
|
lines3.push(`- ${formatTaskCompact(task)}`);
|
|
34299
34278
|
}
|
|
34300
|
-
lines3.push("");
|
|
34301
34279
|
}
|
|
34302
34280
|
if (readyTasks.length > 0) {
|
|
34281
|
+
lines3.push("");
|
|
34303
34282
|
lines3.push("## Ready to Work");
|
|
34304
34283
|
for (const task of readyTasks.slice(0, 10)) {
|
|
34305
34284
|
lines3.push(`- ${formatTaskCompact(task)}`);
|
|
@@ -34307,9 +34286,9 @@ var primeCommand = make58(
|
|
|
34307
34286
|
if (readyTasks.length > 10) {
|
|
34308
34287
|
lines3.push(` ... and ${readyTasks.length - 10} more`);
|
|
34309
34288
|
}
|
|
34310
|
-
lines3.push("");
|
|
34311
34289
|
}
|
|
34312
34290
|
if (blockedTasks.length > 0) {
|
|
34291
|
+
lines3.push("");
|
|
34313
34292
|
lines3.push("## Blocked");
|
|
34314
34293
|
for (const task of blockedTasks.slice(0, 5)) {
|
|
34315
34294
|
lines3.push(`- ${formatTaskCompact(task)}`);
|
|
@@ -34320,13 +34299,7 @@ var primeCommand = make58(
|
|
|
34320
34299
|
if (blockedTasks.length > 5) {
|
|
34321
34300
|
lines3.push(` ... and ${blockedTasks.length - 5} more`);
|
|
34322
34301
|
}
|
|
34323
|
-
lines3.push("");
|
|
34324
34302
|
}
|
|
34325
|
-
lines3.push("</ship-context>");
|
|
34326
|
-
lines3.push("");
|
|
34327
|
-
lines3.push("<ship-guidance>");
|
|
34328
|
-
lines3.push(AGENT_GUIDANCE);
|
|
34329
|
-
lines3.push("</ship-guidance>");
|
|
34330
34303
|
yield* log3(lines3.join("\n"));
|
|
34331
34304
|
})
|
|
34332
34305
|
);
|
|
@@ -49133,7 +49106,9 @@ var make72 = gen2(function* () {
|
|
|
49133
49106
|
catch: (e3) => new LinearApiError({ message: `Failed to create relation: ${e3}`, cause: e3 })
|
|
49134
49107
|
});
|
|
49135
49108
|
if (!result.success) {
|
|
49136
|
-
return yield* fail7(
|
|
49109
|
+
return yield* fail7(
|
|
49110
|
+
new TaskError({ message: "Failed to create blocking relation" })
|
|
49111
|
+
);
|
|
49137
49112
|
}
|
|
49138
49113
|
}),
|
|
49139
49114
|
"Adding blocker"
|
package/package.json
CHANGED
|
@@ -388,11 +388,14 @@ const make = Effect.gen(function* () {
|
|
|
388
388
|
relatedIssueId: blockerId,
|
|
389
389
|
type: LinearDocument.IssueRelationType.Blocks,
|
|
390
390
|
}),
|
|
391
|
-
catch: (e) =>
|
|
391
|
+
catch: (e) =>
|
|
392
|
+
new LinearApiError({ message: `Failed to create relation: ${e}`, cause: e }),
|
|
392
393
|
});
|
|
393
394
|
|
|
394
395
|
if (!result.success) {
|
|
395
|
-
return yield* Effect.fail(
|
|
396
|
+
return yield* Effect.fail(
|
|
397
|
+
new TaskError({ message: "Failed to create blocking relation" }),
|
|
398
|
+
);
|
|
396
399
|
}
|
|
397
400
|
}),
|
|
398
401
|
"Adding blocker",
|
|
@@ -417,7 +420,8 @@ const make = Effect.gen(function* () {
|
|
|
417
420
|
|
|
418
421
|
const relations = yield* Effect.tryPromise({
|
|
419
422
|
try: () => blocked.relations(),
|
|
420
|
-
catch: (e) =>
|
|
423
|
+
catch: (e) =>
|
|
424
|
+
new LinearApiError({ message: `Failed to fetch relations: ${e}`, cause: e }),
|
|
421
425
|
});
|
|
422
426
|
|
|
423
427
|
const relationToDelete = yield* Effect.tryPromise({
|
|
@@ -442,12 +446,41 @@ const make = Effect.gen(function* () {
|
|
|
442
446
|
|
|
443
447
|
yield* Effect.tryPromise({
|
|
444
448
|
try: () => client.deleteIssueRelation(relationToDelete.id),
|
|
445
|
-
catch: (e) =>
|
|
449
|
+
catch: (e) =>
|
|
450
|
+
new LinearApiError({ message: `Failed to delete relation: ${e}`, cause: e }),
|
|
446
451
|
});
|
|
447
452
|
}),
|
|
448
453
|
"Removing blocker",
|
|
449
454
|
);
|
|
450
455
|
|
|
456
|
+
const addRelated = (
|
|
457
|
+
taskId: TaskId,
|
|
458
|
+
relatedTaskId: TaskId,
|
|
459
|
+
): Effect.Effect<void, TaskNotFoundError | TaskError | LinearApiError> =>
|
|
460
|
+
withRetryAndTimeout(
|
|
461
|
+
Effect.gen(function* () {
|
|
462
|
+
const client = yield* linearClient.client();
|
|
463
|
+
|
|
464
|
+
const result = yield* Effect.tryPromise({
|
|
465
|
+
try: () =>
|
|
466
|
+
client.createIssueRelation({
|
|
467
|
+
issueId: taskId,
|
|
468
|
+
relatedIssueId: relatedTaskId,
|
|
469
|
+
type: LinearDocument.IssueRelationType.Related,
|
|
470
|
+
}),
|
|
471
|
+
catch: (e) =>
|
|
472
|
+
new LinearApiError({ message: `Failed to create relation: ${e}`, cause: e }),
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (!result.success) {
|
|
476
|
+
return yield* Effect.fail(
|
|
477
|
+
new TaskError({ message: "Failed to create related relation" }),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}),
|
|
481
|
+
"Adding related",
|
|
482
|
+
);
|
|
483
|
+
|
|
451
484
|
const getBranchName = (id: TaskId): Effect.Effect<string, TaskNotFoundError | LinearApiError> =>
|
|
452
485
|
withRetryAndTimeout(
|
|
453
486
|
Effect.gen(function* () {
|
|
@@ -487,6 +520,7 @@ const make = Effect.gen(function* () {
|
|
|
487
520
|
getBlockedTasks,
|
|
488
521
|
addBlocker,
|
|
489
522
|
removeBlocker,
|
|
523
|
+
addRelated,
|
|
490
524
|
getBranchName,
|
|
491
525
|
};
|
|
492
526
|
});
|
|
@@ -82,7 +82,8 @@ const make = Effect.gen(function* () {
|
|
|
82
82
|
if (!t) throw new Error("Team not returned");
|
|
83
83
|
return t;
|
|
84
84
|
},
|
|
85
|
-
catch: (e) =>
|
|
85
|
+
catch: (e) =>
|
|
86
|
+
new LinearApiError({ message: `Failed to get created team: ${e}`, cause: e }),
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
return mapTeam(team);
|
|
@@ -6,32 +6,42 @@ import { ConfigRepository } from "../../../../ports/ConfigRepository.js";
|
|
|
6
6
|
import { IssueRepository } from "../../../../ports/IssueRepository.js";
|
|
7
7
|
import { TaskFilter, type Task } from "../../../../domain/Task.js";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const formatTaskCompact = (task: Task): string => {
|
|
10
|
+
const priority = task.priority === "urgent" ? "!" : task.priority === "high" ? "^" : "";
|
|
11
|
+
return `${priority}${task.identifier}: ${task.title} [${task.state.name}]`;
|
|
12
|
+
};
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
4. **Always update status** - in_progress when starting, done when complete
|
|
15
|
-
5. **Stack blocking tasks** - work on blockers first, stack changes appropriately
|
|
16
|
-
6. **Use conventional commits** - format: type(TASK-ID): description
|
|
14
|
+
const formatTaskFull = (task: Task): string[] => {
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
const priority = task.priority === "urgent" ? "!" : task.priority === "high" ? "^" : "";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
lines.push(`### ${priority}${task.identifier}: ${task.title}`);
|
|
19
|
+
lines.push(`**Status:** ${task.state.name} | **Priority:** ${task.priority}`);
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- \`ship done <id> --reason "msg"\` - Complete task
|
|
24
|
-
- \`ship blocked --json\` - Show blocked tasks
|
|
25
|
-
- \`ship block <blocker> <blocked>\` - Create blocking relationship
|
|
26
|
-
- \`ship unblock <blocker> <blocked>\` - Remove blocking relationship
|
|
27
|
-
- \`ship list --json\` - List all tasks
|
|
28
|
-
- \`ship create "title" -p priority -t type\` - Create new task
|
|
21
|
+
if (task.labels.length > 0) {
|
|
22
|
+
lines.push(`**Labels:** ${task.labels.join(", ")}`);
|
|
23
|
+
}
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
if (Option.isSome(task.branchName)) {
|
|
26
|
+
lines.push(`**Branch:** \`${task.branchName.value}\``);
|
|
27
|
+
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
if (task.blockedBy.length > 0) {
|
|
30
|
+
lines.push(`**Blocked by:** ${task.blockedBy.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (task.blocks.length > 0) {
|
|
34
|
+
lines.push(`**Blocks:** ${task.blocks.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (Option.isSome(task.description) && task.description.value.trim()) {
|
|
38
|
+
lines.push("");
|
|
39
|
+
lines.push(task.description.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.push(`**URL:** ${task.url}`);
|
|
43
|
+
|
|
44
|
+
return lines;
|
|
35
45
|
};
|
|
36
46
|
|
|
37
47
|
export const primeCommand = Command.make("prime", {}, () =>
|
|
@@ -60,25 +70,30 @@ export const primeCommand = Command.make("prime", {}, () =>
|
|
|
60
70
|
// Filter to only "started" state type tasks
|
|
61
71
|
const inProgressTasks = allTasks.filter((t: Task) => t.state.type === "started");
|
|
62
72
|
|
|
63
|
-
// Build context output
|
|
73
|
+
// Build context output (plain markdown, no XML tags - plugin wraps it)
|
|
64
74
|
const lines: string[] = [];
|
|
65
75
|
|
|
66
|
-
lines.push("<ship-context>");
|
|
67
76
|
lines.push(`Team: ${cfg.linear.teamKey}`);
|
|
68
77
|
if (Option.isSome(cfg.linear.projectId)) {
|
|
69
78
|
lines.push(`Project: ${cfg.linear.projectId.value}`);
|
|
70
79
|
}
|
|
71
|
-
lines.push("");
|
|
72
80
|
|
|
81
|
+
// Show in-progress tasks with FULL details (description, acceptance criteria, etc.)
|
|
73
82
|
if (inProgressTasks.length > 0) {
|
|
74
|
-
lines.push("
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("## Current Work (In Progress)");
|
|
85
|
+
lines.push("");
|
|
75
86
|
for (const task of inProgressTasks) {
|
|
76
|
-
lines.push(
|
|
87
|
+
lines.push(...formatTaskFull(task));
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("---");
|
|
90
|
+
lines.push("");
|
|
77
91
|
}
|
|
78
|
-
lines.push("");
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
// Show ready tasks with compact format (just titles)
|
|
81
95
|
if (readyTasks.length > 0) {
|
|
96
|
+
lines.push("");
|
|
82
97
|
lines.push("## Ready to Work");
|
|
83
98
|
for (const task of readyTasks.slice(0, 10)) {
|
|
84
99
|
lines.push(`- ${formatTaskCompact(task)}`);
|
|
@@ -86,10 +101,11 @@ export const primeCommand = Command.make("prime", {}, () =>
|
|
|
86
101
|
if (readyTasks.length > 10) {
|
|
87
102
|
lines.push(` ... and ${readyTasks.length - 10} more`);
|
|
88
103
|
}
|
|
89
|
-
lines.push("");
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
// Show blocked tasks with blockers info
|
|
92
107
|
if (blockedTasks.length > 0) {
|
|
108
|
+
lines.push("");
|
|
93
109
|
lines.push("## Blocked");
|
|
94
110
|
for (const task of blockedTasks.slice(0, 5)) {
|
|
95
111
|
lines.push(`- ${formatTaskCompact(task)}`);
|
|
@@ -100,15 +116,8 @@ export const primeCommand = Command.make("prime", {}, () =>
|
|
|
100
116
|
if (blockedTasks.length > 5) {
|
|
101
117
|
lines.push(` ... and ${blockedTasks.length - 5} more`);
|
|
102
118
|
}
|
|
103
|
-
lines.push("");
|
|
104
119
|
}
|
|
105
120
|
|
|
106
|
-
lines.push("</ship-context>");
|
|
107
|
-
lines.push("");
|
|
108
|
-
lines.push("<ship-guidance>");
|
|
109
|
-
lines.push(AGENT_GUIDANCE);
|
|
110
|
-
lines.push("</ship-guidance>");
|
|
111
|
-
|
|
112
121
|
yield* Console.log(lines.join("\n"));
|
|
113
122
|
}),
|
|
114
123
|
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Command from "@effect/cli/Command";
|
|
2
|
+
import * as Args from "@effect/cli/Args";
|
|
3
|
+
import * as Options from "@effect/cli/Options";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Console from "effect/Console";
|
|
6
|
+
import { IssueRepository } from "../../../../ports/IssueRepository.js";
|
|
7
|
+
import type { TaskId } from "../../../../domain/Task.js";
|
|
8
|
+
|
|
9
|
+
const taskAArg = Args.text({ name: "task-a" }).pipe(
|
|
10
|
+
Args.withDescription("First task identifier (e.g., BRI-123)"),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const taskBArg = Args.text({ name: "task-b" }).pipe(
|
|
14
|
+
Args.withDescription("Second task identifier (e.g., BRI-456)"),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const jsonOption = Options.boolean("json").pipe(
|
|
18
|
+
Options.withDescription("Output as JSON"),
|
|
19
|
+
Options.withDefault(false),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const relateCommand = Command.make(
|
|
23
|
+
"relate",
|
|
24
|
+
{ taskA: taskAArg, taskB: taskBArg, json: jsonOption },
|
|
25
|
+
({ taskA, taskB, json }) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const issueRepo = yield* IssueRepository;
|
|
28
|
+
|
|
29
|
+
// Resolve identifiers to IDs
|
|
30
|
+
const [resolvedA, resolvedB] = yield* Effect.all([
|
|
31
|
+
issueRepo
|
|
32
|
+
.getTaskByIdentifier(taskA)
|
|
33
|
+
.pipe(Effect.catchTag("TaskNotFoundError", () => issueRepo.getTask(taskA as TaskId))),
|
|
34
|
+
issueRepo
|
|
35
|
+
.getTaskByIdentifier(taskB)
|
|
36
|
+
.pipe(Effect.catchTag("TaskNotFoundError", () => issueRepo.getTask(taskB as TaskId))),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
yield* issueRepo.addRelated(resolvedA.id, resolvedB.id);
|
|
40
|
+
|
|
41
|
+
if (json) {
|
|
42
|
+
yield* Console.log(
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
status: "related",
|
|
45
|
+
taskA: resolvedA.identifier,
|
|
46
|
+
taskB: resolvedB.identifier,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
yield* Console.log(`Linked ${resolvedA.identifier} ↔ ${resolvedB.identifier} as related`);
|
|
51
|
+
yield* Console.log(
|
|
52
|
+
`\nMentioning ${resolvedA.identifier} in ${resolvedB.identifier}'s description (or vice versa) will now auto-link.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
@@ -37,7 +37,9 @@ export const startCommand = Command.make(
|
|
|
37
37
|
if (json) {
|
|
38
38
|
yield* Console.log(JSON.stringify({ status: "already_in_progress", task: taskId }));
|
|
39
39
|
} else {
|
|
40
|
-
yield* Console.log(
|
|
40
|
+
yield* Console.log(
|
|
41
|
+
`Task ${task.identifier} is already in progress (${task.state.name}).`,
|
|
42
|
+
);
|
|
41
43
|
}
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
@@ -40,14 +40,15 @@ export const teamCommand = Command.make("team", {}, () =>
|
|
|
40
40
|
|
|
41
41
|
// Select team or create new
|
|
42
42
|
const currentTeamId = Option.isSome(partial.linear) ? partial.linear.value.teamId : null;
|
|
43
|
-
const teamOptions: Array<{ value: TeamId | typeof CREATE_NEW; label: string; hint?: string }> =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
const teamOptions: Array<{ value: TeamId | typeof CREATE_NEW; label: string; hint?: string }> =
|
|
44
|
+
[
|
|
45
|
+
...teams.map((t) =>
|
|
46
|
+
currentTeamId === t.id
|
|
47
|
+
? { value: t.id, label: `${t.key} - ${t.name}`, hint: "current" as const }
|
|
48
|
+
: { value: t.id, label: `${t.key} - ${t.name}` },
|
|
49
|
+
),
|
|
50
|
+
{ value: CREATE_NEW, label: "Create new team..." },
|
|
51
|
+
];
|
|
51
52
|
|
|
52
53
|
const teamChoice = yield* Effect.tryPromise({
|
|
53
54
|
try: () =>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as Command from "@effect/cli/Command";
|
|
2
|
+
import * as Args from "@effect/cli/Args";
|
|
3
|
+
import * as Options from "@effect/cli/Options";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Console from "effect/Console";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import { IssueRepository } from "../../../../ports/IssueRepository.js";
|
|
8
|
+
import {
|
|
9
|
+
UpdateTaskInput,
|
|
10
|
+
type TaskId,
|
|
11
|
+
type Priority,
|
|
12
|
+
type TaskStatus,
|
|
13
|
+
} from "../../../../domain/Task.js";
|
|
14
|
+
|
|
15
|
+
const taskIdArg = Args.text({ name: "task-id" }).pipe(
|
|
16
|
+
Args.withDescription("Task identifier (e.g., ENG-123)"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const titleOption = Options.text("title").pipe(
|
|
20
|
+
Options.withAlias("t"),
|
|
21
|
+
Options.withDescription("New task title"),
|
|
22
|
+
Options.optional,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const descriptionOption = Options.text("description").pipe(
|
|
26
|
+
Options.withAlias("d"),
|
|
27
|
+
Options.withDescription("New task description (use - to read from stdin)"),
|
|
28
|
+
Options.optional,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const priorityOption = Options.choice("priority", ["urgent", "high", "medium", "low", "none"]).pipe(
|
|
32
|
+
Options.withAlias("p"),
|
|
33
|
+
Options.withDescription("New task priority"),
|
|
34
|
+
Options.optional,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const statusOption = Options.choice("status", [
|
|
38
|
+
"backlog",
|
|
39
|
+
"todo",
|
|
40
|
+
"in_progress",
|
|
41
|
+
"in_review",
|
|
42
|
+
"done",
|
|
43
|
+
"cancelled",
|
|
44
|
+
]).pipe(Options.withAlias("s"), Options.withDescription("New task status"), Options.optional);
|
|
45
|
+
|
|
46
|
+
const jsonOption = Options.boolean("json").pipe(
|
|
47
|
+
Options.withDescription("Output as JSON"),
|
|
48
|
+
Options.withDefault(false),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const updateCommand = Command.make(
|
|
52
|
+
"update",
|
|
53
|
+
{
|
|
54
|
+
taskId: taskIdArg,
|
|
55
|
+
title: titleOption,
|
|
56
|
+
description: descriptionOption,
|
|
57
|
+
priority: priorityOption,
|
|
58
|
+
status: statusOption,
|
|
59
|
+
json: jsonOption,
|
|
60
|
+
},
|
|
61
|
+
({ taskId, title, description, priority, status, json }) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const issueRepo = yield* IssueRepository;
|
|
64
|
+
|
|
65
|
+
// Check if any update was provided
|
|
66
|
+
const hasUpdate =
|
|
67
|
+
Option.isSome(title) ||
|
|
68
|
+
Option.isSome(description) ||
|
|
69
|
+
Option.isSome(priority) ||
|
|
70
|
+
Option.isSome(status);
|
|
71
|
+
|
|
72
|
+
if (!hasUpdate) {
|
|
73
|
+
yield* Console.error(
|
|
74
|
+
"No updates provided. Use --title, --description, --priority, or --status.",
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get the task first to resolve identifier to ID
|
|
80
|
+
const existingTask = yield* issueRepo
|
|
81
|
+
.getTaskByIdentifier(taskId)
|
|
82
|
+
.pipe(Effect.catchTag("TaskNotFoundError", () => issueRepo.getTask(taskId as TaskId)));
|
|
83
|
+
|
|
84
|
+
// Build update input
|
|
85
|
+
const input = new UpdateTaskInput({
|
|
86
|
+
title: Option.isSome(title) ? Option.some(title.value) : Option.none(),
|
|
87
|
+
description: Option.isSome(description) ? Option.some(description.value) : Option.none(),
|
|
88
|
+
priority: Option.isSome(priority) ? Option.some(priority.value as Priority) : Option.none(),
|
|
89
|
+
status: Option.isSome(status) ? Option.some(status.value as TaskStatus) : Option.none(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const task = yield* issueRepo.updateTask(existingTask.id, input);
|
|
93
|
+
|
|
94
|
+
if (json) {
|
|
95
|
+
const output = {
|
|
96
|
+
status: "updated",
|
|
97
|
+
task: {
|
|
98
|
+
id: task.id,
|
|
99
|
+
identifier: task.identifier,
|
|
100
|
+
title: task.title,
|
|
101
|
+
description: Option.getOrNull(task.description),
|
|
102
|
+
priority: task.priority,
|
|
103
|
+
state: task.state.name,
|
|
104
|
+
url: task.url,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
yield* Console.log(JSON.stringify(output, null, 2));
|
|
108
|
+
} else {
|
|
109
|
+
yield* Console.log(`Updated: ${task.identifier} - ${task.title}`);
|
|
110
|
+
if (Option.isSome(title)) {
|
|
111
|
+
yield* Console.log(`Title: ${task.title}`);
|
|
112
|
+
}
|
|
113
|
+
if (Option.isSome(description)) {
|
|
114
|
+
yield* Console.log(`Description updated`);
|
|
115
|
+
}
|
|
116
|
+
if (Option.isSome(priority)) {
|
|
117
|
+
yield* Console.log(`Priority: ${task.priority}`);
|
|
118
|
+
}
|
|
119
|
+
if (Option.isSome(status)) {
|
|
120
|
+
yield* Console.log(`Status: ${task.state.name}`);
|
|
121
|
+
}
|
|
122
|
+
yield* Console.log(`URL: ${task.url}`);
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
@@ -14,6 +14,8 @@ import { blockCommand } from "./commands/block.js";
|
|
|
14
14
|
import { unblockCommand } from "./commands/unblock.js";
|
|
15
15
|
import { blockedCommand } from "./commands/blocked.js";
|
|
16
16
|
import { primeCommand } from "./commands/prime.js";
|
|
17
|
+
import { updateCommand } from "./commands/update.js";
|
|
18
|
+
import { relateCommand } from "./commands/relate.js";
|
|
17
19
|
|
|
18
20
|
// Root command
|
|
19
21
|
const ship = Command.make("ship", {}, () =>
|
|
@@ -35,9 +37,11 @@ Commands:
|
|
|
35
37
|
|
|
36
38
|
start <id> Start working on a task
|
|
37
39
|
done <id> Mark task as complete
|
|
40
|
+
update <id> Update task details
|
|
38
41
|
|
|
39
42
|
block <a> <b> Mark task A as blocking task B
|
|
40
43
|
unblock <a> <b> Remove blocking relationship
|
|
44
|
+
relate <a> <b> Link two tasks as related
|
|
41
45
|
|
|
42
46
|
prime Output AI-optimized context
|
|
43
47
|
|
|
@@ -56,9 +60,11 @@ export const command = ship.pipe(
|
|
|
56
60
|
showCommand,
|
|
57
61
|
startCommand,
|
|
58
62
|
doneCommand,
|
|
63
|
+
updateCommand,
|
|
59
64
|
createCommand,
|
|
60
65
|
blockCommand,
|
|
61
66
|
unblockCommand,
|
|
67
|
+
relateCommand,
|
|
62
68
|
blockedCommand,
|
|
63
69
|
primeCommand,
|
|
64
70
|
]),
|
|
@@ -62,6 +62,12 @@ export interface IssueRepository {
|
|
|
62
62
|
blockerId: TaskId,
|
|
63
63
|
) => Effect.Effect<void, TaskNotFoundError | TaskError | LinearApiError>;
|
|
64
64
|
|
|
65
|
+
/** Add a "related" relationship between tasks (enables auto-linking in Linear) */
|
|
66
|
+
readonly addRelated: (
|
|
67
|
+
taskId: TaskId,
|
|
68
|
+
relatedTaskId: TaskId,
|
|
69
|
+
) => Effect.Effect<void, TaskNotFoundError | TaskError | LinearApiError>;
|
|
70
|
+
|
|
65
71
|
/** Get the suggested branch name for a task */
|
|
66
72
|
readonly getBranchName: (id: TaskId) => Effect.Effect<string, TaskNotFoundError | LinearApiError>;
|
|
67
73
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"fileNames":[],"fileInfos":[],"root":[],"options":{"allowJs":false,"checkJs":false,"composite":true,"declaration":true,"declarationMap":true,"downlevelIteration":true,"emitDecoratorMetadata":false,"esModuleInterop":false,"exactOptionalPropertyTypes":true,"experimentalDecorators":true,"module":199,"noEmitOnError":false,"noErrorTruncation":false,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitReturns":false,"noImplicitThis":true,"noUncheckedIndexedAccess":false,"noUnusedLocals":true,"noUnusedParameters":false,"removeComments":false,"skipLibCheck":true,"sourceMap":true,"strict":true,"strictNullChecks":true,"stripInternal":true,"target":9},"version":"5.6.3"}
|