@ship-cli/core 0.0.2 → 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 -499
- 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 -83
- 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/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/main.ts +0 -70
- 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 -69
- 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/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -12
|
@@ -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,70 +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
|
-
|
|
18
|
-
// Root command
|
|
19
|
-
const ship = Command.make("ship", {}, () =>
|
|
20
|
-
Console.log(`ship - Linear + jj workflow CLI
|
|
21
|
-
|
|
22
|
-
Usage: ship <command> [options]
|
|
23
|
-
|
|
24
|
-
Commands:
|
|
25
|
-
init Initialize workspace and authenticate
|
|
26
|
-
login Re-authenticate with Linear
|
|
27
|
-
team Switch team
|
|
28
|
-
project Switch project
|
|
29
|
-
|
|
30
|
-
ready List tasks ready to work on (no blockers)
|
|
31
|
-
blocked List blocked tasks
|
|
32
|
-
list List all tasks with filters
|
|
33
|
-
show <id> Show task details
|
|
34
|
-
create <title> Create new task
|
|
35
|
-
|
|
36
|
-
start <id> Start working on a task
|
|
37
|
-
done <id> Mark task as complete
|
|
38
|
-
|
|
39
|
-
block <a> <b> Mark task A as blocking task B
|
|
40
|
-
unblock <a> <b> Remove blocking relationship
|
|
41
|
-
|
|
42
|
-
prime Output AI-optimized context
|
|
43
|
-
|
|
44
|
-
Run 'ship <command> --help' for more information on a command.`),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// Combine all commands
|
|
48
|
-
export const command = ship.pipe(
|
|
49
|
-
Command.withSubcommands([
|
|
50
|
-
initCommand,
|
|
51
|
-
loginCommand,
|
|
52
|
-
teamCommand,
|
|
53
|
-
projectCommand,
|
|
54
|
-
readyCommand,
|
|
55
|
-
listCommand,
|
|
56
|
-
showCommand,
|
|
57
|
-
startCommand,
|
|
58
|
-
doneCommand,
|
|
59
|
-
createCommand,
|
|
60
|
-
blockCommand,
|
|
61
|
-
unblockCommand,
|
|
62
|
-
blockedCommand,
|
|
63
|
-
primeCommand,
|
|
64
|
-
]),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
export const run = Command.run(command, {
|
|
68
|
-
name: "ship",
|
|
69
|
-
version: "0.0.1",
|
|
70
|
-
});
|
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
|
-
}> {}
|
package/src/domain/Task.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import * as Schema from "effect/Schema";
|
|
2
|
-
|
|
3
|
-
// === Branded Types ===
|
|
4
|
-
|
|
5
|
-
export const TaskId = Schema.String.pipe(Schema.brand("TaskId"));
|
|
6
|
-
export type TaskId = typeof TaskId.Type;
|
|
7
|
-
|
|
8
|
-
export const TeamId = Schema.String.pipe(Schema.brand("TeamId"));
|
|
9
|
-
export type TeamId = typeof TeamId.Type;
|
|
10
|
-
|
|
11
|
-
export const ProjectId = Schema.String.pipe(Schema.brand("ProjectId"));
|
|
12
|
-
export type ProjectId = typeof ProjectId.Type;
|
|
13
|
-
|
|
14
|
-
// === Enums ===
|
|
15
|
-
|
|
16
|
-
// Linear's workflow state types (not custom state names)
|
|
17
|
-
export const WorkflowStateType = Schema.Literal(
|
|
18
|
-
"backlog",
|
|
19
|
-
"unstarted",
|
|
20
|
-
"started",
|
|
21
|
-
"completed",
|
|
22
|
-
"canceled",
|
|
23
|
-
);
|
|
24
|
-
export type WorkflowStateType = typeof WorkflowStateType.Type;
|
|
25
|
-
|
|
26
|
-
// For backwards compatibility - maps to Linear state types
|
|
27
|
-
export const TaskStatus = Schema.Literal(
|
|
28
|
-
"backlog",
|
|
29
|
-
"todo",
|
|
30
|
-
"in_progress",
|
|
31
|
-
"in_review",
|
|
32
|
-
"done",
|
|
33
|
-
"cancelled",
|
|
34
|
-
);
|
|
35
|
-
export type TaskStatus = typeof TaskStatus.Type;
|
|
36
|
-
|
|
37
|
-
export const Priority = Schema.Literal("urgent", "high", "medium", "low", "none");
|
|
38
|
-
export type Priority = typeof Priority.Type;
|
|
39
|
-
|
|
40
|
-
export const TaskType = Schema.Literal("bug", "feature", "task", "epic", "chore");
|
|
41
|
-
export type TaskType = typeof TaskType.Type;
|
|
42
|
-
|
|
43
|
-
export const DependencyType = Schema.Literal("blocks", "blocked_by", "related", "duplicate");
|
|
44
|
-
export type DependencyType = typeof DependencyType.Type;
|
|
45
|
-
|
|
46
|
-
// === Domain Models ===
|
|
47
|
-
|
|
48
|
-
export class Dependency extends Schema.Class<Dependency>("Dependency")({
|
|
49
|
-
id: Schema.String,
|
|
50
|
-
type: DependencyType,
|
|
51
|
-
relatedTaskId: TaskId,
|
|
52
|
-
}) {}
|
|
53
|
-
|
|
54
|
-
// Workflow state from Linear (custom states)
|
|
55
|
-
export class WorkflowState extends Schema.Class<WorkflowState>("WorkflowState")({
|
|
56
|
-
id: Schema.String,
|
|
57
|
-
name: Schema.String,
|
|
58
|
-
type: WorkflowStateType,
|
|
59
|
-
}) {}
|
|
60
|
-
|
|
61
|
-
export class Task extends Schema.Class<Task>("Task")({
|
|
62
|
-
id: TaskId,
|
|
63
|
-
identifier: Schema.String, // e.g., "ENG-123"
|
|
64
|
-
title: Schema.String,
|
|
65
|
-
description: Schema.OptionFromNullOr(Schema.String),
|
|
66
|
-
state: WorkflowState, // The actual Linear state (custom or default)
|
|
67
|
-
priority: Priority,
|
|
68
|
-
type: Schema.OptionFromNullOr(TaskType),
|
|
69
|
-
teamId: TeamId,
|
|
70
|
-
projectId: Schema.OptionFromNullOr(ProjectId),
|
|
71
|
-
branchName: Schema.OptionFromNullOr(Schema.String),
|
|
72
|
-
url: Schema.String,
|
|
73
|
-
labels: Schema.Array(Schema.String),
|
|
74
|
-
blockedBy: Schema.Array(TaskId),
|
|
75
|
-
blocks: Schema.Array(TaskId),
|
|
76
|
-
createdAt: Schema.Date,
|
|
77
|
-
updatedAt: Schema.Date,
|
|
78
|
-
}) {
|
|
79
|
-
// Helper to check if task is in a "done" state (completed or canceled)
|
|
80
|
-
get isDone(): boolean {
|
|
81
|
-
return this.state.type === "completed" || this.state.type === "canceled";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Helper to check if task is actionable (not done)
|
|
85
|
-
get isActionable(): boolean {
|
|
86
|
-
return !this.isDone;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class Team extends Schema.Class<Team>("Team")({
|
|
91
|
-
id: TeamId,
|
|
92
|
-
name: Schema.String,
|
|
93
|
-
key: Schema.String, // e.g., "ENG"
|
|
94
|
-
}) {}
|
|
95
|
-
|
|
96
|
-
export class Project extends Schema.Class<Project>("Project")({
|
|
97
|
-
id: ProjectId,
|
|
98
|
-
name: Schema.String,
|
|
99
|
-
teamId: TeamId,
|
|
100
|
-
}) {}
|
|
101
|
-
|
|
102
|
-
// === Input Types ===
|
|
103
|
-
|
|
104
|
-
export class CreateTaskInput extends Schema.Class<CreateTaskInput>("CreateTaskInput")({
|
|
105
|
-
title: Schema.String,
|
|
106
|
-
description: Schema.OptionFromNullOr(Schema.String),
|
|
107
|
-
priority: Schema.optionalWith(Priority, { default: () => "medium" as const }),
|
|
108
|
-
type: Schema.optionalWith(TaskType, { default: () => "task" as const }),
|
|
109
|
-
projectId: Schema.OptionFromNullOr(ProjectId),
|
|
110
|
-
}) {}
|
|
111
|
-
|
|
112
|
-
export class UpdateTaskInput extends Schema.Class<UpdateTaskInput>("UpdateTaskInput")({
|
|
113
|
-
title: Schema.OptionFromNullOr(Schema.String),
|
|
114
|
-
description: Schema.OptionFromNullOr(Schema.String),
|
|
115
|
-
status: Schema.OptionFromNullOr(TaskStatus),
|
|
116
|
-
priority: Schema.OptionFromNullOr(Priority),
|
|
117
|
-
}) {}
|
|
118
|
-
|
|
119
|
-
export class TaskFilter extends Schema.Class<TaskFilter>("TaskFilter")({
|
|
120
|
-
status: Schema.OptionFromNullOr(TaskStatus),
|
|
121
|
-
priority: Schema.OptionFromNullOr(Priority),
|
|
122
|
-
projectId: Schema.OptionFromNullOr(ProjectId),
|
|
123
|
-
assignedToMe: Schema.optionalWith(Schema.Boolean, { default: () => false }),
|
|
124
|
-
}) {}
|
package/src/domain/index.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import * as Layer from "effect/Layer";
|
|
2
|
-
import * as NodeContext from "@effect/platform-node/NodeContext";
|
|
3
|
-
import { ConfigRepositoryLive } from "../adapters/driven/config/ConfigRepositoryLive.js";
|
|
4
|
-
import { AuthServiceLive } from "../adapters/driven/auth/AuthServiceLive.js";
|
|
5
|
-
import { LinearClientLive } from "../adapters/driven/linear/LinearClient.js";
|
|
6
|
-
import { TeamRepositoryLive } from "../adapters/driven/linear/TeamRepositoryLive.js";
|
|
7
|
-
import { ProjectRepositoryLive } from "../adapters/driven/linear/ProjectRepositoryLive.js";
|
|
8
|
-
import { IssueRepositoryLive } from "../adapters/driven/linear/IssueRepositoryLive.js";
|
|
9
|
-
|
|
10
|
-
// Layer dependencies:
|
|
11
|
-
// ConfigRepositoryLive: FileSystem + Path -> ConfigRepository
|
|
12
|
-
// AuthServiceLive: ConfigRepository -> AuthService
|
|
13
|
-
// LinearClientLive: AuthService -> LinearClientService
|
|
14
|
-
// TeamRepositoryLive: LinearClientService -> TeamRepository
|
|
15
|
-
// ProjectRepositoryLive: LinearClientService -> ProjectRepository
|
|
16
|
-
// IssueRepositoryLive: LinearClientService -> IssueRepository
|
|
17
|
-
|
|
18
|
-
// Build the layer chain - each layer provides what the next needs
|
|
19
|
-
// ConfigRepository provides what AuthService needs
|
|
20
|
-
const ConfigAndAuth = AuthServiceLive.pipe(Layer.provideMerge(ConfigRepositoryLive));
|
|
21
|
-
|
|
22
|
-
// AuthService provides what LinearClient needs
|
|
23
|
-
const ConfigAuthAndLinear = LinearClientLive.pipe(Layer.provideMerge(ConfigAndAuth));
|
|
24
|
-
|
|
25
|
-
// LinearClientService provides what repositories need
|
|
26
|
-
// Merge all three repository layers together
|
|
27
|
-
const RepositoryLayers = Layer.mergeAll(
|
|
28
|
-
TeamRepositoryLive,
|
|
29
|
-
ProjectRepositoryLive,
|
|
30
|
-
IssueRepositoryLive,
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const AllServices = RepositoryLayers.pipe(Layer.provideMerge(ConfigAuthAndLinear));
|
|
34
|
-
|
|
35
|
-
// Full application layer for Phase 1 (Linear + Config + Auth)
|
|
36
|
-
// Use provideMerge to:
|
|
37
|
-
// 1. Satisfy platform deps (FileSystem, Path) that ConfigRepositoryLive needs
|
|
38
|
-
// 2. Keep platform services (FileSystem, Path, Terminal) in output for @effect/cli
|
|
39
|
-
export const AppLayer = AllServices.pipe(Layer.provideMerge(NodeContext.layer));
|
|
40
|
-
|
|
41
|
-
// Minimal layer for init/login commands (before full config exists)
|
|
42
|
-
// Also includes TeamRepository and ProjectRepository for init flow
|
|
43
|
-
const MinimalRepositories = Layer.mergeAll(TeamRepositoryLive, ProjectRepositoryLive);
|
|
44
|
-
const MinimalServices = MinimalRepositories.pipe(Layer.provideMerge(ConfigAuthAndLinear));
|
|
45
|
-
export const MinimalLayer = MinimalServices.pipe(Layer.provideMerge(NodeContext.layer));
|
package/src/ports/AuthService.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as Context from "effect/Context";
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import type { AuthConfig } from "../domain/Config.js";
|
|
4
|
-
import type { AuthError, ConfigError, NotAuthenticatedError } from "../domain/Errors.js";
|
|
5
|
-
|
|
6
|
-
export interface AuthService {
|
|
7
|
-
/** Save API key after user pastes it */
|
|
8
|
-
readonly saveApiKey: (apiKey: string) => Effect.Effect<AuthConfig, AuthError>;
|
|
9
|
-
/** Validate an API key by making a test request */
|
|
10
|
-
readonly validateApiKey: (apiKey: string) => Effect.Effect<boolean, AuthError>;
|
|
11
|
-
/** Get the stored API key - may fail if config is corrupted */
|
|
12
|
-
readonly getApiKey: () => Effect.Effect<string, NotAuthenticatedError | ConfigError>;
|
|
13
|
-
/** Remove stored credentials */
|
|
14
|
-
readonly logout: () => Effect.Effect<void, never>;
|
|
15
|
-
/** Check if we have stored credentials */
|
|
16
|
-
readonly isAuthenticated: () => Effect.Effect<boolean, never>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const AuthService = Context.GenericTag<AuthService>("AuthService");
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import * as Context from "effect/Context";
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import type { AuthConfig, LinearConfig, PartialShipConfig, ShipConfig } from "../domain/Config.js";
|
|
4
|
-
import type { ConfigError, WorkspaceNotInitializedError } from "../domain/Errors.js";
|
|
5
|
-
|
|
6
|
-
export interface ConfigRepository {
|
|
7
|
-
readonly load: () => Effect.Effect<ShipConfig, WorkspaceNotInitializedError | ConfigError>;
|
|
8
|
-
readonly loadPartial: () => Effect.Effect<PartialShipConfig, ConfigError>;
|
|
9
|
-
readonly save: (config: ShipConfig) => Effect.Effect<void, ConfigError>;
|
|
10
|
-
readonly savePartial: (config: PartialShipConfig) => Effect.Effect<void, ConfigError>;
|
|
11
|
-
readonly saveAuth: (auth: AuthConfig) => Effect.Effect<void, ConfigError>;
|
|
12
|
-
readonly saveLinear: (linear: LinearConfig) => Effect.Effect<void, ConfigError>;
|
|
13
|
-
readonly exists: () => Effect.Effect<boolean, ConfigError>;
|
|
14
|
-
readonly getConfigDir: () => Effect.Effect<string, never>;
|
|
15
|
-
readonly ensureConfigDir: () => Effect.Effect<void, ConfigError>;
|
|
16
|
-
readonly ensureGitignore: () => Effect.Effect<void, ConfigError>;
|
|
17
|
-
readonly delete: () => Effect.Effect<void, ConfigError>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const ConfigRepository = Context.GenericTag<ConfigRepository>("ConfigRepository");
|