@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,366 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import * as Layer from "effect/Layer";
3
- import * as Option from "effect/Option";
4
- import * as Schema from "effect/Schema";
5
- import * as FileSystem from "@effect/platform/FileSystem";
6
- import * as Path from "@effect/platform/Path";
7
- import * as YAML from "yaml";
8
- import {
9
- AuthConfig,
10
- GitConfig,
11
- LinearConfig,
12
- PartialShipConfig,
13
- PrConfig,
14
- CommitConfig,
15
- ShipConfig,
16
- } from "../../../domain/Config.js";
17
- import { ConfigError, WorkspaceNotInitializedError } from "../../../domain/Errors.js";
18
- import { ConfigRepository } from "../../../ports/ConfigRepository.js";
19
- import { TeamId, ProjectId } from "../../../domain/Task.js";
20
-
21
- // Helper to convert config strings to branded types
22
- // These are stored values that we trust as valid IDs
23
- const asTeamId = (s: string): typeof TeamId.Type => s as typeof TeamId.Type;
24
- const asProjectId = (s: string): typeof ProjectId.Type => s as typeof ProjectId.Type;
25
-
26
- const CONFIG_DIR = ".ship";
27
- const CONFIG_FILE = "config.yaml";
28
-
29
- // YAML representation (with optional fields for partial configs)
30
- const YamlConfig = Schema.Struct({
31
- linear: Schema.optional(
32
- Schema.Struct({
33
- teamId: Schema.String,
34
- teamKey: Schema.String,
35
- projectId: Schema.NullOr(Schema.String),
36
- }),
37
- ),
38
- auth: Schema.optional(
39
- Schema.Struct({
40
- apiKey: Schema.String,
41
- }),
42
- ),
43
- git: Schema.optional(
44
- Schema.Struct({
45
- defaultBranch: Schema.optional(Schema.String),
46
- }),
47
- ),
48
- pr: Schema.optional(
49
- Schema.Struct({
50
- openBrowser: Schema.optional(Schema.Boolean),
51
- }),
52
- ),
53
- commit: Schema.optional(
54
- Schema.Struct({
55
- conventionalFormat: Schema.optional(Schema.Boolean),
56
- }),
57
- ),
58
- });
59
-
60
- type YamlConfig = typeof YamlConfig.Type;
61
-
62
- interface MutableYamlConfig {
63
- linear?: {
64
- teamId: string;
65
- teamKey: string;
66
- projectId: string | null;
67
- };
68
- auth?: {
69
- apiKey: string;
70
- };
71
- git?: {
72
- defaultBranch?: string;
73
- };
74
- pr?: {
75
- openBrowser?: boolean;
76
- };
77
- commit?: {
78
- conventionalFormat?: boolean;
79
- };
80
- }
81
-
82
- const make = Effect.gen(function* () {
83
- const fs = yield* FileSystem.FileSystem;
84
- const path = yield* Path.Path;
85
-
86
- const getConfigDir = () => Effect.succeed(path.join(process.cwd(), CONFIG_DIR));
87
-
88
- const getConfigPath = () => Effect.map(getConfigDir(), (dir) => path.join(dir, CONFIG_FILE));
89
-
90
- const ensureConfigDir = () =>
91
- Effect.gen(function* () {
92
- const dir = yield* getConfigDir();
93
- const dirExists = yield* fs.exists(dir);
94
- if (!dirExists) {
95
- yield* fs.makeDirectory(dir, { recursive: true });
96
- }
97
- }).pipe(
98
- Effect.catchAll((e) =>
99
- Effect.fail(new ConfigError({ message: `Failed to create config directory: ${e}` })),
100
- ),
101
- );
102
-
103
- const ensureGitignore = () =>
104
- Effect.gen(function* () {
105
- const gitignorePath = path.join(process.cwd(), ".gitignore");
106
- const gitignoreExists = yield* fs.exists(gitignorePath);
107
-
108
- if (gitignoreExists) {
109
- const content = yield* fs.readFileString(gitignorePath);
110
- // Check if .ship is already in gitignore
111
- const lines = content.split("\n");
112
- const hasShip = lines.some((line) => line.trim() === ".ship" || line.trim() === ".ship/");
113
- if (!hasShip) {
114
- // Append .ship to gitignore
115
- const newContent = content.endsWith("\n") ? `${content}.ship/\n` : `${content}\n.ship/\n`;
116
- yield* fs.writeFileString(gitignorePath, newContent);
117
- }
118
- } else {
119
- // Create new .gitignore with .ship
120
- yield* fs.writeFileString(gitignorePath, ".ship/\n");
121
- }
122
- }).pipe(
123
- Effect.catchAll((e) =>
124
- Effect.fail(new ConfigError({ message: `Failed to update .gitignore: ${e}` })),
125
- ),
126
- );
127
-
128
- const exists = () =>
129
- Effect.gen(function* () {
130
- const configPath = yield* getConfigPath();
131
- return yield* fs.exists(configPath);
132
- }).pipe(Effect.catchAll(() => Effect.succeed(false)));
133
-
134
- const readYaml = (): Effect.Effect<YamlConfig | null, ConfigError> =>
135
- Effect.gen(function* () {
136
- const configPath = yield* getConfigPath();
137
- const fileExists = yield* fs.exists(configPath);
138
- if (!fileExists) {
139
- return null;
140
- }
141
- const content = yield* fs.readFileString(configPath);
142
-
143
- // Parse YAML
144
- let parsed: unknown;
145
- try {
146
- parsed = YAML.parse(content);
147
- } catch (e) {
148
- return yield* Effect.fail(
149
- new ConfigError({
150
- message: `Invalid YAML in .ship/config.yaml: ${e instanceof Error ? e.message : e}`,
151
- cause: e,
152
- }),
153
- );
154
- }
155
-
156
- // Validate schema
157
- return yield* Schema.decodeUnknown(YamlConfig)(parsed).pipe(
158
- Effect.mapError(
159
- (e) =>
160
- new ConfigError({
161
- message: `Invalid config in .ship/config.yaml. Run 'ship init' to reconfigure.\nDetails: ${e.message}`,
162
- cause: e,
163
- }),
164
- ),
165
- );
166
- }).pipe(
167
- Effect.catchAll((e) => {
168
- if (e instanceof ConfigError) {
169
- return Effect.fail(e);
170
- }
171
- return Effect.fail(new ConfigError({ message: `Failed to read config: ${e}`, cause: e }));
172
- }),
173
- );
174
-
175
- const writeYaml = (yamlConfig: MutableYamlConfig): Effect.Effect<void, ConfigError> =>
176
- Effect.gen(function* () {
177
- yield* ensureConfigDir();
178
- const configPath = yield* getConfigPath();
179
- const content = YAML.stringify(yamlConfig);
180
- yield* fs.writeFileString(configPath, content);
181
- }).pipe(
182
- Effect.catchAll((e) =>
183
- Effect.fail(new ConfigError({ message: `Failed to write config: ${e}`, cause: e })),
184
- ),
185
- );
186
-
187
- const yamlToPartial = (yaml: YamlConfig | null): PartialShipConfig => {
188
- if (!yaml) {
189
- return new PartialShipConfig({
190
- linear: Option.none(),
191
- auth: Option.none(),
192
- });
193
- }
194
-
195
- return new PartialShipConfig({
196
- linear: yaml.linear
197
- ? Option.some(
198
- new LinearConfig({
199
- teamId: asTeamId(yaml.linear.teamId),
200
- teamKey: yaml.linear.teamKey,
201
- projectId: yaml.linear.projectId
202
- ? Option.some(asProjectId(yaml.linear.projectId))
203
- : Option.none(),
204
- }),
205
- )
206
- : Option.none(),
207
- auth: yaml.auth ? Option.some(new AuthConfig({ apiKey: yaml.auth.apiKey })) : Option.none(),
208
- });
209
- };
210
-
211
- const partialToYaml = (partialConfig: PartialShipConfig): MutableYamlConfig => {
212
- const yaml: MutableYamlConfig = {};
213
-
214
- if (Option.isSome(partialConfig.linear)) {
215
- const linear = partialConfig.linear.value;
216
- yaml.linear = {
217
- teamId: linear.teamId,
218
- teamKey: linear.teamKey,
219
- projectId: Option.isSome(linear.projectId) ? linear.projectId.value : null,
220
- };
221
- }
222
-
223
- if (Option.isSome(partialConfig.auth)) {
224
- yaml.auth = { apiKey: partialConfig.auth.value.apiKey };
225
- }
226
-
227
- if (partialConfig.git) {
228
- yaml.git = { defaultBranch: partialConfig.git.defaultBranch };
229
- }
230
-
231
- if (partialConfig.pr) {
232
- yaml.pr = { openBrowser: partialConfig.pr.openBrowser };
233
- }
234
-
235
- if (partialConfig.commit) {
236
- yaml.commit = { conventionalFormat: partialConfig.commit.conventionalFormat };
237
- }
238
-
239
- return yaml;
240
- };
241
-
242
- const fullToYaml = (fullConfig: ShipConfig): MutableYamlConfig => ({
243
- linear: {
244
- teamId: fullConfig.linear.teamId,
245
- teamKey: fullConfig.linear.teamKey,
246
- projectId: Option.isSome(fullConfig.linear.projectId)
247
- ? fullConfig.linear.projectId.value
248
- : null,
249
- },
250
- auth: { apiKey: fullConfig.auth.apiKey },
251
- git: { defaultBranch: fullConfig.git.defaultBranch },
252
- pr: { openBrowser: fullConfig.pr.openBrowser },
253
- commit: { conventionalFormat: fullConfig.commit.conventionalFormat },
254
- });
255
-
256
- const load = (): Effect.Effect<ShipConfig, WorkspaceNotInitializedError | ConfigError> =>
257
- Effect.gen(function* () {
258
- const yaml = yield* readYaml();
259
- if (!yaml || !yaml.linear || !yaml.auth) {
260
- return yield* Effect.fail(WorkspaceNotInitializedError.default);
261
- }
262
-
263
- return new ShipConfig({
264
- linear: new LinearConfig({
265
- teamId: asTeamId(yaml.linear.teamId),
266
- teamKey: yaml.linear.teamKey,
267
- projectId: yaml.linear.projectId
268
- ? Option.some(asProjectId(yaml.linear.projectId))
269
- : Option.none(),
270
- }),
271
- auth: new AuthConfig({ apiKey: yaml.auth.apiKey }),
272
- git: new GitConfig({ defaultBranch: yaml.git?.defaultBranch ?? "main" }),
273
- pr: new PrConfig({ openBrowser: yaml.pr?.openBrowser ?? true }),
274
- commit: new CommitConfig({ conventionalFormat: yaml.commit?.conventionalFormat ?? true }),
275
- });
276
- });
277
-
278
- const loadPartial = () =>
279
- Effect.gen(function* () {
280
- const yaml = yield* readYaml();
281
- return yamlToPartial(yaml);
282
- });
283
-
284
- const save = (fullConfig: ShipConfig) =>
285
- Effect.gen(function* () {
286
- const yaml = fullToYaml(fullConfig);
287
- yield* writeYaml(yaml);
288
- });
289
-
290
- const savePartial = (partialConfig: PartialShipConfig) =>
291
- Effect.gen(function* () {
292
- const yaml = partialToYaml(partialConfig);
293
- yield* writeYaml(yaml);
294
- });
295
-
296
- const saveAuth = (auth: AuthConfig) =>
297
- Effect.gen(function* () {
298
- const existingYaml = yield* readYaml();
299
- const yaml: MutableYamlConfig = {};
300
- if (existingYaml?.linear) {
301
- yaml.linear = {
302
- teamId: existingYaml.linear.teamId,
303
- teamKey: existingYaml.linear.teamKey,
304
- projectId: existingYaml.linear.projectId,
305
- };
306
- }
307
- if (existingYaml?.git?.defaultBranch)
308
- yaml.git = { defaultBranch: existingYaml.git.defaultBranch };
309
- if (existingYaml?.pr?.openBrowser !== undefined)
310
- yaml.pr = { openBrowser: existingYaml.pr.openBrowser };
311
- if (existingYaml?.commit?.conventionalFormat !== undefined)
312
- yaml.commit = { conventionalFormat: existingYaml.commit.conventionalFormat };
313
- yaml.auth = { apiKey: auth.apiKey };
314
- yield* writeYaml(yaml);
315
- });
316
-
317
- const saveLinear = (linear: LinearConfig) =>
318
- Effect.gen(function* () {
319
- const existingYaml = yield* readYaml();
320
- const yaml: MutableYamlConfig = {};
321
- if (existingYaml?.auth) {
322
- yaml.auth = { apiKey: existingYaml.auth.apiKey };
323
- }
324
- if (existingYaml?.git?.defaultBranch)
325
- yaml.git = { defaultBranch: existingYaml.git.defaultBranch };
326
- if (existingYaml?.pr?.openBrowser !== undefined)
327
- yaml.pr = { openBrowser: existingYaml.pr.openBrowser };
328
- if (existingYaml?.commit?.conventionalFormat !== undefined)
329
- yaml.commit = { conventionalFormat: existingYaml.commit.conventionalFormat };
330
- yaml.linear = {
331
- teamId: linear.teamId,
332
- teamKey: linear.teamKey,
333
- projectId: Option.isSome(linear.projectId) ? linear.projectId.value : null,
334
- };
335
- yield* writeYaml(yaml);
336
- });
337
-
338
- const deleteConfig = () =>
339
- Effect.gen(function* () {
340
- const configPath = yield* getConfigPath();
341
- const fileExists = yield* fs.exists(configPath);
342
- if (fileExists) {
343
- yield* fs.remove(configPath);
344
- }
345
- }).pipe(
346
- Effect.catchAll((e) =>
347
- Effect.fail(new ConfigError({ message: `Failed to delete config: ${e}`, cause: e })),
348
- ),
349
- );
350
-
351
- return {
352
- load,
353
- loadPartial,
354
- save,
355
- savePartial,
356
- saveAuth,
357
- saveLinear,
358
- exists,
359
- getConfigDir,
360
- ensureConfigDir,
361
- ensureGitignore,
362
- delete: deleteConfig,
363
- };
364
- });
365
-
366
- export const ConfigRepositoryLive = Layer.effect(ConfigRepository, make);