@ship-cli/core 0.0.3 → 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.
Files changed (52) hide show
  1. package/README.md +90 -0
  2. package/dist/bin.js +43263 -30230
  3. package/package.json +47 -23
  4. package/.tsbuildinfo/src.tsbuildinfo +0 -1
  5. package/.tsbuildinfo/test.tsbuildinfo +0 -1
  6. package/LICENSE +0 -21
  7. package/src/adapters/driven/auth/AuthServiceLive.ts +0 -125
  8. package/src/adapters/driven/config/ConfigRepositoryLive.ts +0 -366
  9. package/src/adapters/driven/linear/IssueRepositoryLive.ts +0 -528
  10. package/src/adapters/driven/linear/LinearClient.ts +0 -33
  11. package/src/adapters/driven/linear/Mapper.ts +0 -142
  12. package/src/adapters/driven/linear/ProjectRepositoryLive.ts +0 -98
  13. package/src/adapters/driven/linear/TeamRepositoryLive.ts +0 -101
  14. package/src/adapters/driving/cli/commands/block.ts +0 -63
  15. package/src/adapters/driving/cli/commands/blocked.ts +0 -61
  16. package/src/adapters/driving/cli/commands/create.ts +0 -83
  17. package/src/adapters/driving/cli/commands/done.ts +0 -82
  18. package/src/adapters/driving/cli/commands/init.ts +0 -194
  19. package/src/adapters/driving/cli/commands/list.ts +0 -87
  20. package/src/adapters/driving/cli/commands/login.ts +0 -46
  21. package/src/adapters/driving/cli/commands/prime.ts +0 -123
  22. package/src/adapters/driving/cli/commands/project.ts +0 -155
  23. package/src/adapters/driving/cli/commands/ready.ts +0 -73
  24. package/src/adapters/driving/cli/commands/relate.ts +0 -56
  25. package/src/adapters/driving/cli/commands/show.ts +0 -94
  26. package/src/adapters/driving/cli/commands/start.ts +0 -101
  27. package/src/adapters/driving/cli/commands/team.ts +0 -135
  28. package/src/adapters/driving/cli/commands/unblock.ts +0 -63
  29. package/src/adapters/driving/cli/commands/update.ts +0 -125
  30. package/src/adapters/driving/cli/main.ts +0 -76
  31. package/src/bin.ts +0 -12
  32. package/src/domain/Config.ts +0 -42
  33. package/src/domain/Errors.ts +0 -89
  34. package/src/domain/Task.ts +0 -124
  35. package/src/domain/index.ts +0 -3
  36. package/src/infrastructure/Layers.ts +0 -45
  37. package/src/ports/AuthService.ts +0 -19
  38. package/src/ports/ConfigRepository.ts +0 -20
  39. package/src/ports/IssueRepository.ts +0 -75
  40. package/src/ports/PrService.ts +0 -52
  41. package/src/ports/ProjectRepository.ts +0 -19
  42. package/src/ports/TeamRepository.ts +0 -17
  43. package/src/ports/VcsService.ts +0 -87
  44. package/src/ports/index.ts +0 -7
  45. package/test/Dummy.test.ts +0 -7
  46. package/tsconfig.base.json +0 -45
  47. package/tsconfig.json +0 -7
  48. package/tsconfig.src.json +0 -11
  49. package/tsconfig.test.json +0 -10
  50. package/tsconfig.tsbuildinfo +0 -1
  51. package/tsup.config.ts +0 -14
  52. package/vitest.config.ts +0 -12
@@ -1,87 +0,0 @@
1
- import * as Command from "@effect/cli/Command";
2
- import * as Options from "@effect/cli/Options";
3
- import * as Effect from "effect/Effect";
4
- import * as Console from "effect/Console";
5
- import * as Option from "effect/Option";
6
- import { ConfigRepository } from "../../../../ports/ConfigRepository.js";
7
- import { IssueRepository } from "../../../../ports/IssueRepository.js";
8
- import { TaskFilter, type TaskStatus, type Priority, type Task } from "../../../../domain/Task.js";
9
-
10
- const jsonOption = Options.boolean("json").pipe(
11
- Options.withDescription("Output as JSON"),
12
- Options.withDefault(false),
13
- );
14
-
15
- const statusOption = Options.choice("status", [
16
- "backlog",
17
- "todo",
18
- "in_progress",
19
- "in_review",
20
- "done",
21
- "cancelled",
22
- ]).pipe(Options.withAlias("s"), Options.withDescription("Filter by status"), Options.optional);
23
-
24
- const priorityOption = Options.choice("priority", ["urgent", "high", "medium", "low", "none"]).pipe(
25
- Options.withAlias("P"),
26
- Options.withDescription("Filter by priority"),
27
- Options.optional,
28
- );
29
-
30
- const mineOption = Options.boolean("mine").pipe(
31
- Options.withAlias("m"),
32
- Options.withDescription("Show only tasks assigned to me"),
33
- Options.withDefault(false),
34
- );
35
-
36
- const formatTask = (task: Task): string => {
37
- const priority = task.priority === "urgent" ? "[!]" : task.priority === "high" ? "[^]" : " ";
38
- const stateName = task.state.name.padEnd(11);
39
- return `${priority} ${task.identifier.padEnd(10)} ${stateName} ${task.title}`;
40
- };
41
-
42
- export const listCommand = Command.make(
43
- "list",
44
- { json: jsonOption, status: statusOption, priority: priorityOption, mine: mineOption },
45
- ({ json, status, priority, mine }) =>
46
- Effect.gen(function* () {
47
- const config = yield* ConfigRepository;
48
- const issueRepo = yield* IssueRepository;
49
-
50
- const cfg = yield* config.load();
51
-
52
- const filter = new TaskFilter({
53
- status: Option.isSome(status) ? Option.some(status.value as TaskStatus) : Option.none(),
54
- priority: Option.isSome(priority) ? Option.some(priority.value as Priority) : Option.none(),
55
- projectId: cfg.linear.projectId,
56
- assignedToMe: mine,
57
- });
58
-
59
- const taskList = yield* issueRepo.listTasks(cfg.linear.teamId, filter);
60
-
61
- if (json) {
62
- const output = taskList.map((t) => ({
63
- id: t.id,
64
- identifier: t.identifier,
65
- title: t.title,
66
- priority: t.priority,
67
- state: t.state.name,
68
- stateType: t.state.type,
69
- labels: t.labels,
70
- url: t.url,
71
- branchName: Option.getOrNull(t.branchName),
72
- }));
73
- yield* Console.log(JSON.stringify(output, null, 2));
74
- } else {
75
- if (taskList.length === 0) {
76
- yield* Console.log("No tasks found matching the filter.");
77
- } else {
78
- yield* Console.log(`Found ${taskList.length} task(s):\n`);
79
- yield* Console.log("PRI IDENTIFIER STATUS TITLE");
80
- yield* Console.log("─".repeat(60));
81
- for (const task of taskList) {
82
- yield* Console.log(formatTask(task));
83
- }
84
- }
85
- }
86
- }),
87
- );
@@ -1,46 +0,0 @@
1
- import * as Command from "@effect/cli/Command";
2
- import * as Effect from "effect/Effect";
3
- import * as clack from "@clack/prompts";
4
- import { AuthService } from "../../../../ports/AuthService.js";
5
-
6
- export const loginCommand = Command.make("login", {}, () =>
7
- Effect.gen(function* () {
8
- const auth = yield* AuthService;
9
-
10
- clack.intro("ship login");
11
-
12
- clack.note(
13
- "Create a personal API key at:\nhttps://linear.app/settings/api",
14
- "Linear Authentication",
15
- );
16
-
17
- const apiKey = yield* Effect.tryPromise({
18
- try: () =>
19
- clack.text({
20
- message: "Paste your API key",
21
- placeholder: "lin_api_...",
22
- validate: (value) => {
23
- if (!value) return "API key is required";
24
- if (!value.startsWith("lin_api_")) return "API key should start with lin_api_";
25
- },
26
- }),
27
- catch: () => new Error("Prompt cancelled"),
28
- });
29
-
30
- if (clack.isCancel(apiKey)) {
31
- clack.cancel("Login cancelled");
32
- return;
33
- }
34
-
35
- const spinner = clack.spinner();
36
- spinner.start("Validating API key...");
37
-
38
- yield* auth
39
- .saveApiKey(apiKey as string)
40
- .pipe(Effect.tapError(() => Effect.sync(() => spinner.stop("Invalid API key"))));
41
-
42
- spinner.stop("API key validated");
43
-
44
- clack.outro("Run 'ship init' to select your team and project.");
45
- }),
46
- );
@@ -1,123 +0,0 @@
1
- import * as Command from "@effect/cli/Command";
2
- import * as Effect from "effect/Effect";
3
- import * as Console from "effect/Console";
4
- import * as Option from "effect/Option";
5
- import { ConfigRepository } from "../../../../ports/ConfigRepository.js";
6
- import { IssueRepository } from "../../../../ports/IssueRepository.js";
7
- import { TaskFilter, type Task } from "../../../../domain/Task.js";
8
-
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
- };
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
-
47
- export const primeCommand = Command.make("prime", {}, () =>
48
- Effect.gen(function* () {
49
- const config = yield* ConfigRepository;
50
- const issueRepo = yield* IssueRepository;
51
-
52
- const cfg = yield* config.load();
53
- const projectId = Option.getOrUndefined(cfg.linear.projectId);
54
-
55
- // Get ready and blocked tasks
56
- const [readyTasks, blockedTasks] = yield* Effect.all([
57
- issueRepo.getReadyTasks(cfg.linear.teamId, projectId),
58
- issueRepo.getBlockedTasks(cfg.linear.teamId, projectId),
59
- ]);
60
-
61
- // Get in-progress tasks (filter by "started" state type)
62
- const filter = new TaskFilter({
63
- status: Option.some("in_progress"),
64
- priority: Option.none(),
65
- projectId: cfg.linear.projectId,
66
- assignedToMe: false,
67
- });
68
- const allTasks = yield* issueRepo.listTasks(cfg.linear.teamId, filter);
69
-
70
- // Filter to only "started" state type tasks
71
- const inProgressTasks = allTasks.filter((t: Task) => t.state.type === "started");
72
-
73
- // Build context output (plain markdown, no XML tags - plugin wraps it)
74
- const lines: string[] = [];
75
-
76
- lines.push(`Team: ${cfg.linear.teamKey}`);
77
- if (Option.isSome(cfg.linear.projectId)) {
78
- lines.push(`Project: ${cfg.linear.projectId.value}`);
79
- }
80
-
81
- // Show in-progress tasks with FULL details (description, acceptance criteria, etc.)
82
- if (inProgressTasks.length > 0) {
83
- lines.push("");
84
- lines.push("## Current Work (In Progress)");
85
- lines.push("");
86
- for (const task of inProgressTasks) {
87
- lines.push(...formatTaskFull(task));
88
- lines.push("");
89
- lines.push("---");
90
- lines.push("");
91
- }
92
- }
93
-
94
- // Show ready tasks with compact format (just titles)
95
- if (readyTasks.length > 0) {
96
- lines.push("");
97
- lines.push("## Ready to Work");
98
- for (const task of readyTasks.slice(0, 10)) {
99
- lines.push(`- ${formatTaskCompact(task)}`);
100
- }
101
- if (readyTasks.length > 10) {
102
- lines.push(` ... and ${readyTasks.length - 10} more`);
103
- }
104
- }
105
-
106
- // Show blocked tasks with blockers info
107
- if (blockedTasks.length > 0) {
108
- lines.push("");
109
- lines.push("## Blocked");
110
- for (const task of blockedTasks.slice(0, 5)) {
111
- lines.push(`- ${formatTaskCompact(task)}`);
112
- if (task.blockedBy.length > 0) {
113
- lines.push(` Blocked by: ${task.blockedBy.join(", ")}`);
114
- }
115
- }
116
- if (blockedTasks.length > 5) {
117
- lines.push(` ... and ${blockedTasks.length - 5} more`);
118
- }
119
- }
120
-
121
- yield* Console.log(lines.join("\n"));
122
- }),
123
- );
@@ -1,155 +0,0 @@
1
- import * as Command from "@effect/cli/Command";
2
- import * as Effect from "effect/Effect";
3
- import * as Option from "effect/Option";
4
- import * as clack from "@clack/prompts";
5
- import { ConfigRepository } from "../../../../ports/ConfigRepository.js";
6
- import { AuthService } from "../../../../ports/AuthService.js";
7
- import { ProjectRepository } from "../../../../ports/ProjectRepository.js";
8
- import { LinearConfig } from "../../../../domain/Config.js";
9
- import type { Project, ProjectId } from "../../../../domain/Task.js";
10
-
11
- const CREATE_NEW = "__create_new__" as const;
12
- const NO_PROJECT = null;
13
-
14
- export const projectCommand = Command.make("project", {}, () =>
15
- Effect.gen(function* () {
16
- const config = yield* ConfigRepository;
17
- const auth = yield* AuthService;
18
- const projectRepo = yield* ProjectRepository;
19
-
20
- clack.intro("ship project");
21
-
22
- // Check authentication
23
- const isAuth = yield* auth.isAuthenticated();
24
- if (!isAuth) {
25
- clack.log.error("Not authenticated. Run 'ship login' first.");
26
- clack.outro("Setup required");
27
- return;
28
- }
29
-
30
- // Check team is configured
31
- const partial = yield* config.loadPartial();
32
- if (Option.isNone(partial.linear)) {
33
- clack.log.error("No team configured. Run 'ship init' first.");
34
- clack.outro("Setup required");
35
- return;
36
- }
37
-
38
- const currentConfig = partial.linear.value;
39
- clack.log.info(`Team: ${currentConfig.teamKey}`);
40
-
41
- if (Option.isSome(currentConfig.projectId)) {
42
- clack.log.info(`Current project: ${currentConfig.projectId.value}`);
43
- }
44
-
45
- // Fetch projects
46
- const spinner = clack.spinner();
47
- spinner.start("Fetching projects...");
48
- const projects = yield* projectRepo.getProjects(currentConfig.teamId);
49
- spinner.stop("Projects loaded");
50
-
51
- // Select project or create new
52
- const currentProjectId = Option.isSome(currentConfig.projectId)
53
- ? currentConfig.projectId.value
54
- : null;
55
-
56
- const projectOptions: Array<{
57
- value: ProjectId | typeof CREATE_NEW | typeof NO_PROJECT;
58
- label: string;
59
- hint?: string;
60
- }> = [
61
- { value: NO_PROJECT, label: "No project filter", hint: "show all team tasks" },
62
- ...projects.map((p) =>
63
- currentProjectId === p.id
64
- ? { value: p.id, label: p.name, hint: "current" as const }
65
- : { value: p.id, label: p.name },
66
- ),
67
- { value: CREATE_NEW, label: "Create new project..." },
68
- ];
69
-
70
- const projectChoice = yield* Effect.tryPromise({
71
- try: () =>
72
- clack.select({
73
- message: "Select a project",
74
- options: projectOptions,
75
- }),
76
- catch: () => new Error("Prompt cancelled"),
77
- });
78
-
79
- if (clack.isCancel(projectChoice)) {
80
- clack.cancel("Cancelled");
81
- return;
82
- }
83
-
84
- let selectedProject: Project | undefined;
85
-
86
- if (projectChoice === CREATE_NEW) {
87
- // Create new project
88
- const projectName = yield* Effect.tryPromise({
89
- try: () =>
90
- clack.text({
91
- message: "Project name",
92
- placeholder: "My Project",
93
- validate: (v) => (!v ? "Name is required" : undefined),
94
- }),
95
- catch: () => new Error("Prompt cancelled"),
96
- });
97
-
98
- if (clack.isCancel(projectName)) {
99
- clack.cancel("Cancelled");
100
- return;
101
- }
102
-
103
- const projectDesc = yield* Effect.tryPromise({
104
- try: () =>
105
- clack.text({
106
- message: "Description (optional)",
107
- placeholder: "A brief description of the project",
108
- }),
109
- catch: () => new Error("Prompt cancelled"),
110
- });
111
-
112
- if (clack.isCancel(projectDesc)) {
113
- clack.cancel("Cancelled");
114
- return;
115
- }
116
-
117
- const createSpinner = clack.spinner();
118
- createSpinner.start("Creating project...");
119
-
120
- const createInput = { name: projectName as string } as { name: string; description?: string };
121
- if (projectDesc) {
122
- createInput.description = projectDesc as string;
123
- }
124
-
125
- selectedProject = yield* projectRepo.createProject(currentConfig.teamId, createInput);
126
-
127
- createSpinner.stop(`Created project: ${selectedProject.name}`);
128
- } else if (projectChoice !== NO_PROJECT) {
129
- const found = projects.find((p) => p.id === projectChoice);
130
- if (!found) {
131
- clack.log.error("Selected project not found. Please try again.");
132
- clack.outro("Error");
133
- return;
134
- }
135
- selectedProject = found;
136
- }
137
-
138
- // Save updated config
139
- const linearConfig = new LinearConfig({
140
- teamId: currentConfig.teamId,
141
- teamKey: currentConfig.teamKey,
142
- projectId: selectedProject ? Option.some(selectedProject.id) : Option.none(),
143
- });
144
-
145
- yield* config.saveLinear(linearConfig);
146
-
147
- if (selectedProject) {
148
- clack.log.success(`Switched to project: ${selectedProject.name}`);
149
- } else {
150
- clack.log.success("Cleared project filter");
151
- }
152
-
153
- clack.outro("Run 'ship ready' to see available tasks.");
154
- }),
155
- );
@@ -1,73 +0,0 @@
1
- import * as Command from "@effect/cli/Command";
2
- import * as Options from "@effect/cli/Options";
3
- import * as Effect from "effect/Effect";
4
- import * as Console from "effect/Console";
5
- import * as Option from "effect/Option";
6
- import { ConfigRepository } from "../../../../ports/ConfigRepository.js";
7
- import { IssueRepository } from "../../../../ports/IssueRepository.js";
8
- import type { Task } from "../../../../domain/Task.js";
9
-
10
- const jsonOption = Options.boolean("json").pipe(
11
- Options.withDescription("Output as JSON"),
12
- Options.withDefault(false),
13
- );
14
-
15
- const formatTaskDetailed = (task: Task): string[] => {
16
- const lines: string[] = [];
17
- const priority =
18
- task.priority === "urgent"
19
- ? "URGENT"
20
- : task.priority === "high"
21
- ? "HIGH"
22
- : task.priority === "medium"
23
- ? "MEDIUM"
24
- : task.priority === "low"
25
- ? "LOW"
26
- : "";
27
-
28
- lines.push(`${task.identifier}: ${task.title}`);
29
- if (priority) lines.push(` Priority: ${priority}`);
30
- if (task.labels.length > 0) lines.push(` Labels: ${task.labels.join(", ")}`);
31
- if (Option.isSome(task.branchName)) lines.push(` Branch: ${task.branchName.value}`);
32
- lines.push(` URL: ${task.url}`);
33
- return lines;
34
- };
35
-
36
- export const readyCommand = Command.make("ready", { json: jsonOption }, ({ json }) =>
37
- Effect.gen(function* () {
38
- const config = yield* ConfigRepository;
39
- const issueRepo = yield* IssueRepository;
40
-
41
- const cfg = yield* config.load();
42
- const projectId = Option.getOrUndefined(cfg.linear.projectId);
43
- const readyTasks = yield* issueRepo.getReadyTasks(cfg.linear.teamId, projectId);
44
-
45
- if (json) {
46
- const output = readyTasks.map((t) => ({
47
- id: t.id,
48
- identifier: t.identifier,
49
- title: t.title,
50
- priority: t.priority,
51
- state: t.state.name,
52
- stateType: t.state.type,
53
- labels: t.labels,
54
- url: t.url,
55
- branchName: Option.getOrNull(t.branchName),
56
- }));
57
- yield* Console.log(JSON.stringify(output, null, 2));
58
- } else {
59
- if (readyTasks.length === 0) {
60
- yield* Console.log("No ready tasks found.");
61
- yield* Console.log("All tasks may be blocked or completed.");
62
- } else {
63
- yield* Console.log(`Found ${readyTasks.length} ready task(s):\n`);
64
- for (const task of readyTasks) {
65
- for (const line of formatTaskDetailed(task)) {
66
- yield* Console.log(line);
67
- }
68
- yield* Console.log("");
69
- }
70
- }
71
- }
72
- }),
73
- );
@@ -1,56 +0,0 @@
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
- );
@@ -1,94 +0,0 @@
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 type { Task, TaskId } from "../../../../domain/Task.js";
9
-
10
- const taskIdArg = Args.text({ name: "task-id" }).pipe(
11
- Args.withDescription("Task identifier (e.g., ENG-123)"),
12
- );
13
-
14
- const jsonOption = Options.boolean("json").pipe(
15
- Options.withDescription("Output as JSON"),
16
- Options.withDefault(false),
17
- );
18
-
19
- const formatTask = (task: Task): string[] => {
20
- const lines: string[] = [];
21
-
22
- lines.push(`${task.identifier}: ${task.title}`);
23
- lines.push("─".repeat(50));
24
- lines.push(`Status: ${task.state.name}`);
25
- lines.push(`Priority: ${task.priority}`);
26
-
27
- if (task.labels.length > 0) {
28
- lines.push(`Labels: ${task.labels.join(", ")}`);
29
- }
30
-
31
- if (Option.isSome(task.branchName)) {
32
- lines.push(`Branch: ${task.branchName.value}`);
33
- }
34
-
35
- lines.push(`URL: ${task.url}`);
36
-
37
- if (Option.isSome(task.description)) {
38
- lines.push("");
39
- lines.push("Description:");
40
- lines.push(task.description.value);
41
- }
42
-
43
- if (task.blockedBy.length > 0) {
44
- lines.push("");
45
- lines.push(`Blocked by: ${task.blockedBy.join(", ")}`);
46
- }
47
-
48
- if (task.blocks.length > 0) {
49
- lines.push(`Blocks: ${task.blocks.join(", ")}`);
50
- }
51
-
52
- return lines;
53
- };
54
-
55
- export const showCommand = Command.make(
56
- "show",
57
- { taskId: taskIdArg, json: jsonOption },
58
- ({ taskId, json }) =>
59
- Effect.gen(function* () {
60
- const issueRepo = yield* IssueRepository;
61
-
62
- // Try to get by identifier first (ENG-123 format)
63
- const task = yield* issueRepo.getTaskByIdentifier(taskId).pipe(
64
- Effect.catchTag("TaskNotFoundError", () =>
65
- // Fallback to ID lookup
66
- issueRepo.getTask(taskId as TaskId),
67
- ),
68
- );
69
-
70
- if (json) {
71
- const output = {
72
- id: task.id,
73
- identifier: task.identifier,
74
- title: task.title,
75
- description: Option.getOrNull(task.description),
76
- priority: task.priority,
77
- state: task.state.name,
78
- stateType: task.state.type,
79
- labels: task.labels,
80
- url: task.url,
81
- branchName: Option.getOrNull(task.branchName),
82
- blockedBy: task.blockedBy,
83
- blocks: task.blocks,
84
- createdAt: task.createdAt.toISOString(),
85
- updatedAt: task.updatedAt.toISOString(),
86
- };
87
- yield* Console.log(JSON.stringify(output, null, 2));
88
- } else {
89
- for (const line of formatTask(task)) {
90
- yield* Console.log(line);
91
- }
92
- }
93
- }),
94
- );