@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.
- package/README.md +90 -0
- package/dist/bin.js +43263 -30230
- package/package.json +47 -23
- package/.tsbuildinfo/src.tsbuildinfo +0 -1
- package/.tsbuildinfo/test.tsbuildinfo +0 -1
- package/LICENSE +0 -21
- package/src/adapters/driven/auth/AuthServiceLive.ts +0 -125
- package/src/adapters/driven/config/ConfigRepositoryLive.ts +0 -366
- package/src/adapters/driven/linear/IssueRepositoryLive.ts +0 -528
- package/src/adapters/driven/linear/LinearClient.ts +0 -33
- package/src/adapters/driven/linear/Mapper.ts +0 -142
- package/src/adapters/driven/linear/ProjectRepositoryLive.ts +0 -98
- package/src/adapters/driven/linear/TeamRepositoryLive.ts +0 -101
- package/src/adapters/driving/cli/commands/block.ts +0 -63
- package/src/adapters/driving/cli/commands/blocked.ts +0 -61
- package/src/adapters/driving/cli/commands/create.ts +0 -83
- package/src/adapters/driving/cli/commands/done.ts +0 -82
- package/src/adapters/driving/cli/commands/init.ts +0 -194
- package/src/adapters/driving/cli/commands/list.ts +0 -87
- package/src/adapters/driving/cli/commands/login.ts +0 -46
- package/src/adapters/driving/cli/commands/prime.ts +0 -123
- package/src/adapters/driving/cli/commands/project.ts +0 -155
- package/src/adapters/driving/cli/commands/ready.ts +0 -73
- package/src/adapters/driving/cli/commands/relate.ts +0 -56
- package/src/adapters/driving/cli/commands/show.ts +0 -94
- package/src/adapters/driving/cli/commands/start.ts +0 -101
- package/src/adapters/driving/cli/commands/team.ts +0 -135
- package/src/adapters/driving/cli/commands/unblock.ts +0 -63
- package/src/adapters/driving/cli/commands/update.ts +0 -125
- package/src/adapters/driving/cli/main.ts +0 -76
- package/src/bin.ts +0 -12
- package/src/domain/Config.ts +0 -42
- package/src/domain/Errors.ts +0 -89
- package/src/domain/Task.ts +0 -124
- package/src/domain/index.ts +0 -3
- package/src/infrastructure/Layers.ts +0 -45
- package/src/ports/AuthService.ts +0 -19
- package/src/ports/ConfigRepository.ts +0 -20
- package/src/ports/IssueRepository.ts +0 -75
- package/src/ports/PrService.ts +0 -52
- package/src/ports/ProjectRepository.ts +0 -19
- package/src/ports/TeamRepository.ts +0 -17
- package/src/ports/VcsService.ts +0 -87
- package/src/ports/index.ts +0 -7
- package/test/Dummy.test.ts +0 -7
- package/tsconfig.base.json +0 -45
- package/tsconfig.json +0 -7
- package/tsconfig.src.json +0 -11
- package/tsconfig.test.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
- package/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -12
|
@@ -1,101 +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 { ConfigRepository } from "../../../../ports/ConfigRepository.js";
|
|
8
|
-
import { IssueRepository } from "../../../../ports/IssueRepository.js";
|
|
9
|
-
import { UpdateTaskInput, type TaskId } from "../../../../domain/Task.js";
|
|
10
|
-
|
|
11
|
-
const taskIdArg = Args.text({ name: "task-id" }).pipe(
|
|
12
|
-
Args.withDescription("Task identifier (e.g., ENG-123)"),
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
const jsonOption = Options.boolean("json").pipe(
|
|
16
|
-
Options.withDescription("Output as JSON"),
|
|
17
|
-
Options.withDefault(false),
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
export const startCommand = Command.make(
|
|
21
|
-
"start",
|
|
22
|
-
{ taskId: taskIdArg, json: jsonOption },
|
|
23
|
-
({ taskId, json }) =>
|
|
24
|
-
Effect.gen(function* () {
|
|
25
|
-
const config = yield* ConfigRepository;
|
|
26
|
-
const issueRepo = yield* IssueRepository;
|
|
27
|
-
|
|
28
|
-
yield* config.load(); // Ensure initialized
|
|
29
|
-
|
|
30
|
-
// Get the task
|
|
31
|
-
const task = yield* issueRepo
|
|
32
|
-
.getTaskByIdentifier(taskId)
|
|
33
|
-
.pipe(Effect.catchTag("TaskNotFoundError", () => issueRepo.getTask(taskId as TaskId)));
|
|
34
|
-
|
|
35
|
-
// Check if already in progress (Linear's "started" state type)
|
|
36
|
-
if (task.state.type === "started") {
|
|
37
|
-
if (json) {
|
|
38
|
-
yield* Console.log(JSON.stringify({ status: "already_in_progress", task: taskId }));
|
|
39
|
-
} else {
|
|
40
|
-
yield* Console.log(
|
|
41
|
-
`Task ${task.identifier} is already in progress (${task.state.name}).`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check if blocked
|
|
48
|
-
if (task.blockedBy.length > 0) {
|
|
49
|
-
if (json) {
|
|
50
|
-
yield* Console.log(
|
|
51
|
-
JSON.stringify({
|
|
52
|
-
status: "blocked",
|
|
53
|
-
task: taskId,
|
|
54
|
-
blockedBy: task.blockedBy,
|
|
55
|
-
}),
|
|
56
|
-
);
|
|
57
|
-
} else {
|
|
58
|
-
yield* Console.log(
|
|
59
|
-
`Warning: Task ${task.identifier} is blocked by: ${task.blockedBy.join(", ")}`,
|
|
60
|
-
);
|
|
61
|
-
yield* Console.log("Consider working on the blocking tasks first.");
|
|
62
|
-
}
|
|
63
|
-
// Continue anyway - user might want to start despite blockers
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Update status to in_progress
|
|
67
|
-
const updateInput = new UpdateTaskInput({
|
|
68
|
-
title: Option.none(),
|
|
69
|
-
description: Option.none(),
|
|
70
|
-
status: Option.some("in_progress"),
|
|
71
|
-
priority: Option.none(),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const updatedTask = yield* issueRepo.updateTask(task.id, updateInput);
|
|
75
|
-
|
|
76
|
-
// Get branch name
|
|
77
|
-
const branchName = yield* issueRepo.getBranchName(task.id);
|
|
78
|
-
|
|
79
|
-
if (json) {
|
|
80
|
-
yield* Console.log(
|
|
81
|
-
JSON.stringify({
|
|
82
|
-
status: "started",
|
|
83
|
-
task: {
|
|
84
|
-
id: updatedTask.id,
|
|
85
|
-
identifier: updatedTask.identifier,
|
|
86
|
-
title: updatedTask.title,
|
|
87
|
-
state: updatedTask.state.name,
|
|
88
|
-
branchName,
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
92
|
-
} else {
|
|
93
|
-
yield* Console.log(`Started: ${updatedTask.identifier} - ${updatedTask.title}`);
|
|
94
|
-
yield* Console.log(`Status: ${updatedTask.state.name}`);
|
|
95
|
-
yield* Console.log(`\nBranch name: ${branchName}`);
|
|
96
|
-
yield* Console.log("\nTo create a jj change with this branch (Phase 2):");
|
|
97
|
-
yield* Console.log(` jj new -m "${updatedTask.identifier}: ${updatedTask.title}"`);
|
|
98
|
-
yield* Console.log(` jj bookmark create ${branchName}`);
|
|
99
|
-
}
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
@@ -1,135 +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 { TeamRepository } from "../../../../ports/TeamRepository.js";
|
|
8
|
-
import { LinearConfig } from "../../../../domain/Config.js";
|
|
9
|
-
import type { Team, TeamId } from "../../../../domain/Task.js";
|
|
10
|
-
|
|
11
|
-
const CREATE_NEW = "__create_new__" as const;
|
|
12
|
-
|
|
13
|
-
export const teamCommand = Command.make("team", {}, () =>
|
|
14
|
-
Effect.gen(function* () {
|
|
15
|
-
const config = yield* ConfigRepository;
|
|
16
|
-
const auth = yield* AuthService;
|
|
17
|
-
const teamRepo = yield* TeamRepository;
|
|
18
|
-
|
|
19
|
-
clack.intro("ship team");
|
|
20
|
-
|
|
21
|
-
// Check authentication
|
|
22
|
-
const isAuth = yield* auth.isAuthenticated();
|
|
23
|
-
if (!isAuth) {
|
|
24
|
-
clack.log.error("Not authenticated. Run 'ship login' first.");
|
|
25
|
-
clack.outro("Setup required");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Show current team
|
|
30
|
-
const partial = yield* config.loadPartial();
|
|
31
|
-
if (Option.isSome(partial.linear)) {
|
|
32
|
-
clack.log.info(`Current team: ${partial.linear.value.teamKey}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Fetch teams
|
|
36
|
-
const spinner = clack.spinner();
|
|
37
|
-
spinner.start("Fetching teams...");
|
|
38
|
-
const teams = yield* teamRepo.getTeams();
|
|
39
|
-
spinner.stop("Teams loaded");
|
|
40
|
-
|
|
41
|
-
// Select team or create new
|
|
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
|
-
...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
|
-
];
|
|
52
|
-
|
|
53
|
-
const teamChoice = yield* Effect.tryPromise({
|
|
54
|
-
try: () =>
|
|
55
|
-
clack.select({
|
|
56
|
-
message: "Select a team",
|
|
57
|
-
options: teamOptions,
|
|
58
|
-
}),
|
|
59
|
-
catch: () => new Error("Prompt cancelled"),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (clack.isCancel(teamChoice)) {
|
|
63
|
-
clack.cancel("Cancelled");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let selectedTeam: Team;
|
|
68
|
-
|
|
69
|
-
if (teamChoice === CREATE_NEW) {
|
|
70
|
-
// Create new team
|
|
71
|
-
const teamName = yield* Effect.tryPromise({
|
|
72
|
-
try: () =>
|
|
73
|
-
clack.text({
|
|
74
|
-
message: "Team name",
|
|
75
|
-
placeholder: "My Team",
|
|
76
|
-
validate: (v) => (!v ? "Name is required" : undefined),
|
|
77
|
-
}),
|
|
78
|
-
catch: () => new Error("Prompt cancelled"),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (clack.isCancel(teamName)) {
|
|
82
|
-
clack.cancel("Cancelled");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const teamKey = yield* Effect.tryPromise({
|
|
87
|
-
try: () =>
|
|
88
|
-
clack.text({
|
|
89
|
-
message: "Team key (short identifier, e.g. ENG)",
|
|
90
|
-
placeholder: "ENG",
|
|
91
|
-
validate: (v) => {
|
|
92
|
-
if (!v) return "Key is required";
|
|
93
|
-
if (!/^[A-Z]{2,5}$/.test(v.toUpperCase())) return "Key must be 2-5 uppercase letters";
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
catch: () => new Error("Prompt cancelled"),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (clack.isCancel(teamKey)) {
|
|
100
|
-
clack.cancel("Cancelled");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const createSpinner = clack.spinner();
|
|
105
|
-
createSpinner.start("Creating team...");
|
|
106
|
-
|
|
107
|
-
selectedTeam = yield* teamRepo.createTeam({
|
|
108
|
-
name: teamName as string,
|
|
109
|
-
key: (teamKey as string).toUpperCase(),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
createSpinner.stop(`Created team: ${selectedTeam.key}`);
|
|
113
|
-
} else {
|
|
114
|
-
const found = teams.find((t) => t.id === teamChoice);
|
|
115
|
-
if (!found) {
|
|
116
|
-
clack.log.error("Selected team not found. Please try again.");
|
|
117
|
-
clack.outro("Error");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
selectedTeam = found;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Save new team config (clears project since it's team-specific)
|
|
124
|
-
const linearConfig = new LinearConfig({
|
|
125
|
-
teamId: selectedTeam.id,
|
|
126
|
-
teamKey: selectedTeam.key,
|
|
127
|
-
projectId: Option.none(),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
yield* config.saveLinear(linearConfig);
|
|
131
|
-
|
|
132
|
-
clack.log.success(`Switched to team: ${selectedTeam.key} - ${selectedTeam.name}`);
|
|
133
|
-
clack.outro("Run 'ship project' to select a project.");
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
@@ -1,63 +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 { ConfigRepository } from "../../../../ports/ConfigRepository.js";
|
|
7
|
-
import { IssueRepository } from "../../../../ports/IssueRepository.js";
|
|
8
|
-
import type { TaskId } from "../../../../domain/Task.js";
|
|
9
|
-
|
|
10
|
-
const blockerArg = Args.text({ name: "blocker" }).pipe(
|
|
11
|
-
Args.withDescription("Task that was blocking (e.g., ENG-123)"),
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
const blockedArg = Args.text({ name: "blocked" }).pipe(
|
|
15
|
-
Args.withDescription("Task that was blocked (e.g., ENG-456)"),
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const jsonOption = Options.boolean("json").pipe(
|
|
19
|
-
Options.withDescription("Output as JSON"),
|
|
20
|
-
Options.withDefault(false),
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
export const unblockCommand = Command.make(
|
|
24
|
-
"unblock",
|
|
25
|
-
{ blocker: blockerArg, blocked: blockedArg, json: jsonOption },
|
|
26
|
-
({ blocker, blocked, json }) =>
|
|
27
|
-
Effect.gen(function* () {
|
|
28
|
-
const config = yield* ConfigRepository;
|
|
29
|
-
const issueRepo = yield* IssueRepository;
|
|
30
|
-
|
|
31
|
-
yield* config.load(); // Ensure initialized
|
|
32
|
-
|
|
33
|
-
// Get both tasks to get their IDs
|
|
34
|
-
const blockerTask = yield* issueRepo
|
|
35
|
-
.getTaskByIdentifier(blocker)
|
|
36
|
-
.pipe(Effect.catchTag("TaskNotFoundError", () => issueRepo.getTask(blocker as TaskId)));
|
|
37
|
-
|
|
38
|
-
const blockedTask = yield* issueRepo
|
|
39
|
-
.getTaskByIdentifier(blocked)
|
|
40
|
-
.pipe(Effect.catchTag("TaskNotFoundError", () => issueRepo.getTask(blocked as TaskId)));
|
|
41
|
-
|
|
42
|
-
// Remove the blocking relationship
|
|
43
|
-
yield* issueRepo.removeBlocker(blockedTask.id, blockerTask.id);
|
|
44
|
-
|
|
45
|
-
if (json) {
|
|
46
|
-
yield* Console.log(
|
|
47
|
-
JSON.stringify({
|
|
48
|
-
status: "unblocked",
|
|
49
|
-
blocker: {
|
|
50
|
-
id: blockerTask.id,
|
|
51
|
-
identifier: blockerTask.identifier,
|
|
52
|
-
},
|
|
53
|
-
blocked: {
|
|
54
|
-
id: blockedTask.id,
|
|
55
|
-
identifier: blockedTask.identifier,
|
|
56
|
-
},
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
59
|
-
} else {
|
|
60
|
-
yield* Console.log(`${blockerTask.identifier} no longer blocks ${blockedTask.identifier}`);
|
|
61
|
-
}
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
@@ -1,125 +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 {
|
|
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
|
-
);
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import * as Command from "@effect/cli/Command";
|
|
2
|
-
import * as Console from "effect/Console";
|
|
3
|
-
import { initCommand } from "./commands/init.js";
|
|
4
|
-
import { loginCommand } from "./commands/login.js";
|
|
5
|
-
import { teamCommand } from "./commands/team.js";
|
|
6
|
-
import { projectCommand } from "./commands/project.js";
|
|
7
|
-
import { readyCommand } from "./commands/ready.js";
|
|
8
|
-
import { listCommand } from "./commands/list.js";
|
|
9
|
-
import { showCommand } from "./commands/show.js";
|
|
10
|
-
import { startCommand } from "./commands/start.js";
|
|
11
|
-
import { doneCommand } from "./commands/done.js";
|
|
12
|
-
import { createCommand } from "./commands/create.js";
|
|
13
|
-
import { blockCommand } from "./commands/block.js";
|
|
14
|
-
import { unblockCommand } from "./commands/unblock.js";
|
|
15
|
-
import { blockedCommand } from "./commands/blocked.js";
|
|
16
|
-
import { primeCommand } from "./commands/prime.js";
|
|
17
|
-
import { updateCommand } from "./commands/update.js";
|
|
18
|
-
import { relateCommand } from "./commands/relate.js";
|
|
19
|
-
|
|
20
|
-
// Root command
|
|
21
|
-
const ship = Command.make("ship", {}, () =>
|
|
22
|
-
Console.log(`ship - Linear + jj workflow CLI
|
|
23
|
-
|
|
24
|
-
Usage: ship <command> [options]
|
|
25
|
-
|
|
26
|
-
Commands:
|
|
27
|
-
init Initialize workspace and authenticate
|
|
28
|
-
login Re-authenticate with Linear
|
|
29
|
-
team Switch team
|
|
30
|
-
project Switch project
|
|
31
|
-
|
|
32
|
-
ready List tasks ready to work on (no blockers)
|
|
33
|
-
blocked List blocked tasks
|
|
34
|
-
list List all tasks with filters
|
|
35
|
-
show <id> Show task details
|
|
36
|
-
create <title> Create new task
|
|
37
|
-
|
|
38
|
-
start <id> Start working on a task
|
|
39
|
-
done <id> Mark task as complete
|
|
40
|
-
update <id> Update task details
|
|
41
|
-
|
|
42
|
-
block <a> <b> Mark task A as blocking task B
|
|
43
|
-
unblock <a> <b> Remove blocking relationship
|
|
44
|
-
relate <a> <b> Link two tasks as related
|
|
45
|
-
|
|
46
|
-
prime Output AI-optimized context
|
|
47
|
-
|
|
48
|
-
Run 'ship <command> --help' for more information on a command.`),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
// Combine all commands
|
|
52
|
-
export const command = ship.pipe(
|
|
53
|
-
Command.withSubcommands([
|
|
54
|
-
initCommand,
|
|
55
|
-
loginCommand,
|
|
56
|
-
teamCommand,
|
|
57
|
-
projectCommand,
|
|
58
|
-
readyCommand,
|
|
59
|
-
listCommand,
|
|
60
|
-
showCommand,
|
|
61
|
-
startCommand,
|
|
62
|
-
doneCommand,
|
|
63
|
-
updateCommand,
|
|
64
|
-
createCommand,
|
|
65
|
-
blockCommand,
|
|
66
|
-
unblockCommand,
|
|
67
|
-
relateCommand,
|
|
68
|
-
blockedCommand,
|
|
69
|
-
primeCommand,
|
|
70
|
-
]),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
export const run = Command.run(command, {
|
|
74
|
-
name: "ship",
|
|
75
|
-
version: "0.0.1",
|
|
76
|
-
});
|
package/src/bin.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
4
|
-
import * as Effect from "effect/Effect";
|
|
5
|
-
import { run } from "./adapters/driving/cli/main.js";
|
|
6
|
-
import { AppLayer } from "./infrastructure/Layers.js";
|
|
7
|
-
|
|
8
|
-
// Run the CLI
|
|
9
|
-
run(process.argv).pipe(
|
|
10
|
-
Effect.provide(AppLayer),
|
|
11
|
-
NodeRuntime.runMain({ disableErrorReporting: false }),
|
|
12
|
-
);
|
package/src/domain/Config.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import * as Schema from "effect/Schema";
|
|
2
|
-
import { ProjectId, TeamId } from "./Task.js";
|
|
3
|
-
|
|
4
|
-
// Personal API key from https://linear.app/settings/api
|
|
5
|
-
export class AuthConfig extends Schema.Class<AuthConfig>("AuthConfig")({
|
|
6
|
-
apiKey: Schema.String,
|
|
7
|
-
}) {}
|
|
8
|
-
|
|
9
|
-
export class LinearConfig extends Schema.Class<LinearConfig>("LinearConfig")({
|
|
10
|
-
teamId: TeamId,
|
|
11
|
-
teamKey: Schema.String,
|
|
12
|
-
projectId: Schema.OptionFromNullOr(ProjectId),
|
|
13
|
-
}) {}
|
|
14
|
-
|
|
15
|
-
export class GitConfig extends Schema.Class<GitConfig>("GitConfig")({
|
|
16
|
-
defaultBranch: Schema.optionalWith(Schema.String, { default: () => "main" }),
|
|
17
|
-
}) {}
|
|
18
|
-
|
|
19
|
-
export class PrConfig extends Schema.Class<PrConfig>("PrConfig")({
|
|
20
|
-
openBrowser: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
21
|
-
}) {}
|
|
22
|
-
|
|
23
|
-
export class CommitConfig extends Schema.Class<CommitConfig>("CommitConfig")({
|
|
24
|
-
conventionalFormat: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
25
|
-
}) {}
|
|
26
|
-
|
|
27
|
-
export class ShipConfig extends Schema.Class<ShipConfig>("ShipConfig")({
|
|
28
|
-
linear: LinearConfig,
|
|
29
|
-
auth: AuthConfig,
|
|
30
|
-
git: Schema.optionalWith(GitConfig, { default: () => new GitConfig({}) }),
|
|
31
|
-
pr: Schema.optionalWith(PrConfig, { default: () => new PrConfig({}) }),
|
|
32
|
-
commit: Schema.optionalWith(CommitConfig, { default: () => new CommitConfig({}) }),
|
|
33
|
-
}) {}
|
|
34
|
-
|
|
35
|
-
// Partial config for when we're initializing
|
|
36
|
-
export class PartialShipConfig extends Schema.Class<PartialShipConfig>("PartialShipConfig")({
|
|
37
|
-
linear: Schema.OptionFromNullOr(LinearConfig),
|
|
38
|
-
auth: Schema.OptionFromNullOr(AuthConfig),
|
|
39
|
-
git: Schema.optionalWith(GitConfig, { default: () => new GitConfig({}) }),
|
|
40
|
-
pr: Schema.optionalWith(PrConfig, { default: () => new PrConfig({}) }),
|
|
41
|
-
commit: Schema.optionalWith(CommitConfig, { default: () => new CommitConfig({}) }),
|
|
42
|
-
}) {}
|
package/src/domain/Errors.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import * as Data from "effect/Data";
|
|
2
|
-
|
|
3
|
-
// === Task Errors ===
|
|
4
|
-
|
|
5
|
-
export class TaskNotFoundError extends Data.TaggedError("TaskNotFoundError")<{
|
|
6
|
-
readonly taskId: string;
|
|
7
|
-
}> {
|
|
8
|
-
get message() {
|
|
9
|
-
return `Task not found: ${this.taskId}`;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class TaskError extends Data.TaggedError("TaskError")<{
|
|
14
|
-
readonly message: string;
|
|
15
|
-
readonly cause?: unknown;
|
|
16
|
-
}> {}
|
|
17
|
-
|
|
18
|
-
// === Auth Errors ===
|
|
19
|
-
|
|
20
|
-
export class AuthError extends Data.TaggedError("AuthError")<{
|
|
21
|
-
readonly message: string;
|
|
22
|
-
readonly cause?: unknown;
|
|
23
|
-
}> {}
|
|
24
|
-
|
|
25
|
-
export class NotAuthenticatedError extends Data.TaggedError("NotAuthenticatedError")<{
|
|
26
|
-
readonly message: string;
|
|
27
|
-
}> {}
|
|
28
|
-
|
|
29
|
-
// === Config Errors ===
|
|
30
|
-
|
|
31
|
-
export class ConfigError extends Data.TaggedError("ConfigError")<{
|
|
32
|
-
readonly message: string;
|
|
33
|
-
readonly cause?: unknown;
|
|
34
|
-
}> {}
|
|
35
|
-
|
|
36
|
-
export class ConfigNotFoundError extends Data.TaggedError("ConfigNotFoundError")<{
|
|
37
|
-
readonly message: string;
|
|
38
|
-
}> {}
|
|
39
|
-
|
|
40
|
-
export class WorkspaceNotInitializedError extends Data.TaggedError("WorkspaceNotInitializedError")<{
|
|
41
|
-
readonly message: string;
|
|
42
|
-
}> {
|
|
43
|
-
static readonly default = new WorkspaceNotInitializedError({
|
|
44
|
-
message: "Workspace not initialized. Run 'ship init' first.",
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// === VCS Errors ===
|
|
49
|
-
|
|
50
|
-
export class VcsError extends Data.TaggedError("VcsError")<{
|
|
51
|
-
readonly message: string;
|
|
52
|
-
readonly cause?: unknown;
|
|
53
|
-
}> {}
|
|
54
|
-
|
|
55
|
-
export class JjNotInstalledError extends Data.TaggedError("JjNotInstalledError")<{
|
|
56
|
-
readonly message: string;
|
|
57
|
-
}> {
|
|
58
|
-
static readonly default = new JjNotInstalledError({
|
|
59
|
-
message: "jj is not installed. Visit https://jj-vcs.github.io/jj/",
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// === PR Errors ===
|
|
64
|
-
|
|
65
|
-
export class PrError extends Data.TaggedError("PrError")<{
|
|
66
|
-
readonly message: string;
|
|
67
|
-
readonly cause?: unknown;
|
|
68
|
-
}> {}
|
|
69
|
-
|
|
70
|
-
export class GhNotInstalledError extends Data.TaggedError("GhNotInstalledError")<{
|
|
71
|
-
readonly message: string;
|
|
72
|
-
}> {
|
|
73
|
-
static readonly default = new GhNotInstalledError({
|
|
74
|
-
message: "gh CLI is not installed. Visit https://cli.github.com/",
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// === Linear API Errors ===
|
|
79
|
-
|
|
80
|
-
export class LinearApiError extends Data.TaggedError("LinearApiError")<{
|
|
81
|
-
readonly message: string;
|
|
82
|
-
readonly statusCode?: number;
|
|
83
|
-
readonly cause?: unknown;
|
|
84
|
-
}> {}
|
|
85
|
-
|
|
86
|
-
export class RateLimitError extends Data.TaggedError("RateLimitError")<{
|
|
87
|
-
readonly message: string;
|
|
88
|
-
readonly retryAfter?: number;
|
|
89
|
-
}> {}
|