@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,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);
|