@ship-cli/core 0.0.1

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 (48) hide show
  1. package/.tsbuildinfo/src.tsbuildinfo +1 -0
  2. package/.tsbuildinfo/test.tsbuildinfo +1 -0
  3. package/LICENSE +21 -0
  4. package/dist/bin.js +49230 -0
  5. package/package.json +50 -0
  6. package/src/adapters/driven/auth/AuthServiceLive.ts +125 -0
  7. package/src/adapters/driven/config/ConfigRepositoryLive.ts +366 -0
  8. package/src/adapters/driven/linear/IssueRepositoryLive.ts +494 -0
  9. package/src/adapters/driven/linear/LinearClient.ts +33 -0
  10. package/src/adapters/driven/linear/Mapper.ts +142 -0
  11. package/src/adapters/driven/linear/ProjectRepositoryLive.ts +98 -0
  12. package/src/adapters/driven/linear/TeamRepositoryLive.ts +100 -0
  13. package/src/adapters/driving/cli/commands/block.ts +63 -0
  14. package/src/adapters/driving/cli/commands/blocked.ts +61 -0
  15. package/src/adapters/driving/cli/commands/create.ts +83 -0
  16. package/src/adapters/driving/cli/commands/done.ts +82 -0
  17. package/src/adapters/driving/cli/commands/init.ts +194 -0
  18. package/src/adapters/driving/cli/commands/list.ts +87 -0
  19. package/src/adapters/driving/cli/commands/login.ts +46 -0
  20. package/src/adapters/driving/cli/commands/prime.ts +114 -0
  21. package/src/adapters/driving/cli/commands/project.ts +155 -0
  22. package/src/adapters/driving/cli/commands/ready.ts +73 -0
  23. package/src/adapters/driving/cli/commands/show.ts +94 -0
  24. package/src/adapters/driving/cli/commands/start.ts +99 -0
  25. package/src/adapters/driving/cli/commands/team.ts +134 -0
  26. package/src/adapters/driving/cli/commands/unblock.ts +63 -0
  27. package/src/adapters/driving/cli/main.ts +70 -0
  28. package/src/bin.ts +12 -0
  29. package/src/domain/Config.ts +42 -0
  30. package/src/domain/Errors.ts +89 -0
  31. package/src/domain/Task.ts +124 -0
  32. package/src/domain/index.ts +3 -0
  33. package/src/infrastructure/Layers.ts +45 -0
  34. package/src/ports/AuthService.ts +19 -0
  35. package/src/ports/ConfigRepository.ts +20 -0
  36. package/src/ports/IssueRepository.ts +69 -0
  37. package/src/ports/PrService.ts +52 -0
  38. package/src/ports/ProjectRepository.ts +19 -0
  39. package/src/ports/TeamRepository.ts +17 -0
  40. package/src/ports/VcsService.ts +87 -0
  41. package/src/ports/index.ts +7 -0
  42. package/test/Dummy.test.ts +7 -0
  43. package/tsconfig.base.json +45 -0
  44. package/tsconfig.json +7 -0
  45. package/tsconfig.src.json +11 -0
  46. package/tsconfig.test.json +10 -0
  47. package/tsup.config.ts +14 -0
  48. package/vitest.config.ts +12 -0
@@ -0,0 +1,69 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import type {
4
+ CreateTaskInput,
5
+ Task,
6
+ TaskFilter,
7
+ TaskId,
8
+ TeamId,
9
+ UpdateTaskInput,
10
+ ProjectId,
11
+ } from "../domain/Task.js";
12
+ import type { LinearApiError, TaskError, TaskNotFoundError } from "../domain/Errors.js";
13
+
14
+ export interface IssueRepository {
15
+ /** Get a task by its Linear ID */
16
+ readonly getTask: (id: TaskId) => Effect.Effect<Task, TaskNotFoundError | LinearApiError>;
17
+
18
+ /** Get a task by its identifier (e.g., "ENG-123") */
19
+ readonly getTaskByIdentifier: (
20
+ identifier: string,
21
+ ) => Effect.Effect<Task, TaskNotFoundError | LinearApiError>;
22
+
23
+ /** Create a new task */
24
+ readonly createTask: (
25
+ teamId: TeamId,
26
+ input: CreateTaskInput,
27
+ ) => Effect.Effect<Task, TaskError | LinearApiError>;
28
+
29
+ /** Update an existing task */
30
+ readonly updateTask: (
31
+ id: TaskId,
32
+ input: UpdateTaskInput,
33
+ ) => Effect.Effect<Task, TaskNotFoundError | TaskError | LinearApiError>;
34
+
35
+ /** List tasks with optional filters */
36
+ readonly listTasks: (
37
+ teamId: TeamId,
38
+ filter: TaskFilter,
39
+ ) => Effect.Effect<ReadonlyArray<Task>, LinearApiError>;
40
+
41
+ /** Get tasks that are ready to work on (not blocked) */
42
+ readonly getReadyTasks: (
43
+ teamId: TeamId,
44
+ projectId?: ProjectId,
45
+ ) => Effect.Effect<ReadonlyArray<Task>, LinearApiError>;
46
+
47
+ /** Get tasks that are blocked by other tasks */
48
+ readonly getBlockedTasks: (
49
+ teamId: TeamId,
50
+ projectId?: ProjectId,
51
+ ) => Effect.Effect<ReadonlyArray<Task>, LinearApiError>;
52
+
53
+ /** Add a blocking relationship between tasks */
54
+ readonly addBlocker: (
55
+ blockedId: TaskId,
56
+ blockerId: TaskId,
57
+ ) => Effect.Effect<void, TaskNotFoundError | TaskError | LinearApiError>;
58
+
59
+ /** Remove a blocking relationship between tasks */
60
+ readonly removeBlocker: (
61
+ blockedId: TaskId,
62
+ blockerId: TaskId,
63
+ ) => Effect.Effect<void, TaskNotFoundError | TaskError | LinearApiError>;
64
+
65
+ /** Get the suggested branch name for a task */
66
+ readonly getBranchName: (id: TaskId) => Effect.Effect<string, TaskNotFoundError | LinearApiError>;
67
+ }
68
+
69
+ export const IssueRepository = Context.GenericTag<IssueRepository>("IssueRepository");
@@ -0,0 +1,52 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schema from "effect/Schema";
4
+ import type { GhNotInstalledError, PrError } from "../domain/Errors.js";
5
+
6
+ // === PR Domain Types ===
7
+
8
+ export const PrId = Schema.String.pipe(Schema.brand("PrId"));
9
+ export type PrId = typeof PrId.Type;
10
+
11
+ export class PullRequest extends Schema.Class<PullRequest>("PullRequest")({
12
+ id: PrId,
13
+ number: Schema.Number,
14
+ title: Schema.String,
15
+ url: Schema.String,
16
+ state: Schema.Literal("open", "closed", "merged"),
17
+ head: Schema.String,
18
+ base: Schema.String,
19
+ }) {}
20
+
21
+ export class CreatePrInput extends Schema.Class<CreatePrInput>("CreatePrInput")({
22
+ title: Schema.String,
23
+ body: Schema.String,
24
+ head: Schema.String, // branch name
25
+ base: Schema.optionalWith(Schema.String, { default: () => "main" }),
26
+ }) {}
27
+
28
+ export interface PrService {
29
+ /**
30
+ * Check if gh CLI is available
31
+ */
32
+ readonly isAvailable: () => Effect.Effect<boolean, never>;
33
+
34
+ /**
35
+ * Create a new pull request
36
+ */
37
+ readonly createPr: (
38
+ input: CreatePrInput,
39
+ ) => Effect.Effect<PullRequest, GhNotInstalledError | PrError>;
40
+
41
+ /**
42
+ * Open PR URL in browser
43
+ */
44
+ readonly openInBrowser: (url: string) => Effect.Effect<void, PrError>;
45
+
46
+ /**
47
+ * Get PR by branch name
48
+ */
49
+ readonly getPrByBranch: (branch: string) => Effect.Effect<PullRequest | null, PrError>;
50
+ }
51
+
52
+ export class PrService extends Context.Tag("PrService")<PrService, PrService>() {}
@@ -0,0 +1,19 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import type { Project, TeamId } from "../domain/Task.js";
4
+ import type { LinearApiError, TaskError } from "../domain/Errors.js";
5
+
6
+ export interface CreateProjectInput {
7
+ readonly name: string;
8
+ readonly description?: string;
9
+ }
10
+
11
+ export interface ProjectRepository {
12
+ readonly getProjects: (teamId: TeamId) => Effect.Effect<ReadonlyArray<Project>, LinearApiError>;
13
+ readonly createProject: (
14
+ teamId: TeamId,
15
+ input: CreateProjectInput,
16
+ ) => Effect.Effect<Project, TaskError | LinearApiError>;
17
+ }
18
+
19
+ export const ProjectRepository = Context.GenericTag<ProjectRepository>("ProjectRepository");
@@ -0,0 +1,17 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import type { Team, TeamId } from "../domain/Task.js";
4
+ import type { LinearApiError, TaskError } from "../domain/Errors.js";
5
+
6
+ export interface CreateTeamInput {
7
+ readonly name: string;
8
+ readonly key: string;
9
+ }
10
+
11
+ export interface TeamRepository {
12
+ readonly getTeams: () => Effect.Effect<ReadonlyArray<Team>, LinearApiError>;
13
+ readonly getTeam: (id: TeamId) => Effect.Effect<Team, LinearApiError>;
14
+ readonly createTeam: (input: CreateTeamInput) => Effect.Effect<Team, TaskError | LinearApiError>;
15
+ }
16
+
17
+ export const TeamRepository = Context.GenericTag<TeamRepository>("TeamRepository");
@@ -0,0 +1,87 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schema from "effect/Schema";
4
+ import type { JjNotInstalledError, VcsError } from "../domain/Errors.js";
5
+
6
+ // === VCS Domain Types ===
7
+
8
+ export const ChangeId = Schema.String.pipe(Schema.brand("ChangeId"));
9
+ export type ChangeId = typeof ChangeId.Type;
10
+
11
+ export class Change extends Schema.Class<Change>("Change")({
12
+ id: ChangeId,
13
+ changeId: Schema.String, // Short change id
14
+ description: Schema.String,
15
+ author: Schema.String,
16
+ timestamp: Schema.Date,
17
+ bookmarks: Schema.Array(Schema.String),
18
+ isWorkingCopy: Schema.Boolean,
19
+ isEmpty: Schema.Boolean,
20
+ }) {}
21
+
22
+ export class PushResult extends Schema.Class<PushResult>("PushResult")({
23
+ bookmark: Schema.String,
24
+ remote: Schema.String,
25
+ changeId: ChangeId,
26
+ }) {}
27
+
28
+ export interface VcsService {
29
+ /**
30
+ * Check if jj is available
31
+ */
32
+ readonly isAvailable: () => Effect.Effect<boolean, never>;
33
+
34
+ /**
35
+ * Check if current directory is a jj repo
36
+ */
37
+ readonly isRepo: () => Effect.Effect<boolean, VcsError>;
38
+
39
+ /**
40
+ * Create a new change (jj new)
41
+ */
42
+ readonly createChange: (
43
+ message: string,
44
+ ) => Effect.Effect<ChangeId, JjNotInstalledError | VcsError>;
45
+
46
+ /**
47
+ * Describe/update current change message
48
+ */
49
+ readonly describe: (message: string) => Effect.Effect<void, VcsError>;
50
+
51
+ /**
52
+ * Commit (jj commit) - creates new empty change on top
53
+ */
54
+ readonly commit: (message: string) => Effect.Effect<ChangeId, VcsError>;
55
+
56
+ /**
57
+ * Create a bookmark at current change
58
+ */
59
+ readonly createBookmark: (name: string, ref?: ChangeId) => Effect.Effect<void, VcsError>;
60
+
61
+ /**
62
+ * Push bookmark to remote
63
+ */
64
+ readonly push: (bookmark: string) => Effect.Effect<PushResult, VcsError>;
65
+
66
+ /**
67
+ * Get current change
68
+ */
69
+ readonly getCurrentChange: () => Effect.Effect<Change, VcsError>;
70
+
71
+ /**
72
+ * Get the stack of changes (from main to current)
73
+ */
74
+ readonly getStack: () => Effect.Effect<ReadonlyArray<Change>, VcsError>;
75
+
76
+ /**
77
+ * Get log of changes
78
+ */
79
+ readonly getLog: (revset?: string) => Effect.Effect<ReadonlyArray<Change>, VcsError>;
80
+
81
+ /**
82
+ * Fetch from remote
83
+ */
84
+ readonly fetch: () => Effect.Effect<void, VcsError>;
85
+ }
86
+
87
+ export class VcsService extends Context.Tag("VcsService")<VcsService, VcsService>() {}
@@ -0,0 +1,7 @@
1
+ export * from "./TeamRepository.js";
2
+ export * from "./ProjectRepository.js";
3
+ export * from "./IssueRepository.js";
4
+ export * from "./AuthService.js";
5
+ export * from "./ConfigRepository.js";
6
+ export * from "./VcsService.js";
7
+ export * from "./PrService.js";
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+
3
+ describe("Dummy", () => {
4
+ it("should pass", () => {
5
+ expect(true).toBe(true)
6
+ })
7
+ })
@@ -0,0 +1,45 @@
1
+ {
2
+ "include": [],
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "moduleDetection": "force",
6
+ "composite": true,
7
+ "downlevelIteration": true,
8
+ "resolveJsonModule": true,
9
+ "esModuleInterop": false,
10
+ "declaration": true,
11
+ "skipLibCheck": true,
12
+ "exactOptionalPropertyTypes": true,
13
+ "emitDecoratorMetadata": false,
14
+ "experimentalDecorators": true,
15
+ "moduleResolution": "NodeNext",
16
+ "lib": ["ES2022", "DOM"],
17
+ "isolatedModules": true,
18
+ "sourceMap": true,
19
+ "declarationMap": true,
20
+ "noImplicitReturns": false,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": false,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noEmitOnError": false,
25
+ "noErrorTruncation": false,
26
+ "allowJs": false,
27
+ "checkJs": false,
28
+ "forceConsistentCasingInFileNames": true,
29
+ "stripInternal": true,
30
+ "noImplicitAny": true,
31
+ "noImplicitThis": true,
32
+ "noUncheckedIndexedAccess": false,
33
+ "strictNullChecks": true,
34
+ "baseUrl": ".",
35
+ "target": "ES2022",
36
+ "module": "NodeNext",
37
+ "incremental": true,
38
+ "removeComments": false,
39
+ "plugins": [{ "name": "@effect/language-service" }],
40
+ "paths": {
41
+ "@ship/cli": ["./src/index.js"],
42
+ "@ship/cli/*": ["./src/*.js"]
43
+ }
44
+ }
45
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "references": [
4
+ { "path": "tsconfig.src.json" },
5
+ { "path": "tsconfig.test.json" }
6
+ ]
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "include": ["src"],
4
+ "compilerOptions": {
5
+ "types": ["node"],
6
+ "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo",
7
+ "rootDir": "src",
8
+ "outDir": "dist",
9
+ "noEmit": true
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "include": ["test"],
4
+ "compilerOptions": {
5
+ "types": ["node"],
6
+ "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo",
7
+ "rootDir": "test",
8
+ "noEmit": true
9
+ }
10
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "tsup"
2
+
3
+ export default defineConfig({
4
+ entry: ["src/bin.ts"],
5
+ outDir: "dist",
6
+ format: ["esm"],
7
+ clean: true,
8
+ treeshake: "smallest",
9
+ noExternal: [/.*/],
10
+ external: ["@parcel/watcher"],
11
+ banner: {
12
+ js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`
13
+ }
14
+ })
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["./test/**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
6
+ exclude: [],
7
+ globals: true,
8
+ coverage: {
9
+ provider: "v8"
10
+ }
11
+ }
12
+ })