@ship-cli/core 0.0.2 → 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/package.json +1 -1
- package/src/adapters/driven/linear/IssueRepositoryLive.ts +29 -0
- package/src/adapters/driving/cli/commands/prime.ts +42 -2
- package/src/adapters/driving/cli/commands/relate.ts +56 -0
- 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/package.json
CHANGED
|
@@ -453,6 +453,34 @@ const make = Effect.gen(function* () {
|
|
|
453
453
|
"Removing blocker",
|
|
454
454
|
);
|
|
455
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
|
+
|
|
456
484
|
const getBranchName = (id: TaskId): Effect.Effect<string, TaskNotFoundError | LinearApiError> =>
|
|
457
485
|
withRetryAndTimeout(
|
|
458
486
|
Effect.gen(function* () {
|
|
@@ -492,6 +520,7 @@ const make = Effect.gen(function* () {
|
|
|
492
520
|
getBlockedTasks,
|
|
493
521
|
addBlocker,
|
|
494
522
|
removeBlocker,
|
|
523
|
+
addRelated,
|
|
495
524
|
getBranchName,
|
|
496
525
|
};
|
|
497
526
|
});
|
|
@@ -11,6 +11,39 @@ const formatTaskCompact = (task: Task): string => {
|
|
|
11
11
|
return `${priority}${task.identifier}: ${task.title} [${task.state.name}]`;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
const formatTaskFull = (task: Task): string[] => {
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
const priority = task.priority === "urgent" ? "!" : task.priority === "high" ? "^" : "";
|
|
17
|
+
|
|
18
|
+
lines.push(`### ${priority}${task.identifier}: ${task.title}`);
|
|
19
|
+
lines.push(`**Status:** ${task.state.name} | **Priority:** ${task.priority}`);
|
|
20
|
+
|
|
21
|
+
if (task.labels.length > 0) {
|
|
22
|
+
lines.push(`**Labels:** ${task.labels.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Option.isSome(task.branchName)) {
|
|
26
|
+
lines.push(`**Branch:** \`${task.branchName.value}\``);
|
|
27
|
+
}
|
|
28
|
+
|
|
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;
|
|
45
|
+
};
|
|
46
|
+
|
|
14
47
|
export const primeCommand = Command.make("prime", {}, () =>
|
|
15
48
|
Effect.gen(function* () {
|
|
16
49
|
const config = yield* ConfigRepository;
|
|
@@ -45,14 +78,20 @@ export const primeCommand = Command.make("prime", {}, () =>
|
|
|
45
78
|
lines.push(`Project: ${cfg.linear.projectId.value}`);
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
// Show in-progress tasks with FULL details (description, acceptance criteria, etc.)
|
|
48
82
|
if (inProgressTasks.length > 0) {
|
|
49
83
|
lines.push("");
|
|
50
|
-
lines.push("## In Progress");
|
|
84
|
+
lines.push("## Current Work (In Progress)");
|
|
85
|
+
lines.push("");
|
|
51
86
|
for (const task of inProgressTasks) {
|
|
52
|
-
lines.push(
|
|
87
|
+
lines.push(...formatTaskFull(task));
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("---");
|
|
90
|
+
lines.push("");
|
|
53
91
|
}
|
|
54
92
|
}
|
|
55
93
|
|
|
94
|
+
// Show ready tasks with compact format (just titles)
|
|
56
95
|
if (readyTasks.length > 0) {
|
|
57
96
|
lines.push("");
|
|
58
97
|
lines.push("## Ready to Work");
|
|
@@ -64,6 +103,7 @@ export const primeCommand = Command.make("prime", {}, () =>
|
|
|
64
103
|
}
|
|
65
104
|
}
|
|
66
105
|
|
|
106
|
+
// Show blocked tasks with blockers info
|
|
67
107
|
if (blockedTasks.length > 0) {
|
|
68
108
|
lines.push("");
|
|
69
109
|
lines.push("## Blocked");
|
|
@@ -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
|
+
);
|
|
@@ -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"}
|