monoverse 0.0.10 → 0.0.12

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/dist/cli.js CHANGED
@@ -1,481 +1,699 @@
1
- // cli.ts
2
- import * as trpcExpress from "@trpc/server/adapters/express";
3
- import express from "express";
4
- import { fileURLToPath } from "url";
5
- import path4 from "path";
6
-
7
- // trpc/server.ts
8
- import { z as z5 } from "zod";
9
-
10
- // domain/core/schema/schema.ts
11
- import { z } from "zod";
12
- var dependencySchema = z.object({
13
- name: z.string(),
14
- versionRange: z.string(),
15
- type: z.enum([
16
- "dependency",
17
- "devDependency",
18
- "peerDependency",
19
- "optionalDependency"
20
- ])
1
+ import { Command, Args, Options, CliConfig } from "@effect/cli";
2
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
3
+ import { Data, Effect, Schema, Either, Console, Layer } from "effect";
4
+ import yaml from "js-yaml";
5
+ import * as fs from "node:fs/promises";
6
+ import * as nodePath from "node:path";
7
+ import { glob as glob$1 } from "tinyglobby";
8
+ import sortPackageJson from "sort-package-json";
9
+ import * as semver from "semver";
10
+ class FsError extends Data.TaggedError("FsError") {
11
+ }
12
+ Effect.sync(() => process.cwd());
13
+ const getParentDirectory = (dirPath) => Effect.sync(() => nodePath.dirname(dirPath));
14
+ const readFile = (filePath) => Effect.tryPromise({
15
+ try: () => fs.readFile(filePath, "utf-8"),
16
+ catch: (cause) => new FsError({ path: filePath, cause })
21
17
  });
22
- var workspaceSchema = z.object({
23
- name: z.string(),
24
- description: z.string().optional(),
25
- dependencies: z.array(dependencySchema)
18
+ const writeFile = (filePath, content) => Effect.tryPromise({
19
+ try: () => fs.writeFile(filePath, content, "utf-8"),
20
+ catch: (cause) => new FsError({ path: filePath, cause })
26
21
  });
27
- var monorepoSchema = z.object({
28
- workspaces: z.array(workspaceSchema)
22
+ const fileExists = (filePath) => Effect.tryPromise({
23
+ try: async () => {
24
+ await fs.access(filePath);
25
+ return true;
26
+ },
27
+ catch: () => false
28
+ }).pipe(Effect.catchAll(() => Effect.succeed(false)));
29
+ const joinPath = (...paths) => nodePath.join(...paths);
30
+ const basename = (path) => nodePath.basename(path);
31
+ const dirname = (path) => nodePath.dirname(path);
32
+ const isRootPath = (dirPath) => nodePath.dirname(dirPath) === dirPath;
33
+ const glob = (patterns, options) => Effect.tryPromise({
34
+ try: () => glob$1(patterns, {
35
+ cwd: options.cwd,
36
+ ignore: options.ignore ?? [],
37
+ absolute: options.absolute ?? true
38
+ }),
39
+ catch: (cause) => new FsError({ path: options.cwd, cause })
29
40
  });
30
- var dependencyRecordSchema = z.record(z.string());
31
- var packageJsonSchema = z.object({
32
- name: z.string(),
33
- description: z.string().optional(),
34
- version: z.string().optional(),
35
- // Dependencies
36
- dependencies: dependencyRecordSchema.optional(),
37
- devDependencies: dependencyRecordSchema.optional(),
38
- peerDependencies: dependencyRecordSchema.optional(),
39
- optionalDependencies: dependencyRecordSchema.optional()
40
- }).passthrough();
41
-
42
- // domain/tools/ts.ts
43
- var removeUndefined = (obj) => {
44
- const result = { ...obj };
45
- for (const key in result) {
46
- const value = result[key];
47
- if (value === void 0) {
48
- delete result[key];
49
- }
41
+ class NotAMonorepoError extends Data.TaggedError("NotAMonorepoError") {
42
+ }
43
+ const PackageJsonSchema = Schema.Struct({
44
+ workspaces: Schema.optional(
45
+ Schema.Union(
46
+ Schema.mutable(Schema.Array(Schema.String)),
47
+ Schema.Struct({
48
+ packages: Schema.optional(Schema.mutable(Schema.Array(Schema.String)))
49
+ })
50
+ )
51
+ )
52
+ });
53
+ const PnpmWorkspaceSchema = Schema.Struct({
54
+ packages: Schema.optional(Schema.mutable(Schema.Array(Schema.String)))
55
+ });
56
+ const LOCK_FILES = [
57
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
58
+ { file: "yarn.lock", pm: "yarn" },
59
+ { file: "package-lock.json", pm: "npm" },
60
+ { file: "bun.lockb", pm: "bun" }
61
+ ];
62
+ const detectPackageManager = (dirPath) => Effect.gen(function* () {
63
+ for (const { file, pm } of LOCK_FILES) {
64
+ const exists = yield* fileExists(joinPath(dirPath, file));
65
+ if (exists) return pm;
50
66
  }
51
- return result;
52
- };
53
-
54
- // domain/core/schema/transform.ts
55
- var transformPackageJsonToWorkspace = (packageJson) => {
56
- const validatedPackageJson = packageJsonSchema.parse(packageJson);
57
- const {
58
- dependencies,
59
- devDependencies,
60
- peerDependencies,
61
- optionalDependencies
62
- } = validatedPackageJson;
63
- const getDependencies = (list, type) => {
64
- if (!list)
65
- return [];
66
- return Object.entries(list).map(([name, versionRange]) => {
67
+ return "unknown";
68
+ });
69
+ const parseYaml = (content) => Effect.try(() => yaml.load(content)).pipe(
70
+ Effect.flatMap(Schema.decodeUnknown(PnpmWorkspaceSchema))
71
+ );
72
+ const parsePackageJson$1 = (content) => Effect.try(() => JSON.parse(content)).pipe(
73
+ Effect.flatMap(Schema.decodeUnknown(PackageJsonSchema))
74
+ );
75
+ const getPnpmWorkspacePatterns = (dirPath) => Effect.gen(function* () {
76
+ const wsPath = joinPath(dirPath, "pnpm-workspace.yaml");
77
+ const exists = yield* fileExists(wsPath);
78
+ if (!exists) return null;
79
+ const content = yield* readFile(wsPath).pipe(
80
+ Effect.catchAll(() => Effect.succeed(""))
81
+ );
82
+ if (!content) return null;
83
+ const result = yield* parseYaml(content).pipe(
84
+ Effect.catchAll(() => Effect.succeed(null))
85
+ );
86
+ return result?.packages ?? null;
87
+ });
88
+ const getPackageJsonWorkspacePatterns = (dirPath) => Effect.gen(function* () {
89
+ const pkgPath = joinPath(dirPath, "package.json");
90
+ const exists = yield* fileExists(pkgPath);
91
+ if (!exists) return null;
92
+ const content = yield* readFile(pkgPath).pipe(
93
+ Effect.catchAll(() => Effect.succeed(""))
94
+ );
95
+ if (!content) return null;
96
+ const pkg = yield* parsePackageJson$1(content).pipe(
97
+ Effect.catchAll(() => Effect.succeed(null))
98
+ );
99
+ if (!pkg) return null;
100
+ if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
101
+ if (Array.isArray(pkg.workspaces?.packages)) return pkg.workspaces.packages;
102
+ return null;
103
+ });
104
+ const hasPackageJson = (dirPath) => fileExists(joinPath(dirPath, "package.json"));
105
+ const toPackageJsonGlob = (patterns) => patterns.map((p) => joinPath(p, "package.json"));
106
+ const findMonorepoRoot = (startPath, options = {}) => Effect.gen(function* () {
107
+ let currentPath = startPath;
108
+ let singleRepoCandidate = null;
109
+ const stopAt = options.stopAt;
110
+ while (true) {
111
+ const pnpmPatterns = yield* getPnpmWorkspacePatterns(currentPath);
112
+ if (pnpmPatterns) {
67
113
  return {
68
- name,
69
- versionRange,
70
- type
114
+ root: currentPath,
115
+ packageManager: "pnpm",
116
+ patterns: toPackageJsonGlob(pnpmPatterns)
71
117
  };
72
- });
73
- };
74
- return workspaceSchema.parse(
75
- // removeUndefined to ensure that the workspace doesn't have any undefined
76
- removeUndefined({
77
- name: validatedPackageJson.name,
78
- description: validatedPackageJson.description,
79
- dependencies: [
80
- ...getDependencies(dependencies, "dependency"),
81
- ...getDependencies(devDependencies, "devDependency"),
82
- ...getDependencies(peerDependencies, "peerDependency"),
83
- ...getDependencies(optionalDependencies, "optionalDependency")
84
- ]
118
+ }
119
+ const pkgPatterns = yield* getPackageJsonWorkspacePatterns(currentPath);
120
+ if (pkgPatterns) {
121
+ const pm = yield* detectPackageManager(currentPath);
122
+ return {
123
+ root: currentPath,
124
+ packageManager: pm,
125
+ patterns: toPackageJsonGlob(pkgPatterns)
126
+ };
127
+ }
128
+ if (!singleRepoCandidate) {
129
+ const hasPkg = yield* hasPackageJson(currentPath);
130
+ if (hasPkg) {
131
+ singleRepoCandidate = currentPath;
132
+ }
133
+ }
134
+ if (isRootPath(currentPath)) break;
135
+ if (stopAt && currentPath === stopAt) break;
136
+ currentPath = yield* getParentDirectory(currentPath);
137
+ }
138
+ if (singleRepoCandidate) {
139
+ const pm = yield* detectPackageManager(singleRepoCandidate);
140
+ return {
141
+ root: singleRepoCandidate,
142
+ packageManager: pm,
143
+ patterns: ["./package.json"]
144
+ };
145
+ }
146
+ return yield* Effect.fail(
147
+ new NotAMonorepoError({
148
+ startPath,
149
+ message: "No package.json found in directory tree"
85
150
  })
86
151
  );
152
+ });
153
+ const parseDependencySource = (versionRange) => {
154
+ if (versionRange.startsWith("file:") || versionRange.startsWith("./") || versionRange.startsWith("../") || versionRange.startsWith("/")) {
155
+ return "file";
156
+ }
157
+ if (versionRange.startsWith("git+") || versionRange.startsWith("git://") || versionRange.startsWith("github:") || versionRange.startsWith("gitlab:") || versionRange.startsWith("bitbucket:")) {
158
+ return "git";
159
+ }
160
+ if (versionRange.startsWith("http://") || versionRange.startsWith("https://")) {
161
+ return "url";
162
+ }
163
+ return "npm";
87
164
  };
88
-
89
- // domain/core/version.ts
90
- import { gt, maxSatisfying, minSatisfying } from "semver";
91
-
92
- // domain/implementation/api/bundlephobia.ts
93
- import { z as z2 } from "zod";
94
- var requestSchema = z2.string();
95
- var responseSchema = z2.object({
96
- assets: z2.array(
97
- z2.object({
98
- gzip: z2.number(),
99
- name: z2.string(),
100
- size: z2.number(),
101
- type: z2.string()
102
- })
165
+ const parseDependencies = (deps, dependencyType, workspaceNames) => {
166
+ if (!deps) return [];
167
+ return Object.entries(deps).map(([name, versionRange]) => ({
168
+ name,
169
+ versionRange,
170
+ dependencyType,
171
+ source: workspaceNames.has(name) ? "workspace" : parseDependencySource(versionRange)
172
+ }));
173
+ };
174
+ const RawPackageJsonSchema = Schema.Struct({
175
+ name: Schema.optional(Schema.String),
176
+ version: Schema.optional(Schema.String),
177
+ private: Schema.optional(Schema.Boolean),
178
+ dependencies: Schema.optional(
179
+ Schema.Record({ key: Schema.String, value: Schema.String })
180
+ ),
181
+ devDependencies: Schema.optional(
182
+ Schema.Record({ key: Schema.String, value: Schema.String })
103
183
  ),
104
- dependencyCount: z2.number(),
105
- dependencySizes: z2.array(
106
- z2.object({ approximateSize: z2.number(), name: z2.string() })
184
+ peerDependencies: Schema.optional(
185
+ Schema.Record({ key: Schema.String, value: Schema.String })
107
186
  ),
108
- description: z2.string().optional(),
109
- name: z2.string(),
110
- version: z2.string()
187
+ optionalDependencies: Schema.optional(
188
+ Schema.Record({ key: Schema.String, value: Schema.String })
189
+ )
111
190
  });
112
-
113
- // domain/implementation/api/package-info.ts
114
- import { z as z4 } from "zod";
115
-
116
- // domain/implementation/api/schema.ts
117
- import { z as z3 } from "zod";
118
- var packageSchema = z3.object({
119
- name: z3.string(),
120
- description: z3.string().optional(),
121
- repository: z3.string().optional(),
122
- licence: z3.string().optional(),
123
- versions: z3.array(z3.string())
191
+ const parseWorkspace = (pkgPath, raw, workspaceNames) => ({
192
+ name: raw.name ?? basename(dirname(pkgPath)),
193
+ version: raw.version ?? "0.0.0",
194
+ path: dirname(pkgPath),
195
+ private: raw.private ?? false,
196
+ dependencies: [
197
+ ...parseDependencies(raw.dependencies, "dependency", workspaceNames),
198
+ ...parseDependencies(raw.devDependencies, "devDependency", workspaceNames),
199
+ ...parseDependencies(
200
+ raw.peerDependencies,
201
+ "peerDependency",
202
+ workspaceNames
203
+ ),
204
+ ...parseDependencies(
205
+ raw.optionalDependencies,
206
+ "optionalDependency",
207
+ workspaceNames
208
+ )
209
+ ]
124
210
  });
125
-
126
- // domain/implementation/api/package-info.ts
127
- var requestSchema2 = z4.string();
128
- var responseSchema2 = z4.object({
129
- name: z4.string(),
130
- versions: z4.record(z4.any()),
131
- description: z4.string().optional(),
132
- repository: z4.object({
133
- url: z4.string().optional()
134
- }).optional(),
135
- licence: z4.string().optional()
211
+ const parsePackageJson = (content) => Effect.try(() => JSON.parse(content)).pipe(
212
+ Effect.flatMap(Schema.decodeUnknown(RawPackageJsonSchema))
213
+ );
214
+ const discoverWorkspaces = (root, patterns) => Effect.gen(function* () {
215
+ const workspaces = [];
216
+ const errors = [];
217
+ const workspacePaths = yield* glob(patterns, {
218
+ cwd: root,
219
+ ignore: ["**/node_modules/**"],
220
+ absolute: true
221
+ }).pipe(Effect.catchAll(() => Effect.succeed([])));
222
+ const rawPackages = [];
223
+ const workspaceNames = /* @__PURE__ */ new Set();
224
+ for (const workspacePath of workspacePaths) {
225
+ const result = yield* readFile(workspacePath).pipe(
226
+ Effect.flatMap(parsePackageJson),
227
+ Effect.either
228
+ );
229
+ if (Either.isRight(result)) {
230
+ const rawPackageJson = result.right;
231
+ const name = rawPackageJson.name ?? basename(dirname(workspacePath));
232
+ workspaceNames.add(name);
233
+ rawPackages.push({ path: workspacePath, raw: rawPackageJson });
234
+ } else {
235
+ errors.push({
236
+ path: workspacePath,
237
+ message: "Failed to parse package.json",
238
+ cause: result.left
239
+ });
240
+ }
241
+ }
242
+ for (const { path: pkgPath, raw } of rawPackages) {
243
+ workspaces.push(parseWorkspace(pkgPath, raw, workspaceNames));
244
+ }
245
+ return { workspaces, errors };
136
246
  });
137
-
138
- // domain/implementation/filesystem/filesystem.ts
139
- import { getPackages as getWorkspaces } from "@monorepo-utils/package-utils";
140
- import fs from "fs";
141
- import path from "path";
142
- import invariant from "tiny-invariant";
143
- var getMonorepoInfo = (dirPath) => {
144
- const monorepoDir = detectMonorepoDir(dirPath);
145
- const workspaceDir = detectWorkspaceDir(dirPath);
146
- if (monorepoDir) {
147
- const workspaces = getMonorepoWorkspacesAtDir(monorepoDir);
148
- invariant(workspaces !== void 0, "monorepo should be defined");
149
- return {
150
- workspaces
151
- };
152
- } else if (workspaceDir) {
153
- const workspace = getWorkspaceAtDir(workspaceDir);
154
- invariant(workspace !== void 0, "workspace should be defined");
155
- return {
156
- workspaces: [workspace]
157
- };
247
+ const getPackageJsonStr = (workspace) => readFile(joinPath(workspace.path, "package.json"));
248
+ const analyzeMonorepo = (startPath) => Effect.gen(function* () {
249
+ const { root, packageManager, patterns } = yield* findMonorepoRoot(startPath);
250
+ const { workspaces, errors } = yield* discoverWorkspaces(root, patterns);
251
+ return {
252
+ root,
253
+ packageManager,
254
+ workspaces,
255
+ errors
256
+ };
257
+ });
258
+ class ModifyError extends Data.TaggedError("ModifyError") {
259
+ }
260
+ class DependencyNotFoundError extends Data.TaggedError(
261
+ "DependencyNotFoundError"
262
+ ) {
263
+ }
264
+ class PackageJsonParseError extends Data.TaggedError(
265
+ "PackageJsonParseError"
266
+ ) {
267
+ }
268
+ class PackageJsonWriteError extends Data.TaggedError(
269
+ "PackageJsonWriteError"
270
+ ) {
271
+ }
272
+ const ALL_DEPENDENCY_KEYS = [
273
+ "dependencies",
274
+ "devDependencies",
275
+ "peerDependencies",
276
+ "optionalDependencies"
277
+ ];
278
+ const readPackageJson = (workspace) => Effect.gen(function* () {
279
+ const filePath = joinPath(workspace.path, "package.json");
280
+ const raw = yield* readFile(filePath).pipe(
281
+ Effect.mapError(
282
+ (e) => new PackageJsonParseError({ workspace: workspace.name, cause: e })
283
+ )
284
+ );
285
+ return yield* Effect.try({
286
+ try: () => JSON.parse(raw),
287
+ catch: (cause) => new PackageJsonParseError({ workspace: workspace.name, cause })
288
+ });
289
+ });
290
+ const writePackageJson = (workspace, content) => Effect.gen(function* () {
291
+ const filePath = joinPath(workspace.path, "package.json");
292
+ const formatted = sortPackageJson(content);
293
+ const current = yield* readFile(filePath).pipe(
294
+ Effect.mapError(
295
+ (e) => new PackageJsonWriteError({ workspace: workspace.name, cause: e })
296
+ )
297
+ );
298
+ if (current === formatted) {
299
+ return;
158
300
  }
159
- return void 0;
301
+ yield* writeFile(filePath, formatted).pipe(
302
+ Effect.mapError(
303
+ (e) => new PackageJsonWriteError({ workspace: workspace.name, cause: e })
304
+ )
305
+ );
306
+ });
307
+ const DEPENDENCY_TYPE_TO_KEY = {
308
+ dependency: "dependencies",
309
+ devDependency: "devDependencies",
310
+ peerDependency: "peerDependencies",
311
+ optionalDependency: "optionalDependencies"
160
312
  };
161
- var detectMonorepoDir = (dirPath) => {
162
- dirPath = path.resolve(dirPath);
163
- while (true) {
164
- const monorepo = getMonorepoWorkspacesAtDir(dirPath);
165
- if (monorepo)
166
- return dirPath;
167
- const parentDir = path.dirname(dirPath);
168
- if (parentDir === dirPath)
169
- return void 0;
170
- dirPath = parentDir;
313
+ const upsertDependency = (options) => Effect.gen(function* () {
314
+ const { workspace, dependencyName, versionRange, dependencyType } = options;
315
+ const content = yield* readPackageJson(workspace).pipe(
316
+ Effect.mapError(
317
+ (e) => new ModifyError({
318
+ workspace: workspace.name,
319
+ message: "Failed to read package.json",
320
+ cause: e
321
+ })
322
+ )
323
+ );
324
+ const targetKey = DEPENDENCY_TYPE_TO_KEY[dependencyType];
325
+ for (const key of ALL_DEPENDENCY_KEYS) {
326
+ if (key === targetKey) continue;
327
+ const deps = content[key];
328
+ if (deps && dependencyName in deps) {
329
+ delete deps[dependencyName];
330
+ if (Object.keys(deps).length === 0) {
331
+ delete content[key];
332
+ }
333
+ }
171
334
  }
172
- };
173
- function getMonorepoWorkspacesAtDir(dir) {
174
- const workspaces = getWorkspaces(dir);
175
- if (!workspaces || workspaces.length === 0)
176
- return void 0;
177
- return workspaces.map((workspace) => {
178
- const { packageJSON, location } = workspace;
179
- const parsedPackageJson = packageJsonSchema.parse(packageJSON);
180
- return {
181
- workspace: transformPackageJsonToWorkspace(packageJSON),
182
- location,
183
- packageJSON: parsedPackageJson
184
- };
185
- });
186
- }
187
- function detectWorkspaceDir(dirPath) {
188
- while (true) {
189
- const workspace = getWorkspaceAtDir(dirPath);
190
- if (workspace)
191
- return dirPath;
192
- const parentDir = path.dirname(dirPath);
193
- if (parentDir === dirPath)
194
- return void 0;
195
- dirPath = parentDir;
335
+ if (!content[targetKey]) {
336
+ content[targetKey] = {};
196
337
  }
197
- }
198
- function getWorkspaceAtDir(dir) {
199
- const workspacePackageJson = path.resolve(dir, "package.json");
200
- try {
201
- const packageJson = JSON.parse(
202
- fs.readFileSync(workspacePackageJson, "utf-8")
338
+ content[targetKey][dependencyName] = versionRange;
339
+ yield* writePackageJson(workspace, JSON.stringify(content)).pipe(
340
+ Effect.mapError(
341
+ (e) => new ModifyError({
342
+ workspace: workspace.name,
343
+ message: "Failed to write package.json",
344
+ cause: e
345
+ })
346
+ )
347
+ );
348
+ });
349
+ const formatPackageJson = (workspace) => Effect.gen(function* () {
350
+ const filePath = joinPath(workspace.path, "package.json");
351
+ const content = yield* readFile(filePath).pipe(
352
+ Effect.mapError(
353
+ (cause) => new ModifyError({
354
+ workspace: workspace.name,
355
+ message: "Failed to read package.json",
356
+ cause
357
+ })
358
+ )
359
+ );
360
+ yield* writePackageJson(workspace, content).pipe(
361
+ Effect.mapError(
362
+ (cause) => new ModifyError({
363
+ workspace: workspace.name,
364
+ message: "Failed to write package.json",
365
+ cause
366
+ })
367
+ )
368
+ );
369
+ });
370
+ const removeDependency = (options) => Effect.gen(function* () {
371
+ const { workspace, dependencyName } = options;
372
+ const content = yield* readPackageJson(workspace).pipe(
373
+ Effect.mapError(
374
+ (e) => new ModifyError({
375
+ workspace: workspace.name,
376
+ message: "Failed to read package.json",
377
+ cause: e
378
+ })
379
+ )
380
+ );
381
+ let removed = false;
382
+ for (const key of ALL_DEPENDENCY_KEYS) {
383
+ const deps = content[key];
384
+ if (deps && dependencyName in deps) {
385
+ delete deps[dependencyName];
386
+ removed = true;
387
+ if (Object.keys(deps).length === 0) {
388
+ delete content[key];
389
+ }
390
+ }
391
+ }
392
+ if (!removed) {
393
+ return yield* Effect.fail(
394
+ new DependencyNotFoundError({
395
+ workspace: workspace.name,
396
+ dependencyName
397
+ })
203
398
  );
204
- const parsedPackageJson = packageJsonSchema.parse(packageJson);
205
- return {
206
- location: dir,
207
- packageJSON: parsedPackageJson,
208
- workspace: transformPackageJsonToWorkspace(packageJson)
209
- };
210
- } catch {
211
- return void 0;
212
399
  }
400
+ yield* writePackageJson(workspace, JSON.stringify(content)).pipe(
401
+ Effect.mapError(
402
+ (e) => new ModifyError({
403
+ workspace: workspace.name,
404
+ message: "Failed to write package.json",
405
+ cause: e
406
+ })
407
+ )
408
+ );
409
+ });
410
+ function groupDependenciesByPackage(analysis, filterBySources = ["npm"]) {
411
+ const sourceSet = new Set(filterBySources);
412
+ const grouped = /* @__PURE__ */ new Map();
413
+ for (const workspace of analysis.workspaces) {
414
+ for (const dep of workspace.dependencies) {
415
+ if (!sourceSet.has(dep.source)) {
416
+ continue;
417
+ }
418
+ const instance = {
419
+ workspace: workspace.name,
420
+ versionRange: dep.versionRange,
421
+ type: dep.dependencyType
422
+ };
423
+ const existing = grouped.get(dep.name);
424
+ if (existing) {
425
+ existing.push(instance);
426
+ } else {
427
+ grouped.set(dep.name, [instance]);
428
+ }
429
+ }
430
+ }
431
+ return Array.from(grouped.entries()).map(([name, instances]) => ({ name, instances })).sort((a, b) => a.name.localeCompare(b.name));
213
432
  }
214
-
215
- // domain/implementation/filesystem/sync-dependency-versions.ts
216
- import fs2 from "fs";
217
- import path2 from "path";
218
- import { sortPackageJson } from "sort-package-json";
219
- var syncDependencyVersions = (monorepoInfo, updates) => {
220
- try {
221
- const updatedWorkspaces = /* @__PURE__ */ new Set();
222
- for (const update of updates) {
223
- for (const { packageJSON, workspace } of monorepoInfo.workspaces) {
224
- if (packageJSON.dependencies?.[update.dependencyName]) {
225
- packageJSON.dependencies[update.dependencyName] = update.versionRange;
226
- updatedWorkspaces.add(workspace.name);
227
- }
228
- if (packageJSON.devDependencies?.[update.dependencyName]) {
229
- packageJSON.devDependencies[update.dependencyName] = update.versionRange;
230
- updatedWorkspaces.add(workspace.name);
231
- }
232
- if (packageJSON.peerDependencies?.[update.dependencyName]) {
233
- packageJSON.peerDependencies[update.dependencyName] = update.versionRange;
234
- updatedWorkspaces.add(workspace.name);
433
+ class InvalidSemverRangeError extends Data.TaggedError(
434
+ "InvalidSemverRangeError"
435
+ ) {
436
+ }
437
+ const isPinnedVersion = (versionRange) => semver.valid(versionRange.trim()) !== null;
438
+ function detectUnpinnedVersions(analysis) {
439
+ return Effect.sync(() => {
440
+ const violations = [];
441
+ const dependenciesByPackage = groupDependenciesByPackage(analysis, ["npm"]);
442
+ for (const dep of dependenciesByPackage) {
443
+ for (const instance of dep.instances) {
444
+ if (instance.type === "peerDependency") continue;
445
+ if (!isPinnedVersion(instance.versionRange)) {
446
+ violations.push({
447
+ _tag: "ViolationUnpinnedVersion",
448
+ package: dep.name,
449
+ workspace: instance.workspace,
450
+ message: `Version range "${instance.versionRange}" is not pinned`,
451
+ versionRange: instance.versionRange,
452
+ dependencyType: instance.type
453
+ });
235
454
  }
236
455
  }
237
456
  }
238
- for (const {
239
- location,
240
- workspace,
241
- packageJSON
242
- } of monorepoInfo.workspaces) {
243
- if (updatedWorkspaces.has(workspace.name)) {
244
- fs2.writeFileSync(
245
- path2.join(location, "package.json"),
246
- sortPackageJson(JSON.stringify(packageJSON, null, 2))
247
- );
457
+ return violations;
458
+ });
459
+ }
460
+ function detectVersionMismatches(analysis) {
461
+ return Effect.sync(() => {
462
+ const violations = [];
463
+ const dependenciesByPackage = groupDependenciesByPackage(analysis, ["npm"]);
464
+ for (const dep of dependenciesByPackage) {
465
+ const instances = dep.instances.filter((i) => i.type !== "peerDependency");
466
+ if (instances.length < 2) continue;
467
+ const versions = new Set(instances.map((i) => i.versionRange));
468
+ if (versions.size === 1) continue;
469
+ const versionList = Array.from(versions).join(", ");
470
+ const allVersions = Array.from(versions);
471
+ for (const instance of instances) {
472
+ violations.push({
473
+ _tag: "ViolationVersionMismatch",
474
+ package: dep.name,
475
+ workspace: instance.workspace,
476
+ message: `Multiple versions found: ${versionList}`,
477
+ versionRange: instance.versionRange,
478
+ dependencyType: instance.type,
479
+ allVersions
480
+ });
481
+ }
482
+ }
483
+ return violations;
484
+ });
485
+ }
486
+ function detectFormatPackageJson(analysis) {
487
+ return Effect.gen(function* () {
488
+ const violations = [];
489
+ for (const workspace of analysis.workspaces) {
490
+ const content = yield* getPackageJsonStr(workspace).pipe(
491
+ Effect.catchAll(() => Effect.succeed(null))
492
+ );
493
+ if (content === null) continue;
494
+ const sorted = sortPackageJson(content);
495
+ if (content !== sorted) {
496
+ violations.push({
497
+ _tag: "ViolationFormatPackageJson",
498
+ package: workspace.name,
499
+ workspace: workspace.name,
500
+ message: "package.json is not sorted"
501
+ });
248
502
  }
249
503
  }
250
- } catch {
251
- return { success: false };
504
+ return violations;
505
+ });
506
+ }
507
+ class Monoverse extends Effect.Service()("Monoverse", {
508
+ succeed: {
509
+ analyze: (startPath) => analyzeMonorepo(startPath),
510
+ validate: (analysis) => Effect.gen(function* () {
511
+ const mismatches = yield* detectVersionMismatches(analysis);
512
+ const unpinned = yield* detectUnpinnedVersions(analysis);
513
+ const formatting = yield* detectFormatPackageJson(analysis);
514
+ return [...mismatches, ...unpinned, ...formatting];
515
+ }),
516
+ addPackage: (options) => upsertDependency({
517
+ workspace: options.workspace,
518
+ dependencyName: options.packageName,
519
+ versionRange: options.versionRange,
520
+ dependencyType: options.dependencyType
521
+ }),
522
+ removePackage: (options) => removeDependency({
523
+ workspace: options.workspace,
524
+ dependencyName: options.packageName
525
+ }),
526
+ formatWorkspace: (workspace) => formatPackageJson(workspace),
527
+ formatAllWorkspaces: (analysis) => Effect.forEach(analysis.workspaces, formatPackageJson, {
528
+ discard: true
529
+ })
252
530
  }
253
- return { success: true };
254
- };
255
-
256
- // domain/implementation/git/clone.ts
257
- import AdmZip from "adm-zip";
258
- import fs3, { createWriteStream } from "fs";
259
- import { mkdtemp, rm } from "fs/promises";
260
- import os from "os";
261
- import path3 from "path";
262
- import { Readable } from "stream";
263
- import { pipeline } from "stream/promises";
264
- var downloadGitRepo = async (url) => {
265
- const tempDir = await mkdtemp(path3.join(os.tmpdir(), "monoverse-"));
266
- const outputPath = path3.join(tempDir, "github-zip.zip");
267
- const outputDir = path3.join(tempDir, "github-extract");
268
- await rm(outputDir, { recursive: true, force: true });
269
- await rm(outputPath, { force: true });
270
- const { ok } = await downloadZip(url, outputPath);
271
- if (!ok) {
272
- throw new Error("Failed to download the zip file");
531
+ }) {
532
+ }
533
+ const tui = Command.make("tui", {}, () => Effect.void);
534
+ const cwd = process.cwd();
535
+ const findCurrentWorkspace = Effect.gen(function* () {
536
+ const monoverse2 = yield* Monoverse;
537
+ const analysis = yield* monoverse2.analyze(cwd);
538
+ const workspace = analysis.workspaces.find((ws) => cwd.startsWith(ws.path));
539
+ if (!workspace) {
540
+ return yield* Effect.fail(
541
+ new Error("Not inside a workspace. Run from within a workspace directory.")
542
+ );
273
543
  }
274
- const zip = new AdmZip(outputPath);
275
- zip.extractAllTo(outputDir, true);
276
- return {
277
- projectDir: await getInnerDirPath(outputDir),
278
- cleanupDir: () => {
279
- return rm(tempDir, { recursive: true, force: true });
280
- }
544
+ return { analysis, workspace };
545
+ });
546
+ const toDependencyType = (type) => {
547
+ const map = {
548
+ dependency: "dependency",
549
+ dev: "devDependency",
550
+ peer: "peerDependency",
551
+ optional: "optionalDependency"
281
552
  };
553
+ return map[type];
282
554
  };
283
- async function downloadZip(url, outputPath) {
284
- const response = await fetch(url);
285
- if (!response.ok) {
286
- return { ok: false, error: "Failed to fetch the zip file" };
287
- }
288
- if (!response.body) {
289
- return { ok: false, error: "Response body is empty" };
290
- }
291
- const nodeReadableStream = Readable.fromWeb(response.body);
292
- await pipeline(nodeReadableStream, createWriteStream(outputPath));
293
- return { ok: true };
294
- }
295
- var getInnerDirPath = (dirPath) => {
296
- return new Promise((resolve, reject) => {
297
- const files = fs3.readdirSync(dirPath);
298
- const innerDir = files.find((f) => {
299
- console.log("f", path3.join(dirPath, f));
300
- return fs3.statSync(path3.join(dirPath, f)).isDirectory();
301
- });
302
- if (!innerDir) {
303
- return reject("No inner directory found");
304
- }
305
- resolve(path3.join(dirPath, innerDir));
555
+ const packageArg$1 = Args.text({ name: "package" });
556
+ const typeOption = Options.choice("type", [
557
+ "dependency",
558
+ "dev",
559
+ "peer",
560
+ "optional"
561
+ ]).pipe(Options.withAlias("t"), Options.withDefault("dependency"));
562
+ const versionOption = Options.text("version").pipe(
563
+ Options.withAlias("v"),
564
+ Options.withDefault("latest")
565
+ );
566
+ const handler$1 = ({
567
+ package: pkg,
568
+ type,
569
+ version
570
+ }) => Effect.gen(function* () {
571
+ const monoverse2 = yield* Monoverse;
572
+ const { workspace } = yield* findCurrentWorkspace;
573
+ const dependencyType = toDependencyType(type);
574
+ yield* monoverse2.addPackage({
575
+ packageName: pkg,
576
+ versionRange: version,
577
+ dependencyType,
578
+ workspace
306
579
  });
580
+ yield* Console.log(`Added ${pkg}@${version} to ${workspace.name}`);
581
+ });
582
+ const add = Command.make(
583
+ "add",
584
+ { package: packageArg$1, type: typeOption, version: versionOption },
585
+ handler$1
586
+ );
587
+ const packageArg = Args.text({ name: "package" });
588
+ const handler = ({ package: pkg }) => Effect.gen(function* () {
589
+ const monoverse2 = yield* Monoverse;
590
+ const { workspace } = yield* findCurrentWorkspace;
591
+ yield* monoverse2.removePackage({
592
+ packageName: pkg,
593
+ workspace
594
+ });
595
+ yield* Console.log(`Removed ${pkg} from ${workspace.name}`);
596
+ });
597
+ const remove = Command.make("remove", { package: packageArg }, handler);
598
+ const rm = Command.make("rm", { package: packageArg }, handler);
599
+ const deleteCmd = Command.make("delete", { package: packageArg }, handler);
600
+ const format = Command.make(
601
+ "format",
602
+ {},
603
+ () => Effect.gen(function* () {
604
+ const monoverse2 = yield* Monoverse;
605
+ const analysis = yield* monoverse2.analyze(cwd);
606
+ yield* monoverse2.formatAllWorkspaces(analysis);
607
+ yield* Console.log(`Formatted ${analysis.workspaces.length} workspaces`);
608
+ })
609
+ );
610
+ const c = {
611
+ reset: "\x1B[0m",
612
+ red: "\x1B[38;2;238;136;136m",
613
+ green: "\x1B[38;2;136;238;136m",
614
+ gray: "\x1B[38;2;136;136;136m",
615
+ dim: "\x1B[38;2;102;102;102m",
616
+ white: "\x1B[38;2;255;255;255m"
307
617
  };
308
-
309
- // trpc/functionality/overview.ts
310
- var getOverview = (dirPath) => {
311
- const monorepoInfo = getMonorepoInfo(dirPath);
312
- if (!monorepoInfo) {
313
- return null;
314
- }
315
- const workspacesMap = monorepoInfo.workspaces.reduce(
316
- (acc, { workspace }) => {
317
- acc[workspace.name] = workspace;
318
- return acc;
319
- },
320
- {}
618
+ const formatViolations = (violations, workspaces, root) => {
619
+ const pathByName = new Map(
620
+ workspaces.map((w) => [
621
+ w.name,
622
+ w.path === root ? "." : w.path.replace(root + "/", "")
623
+ ])
321
624
  );
322
- const dependencyLinks = monorepoInfo.workspaces.map(({ workspace }) => {
323
- const internalDependencies = workspace.dependencies.filter((dep) => !!workspacesMap[dep.name]).map((v) => v.name);
324
- return {
325
- workspaceName: workspace.name,
326
- internalDependencies
327
- };
328
- });
329
- const getDependencyType = (workspace) => {
330
- const hasDecendants = dependencyLinks.find((v) => v.workspaceName === workspace).internalDependencies.length > 0;
331
- const hasAncestors = dependencyLinks.filter((v) => {
332
- if (v.workspaceName === workspace)
333
- return false;
334
- if (v.internalDependencies.includes(workspace))
335
- return true;
336
- return false;
337
- }).length > 0;
338
- if (hasDecendants && hasAncestors) {
339
- return "internal";
625
+ const grouped = /* @__PURE__ */ new Map();
626
+ for (const v of violations) {
627
+ if (!grouped.has(v.workspace)) {
628
+ grouped.set(v.workspace, /* @__PURE__ */ new Map());
340
629
  }
341
- if (hasAncestors) {
342
- return "leaf";
630
+ const pkgMap = grouped.get(v.workspace);
631
+ if (!pkgMap.has(v.package)) {
632
+ pkgMap.set(v.package, []);
343
633
  }
344
- return "standalone";
345
- };
346
- return dependencyLinks.map((link) => {
347
- return {
348
- ...link,
349
- dependenciesCount: workspacesMap[link.workspaceName].dependencies.length,
350
- type: getDependencyType(link.workspaceName)
351
- };
352
- });
353
- };
354
-
355
- // trpc/functionality/sync-updates.ts
356
- var getSyncUpdates = (dirPath) => {
357
- const monorepoInfo = getMonorepoInfo(dirPath);
358
- if (!monorepoInfo) {
359
- return null;
634
+ pkgMap.get(v.package).push(v);
360
635
  }
361
- const workspaceMap = monorepoInfo.workspaces.reduce(
362
- (acc, { workspace }) => {
363
- acc[workspace.name] = true;
364
- return acc;
365
- },
366
- {}
367
- );
368
- return monorepoInfo.workspaces.flatMap(({ workspace }) => {
369
- return workspace.dependencies.map((dependency) => {
370
- return {
371
- dependencyName: dependency.name,
372
- dependencyType: dependency.type,
373
- workspaceName: workspace.name,
374
- versionRange: dependency.versionRange,
375
- isInternalDependency: !!workspaceMap[dependency.name]
376
- };
377
- });
378
- });
379
- };
380
-
381
- // trpc/functionality/sync-versions.ts
382
- var syncVersions = (dirPath, updates) => {
383
- const monorepoInfo = getMonorepoInfo(dirPath);
384
- if (!monorepoInfo) {
385
- return null;
636
+ const formatDetail = (v) => {
637
+ const tag = v._tag.replace("Violation", "");
638
+ if (v._tag === "ViolationVersionMismatch" && v.allVersions) {
639
+ return `${tag} (${v.allVersions.join(", ")})`;
640
+ }
641
+ if (v._tag === "ViolationUnpinnedVersion" && v.versionRange) {
642
+ return `${tag} (${v.versionRange})`;
643
+ }
644
+ return tag;
645
+ };
646
+ const lines = [];
647
+ for (const [workspace, packages] of grouped) {
648
+ const path = pathByName.get(workspace) ?? "";
649
+ lines.push(`${c.white}${workspace}${c.dim} (${path})${c.reset}`);
650
+ for (const [pkg, vList] of packages) {
651
+ const details = vList.map(formatDetail).join(", ");
652
+ lines.push(`${c.gray} ${pkg.padEnd(28)}${c.red}${details}${c.reset}`);
653
+ }
386
654
  }
387
- return syncDependencyVersions(monorepoInfo, updates);
655
+ return lines.join("\n");
388
656
  };
389
-
390
- // trpc/setup.ts
391
- import { initTRPC } from "@trpc/server";
392
- var t = initTRPC.create();
393
- var router = t.router;
394
- var publicProcedure = t.procedure;
395
-
396
- // trpc/server.ts
397
- var appRouter = router({
398
- helloWorld: publicProcedure.query(async () => {
399
- return "Hello, World!";
400
- }),
401
- getOverview: publicProcedure.input(
402
- z5.object({
403
- type: z5.union([z5.literal("filepath"), z5.literal("url")]),
404
- value: z5.string()
405
- })
406
- ).query(async ({ input }) => {
407
- const { type, value } = input;
408
- if (type === "filepath") {
409
- const dirPath = value;
410
- const result = getOverview(dirPath);
411
- return {
412
- success: true,
413
- result
414
- };
415
- } else {
416
- const { projectDir, cleanupDir } = await downloadGitRepo(value);
417
- const result = getOverview(projectDir);
418
- await cleanupDir();
419
- return {
420
- success: true,
421
- result
422
- };
423
- }
424
- }),
425
- getSyncUpdates: publicProcedure.input(
426
- z5.object({
427
- type: z5.union([z5.literal("filepath"), z5.literal("url")]),
428
- value: z5.string()
429
- })
430
- ).query(async ({ input }) => {
431
- const { type, value } = input;
432
- if (type === "filepath") {
433
- const dirPath = value;
434
- const result = getSyncUpdates(dirPath);
435
- return {
436
- success: true,
437
- result
438
- };
439
- } else {
440
- const { projectDir, cleanupDir } = await downloadGitRepo(value);
441
- const result = getSyncUpdates(projectDir);
442
- await cleanupDir();
443
- return {
444
- success: true,
445
- result
446
- };
657
+ const lint = Command.make(
658
+ "lint",
659
+ {},
660
+ () => Effect.gen(function* () {
661
+ const monoverse2 = yield* Monoverse;
662
+ const analysis = yield* monoverse2.analyze(cwd);
663
+ const violations = yield* monoverse2.validate(analysis);
664
+ if (violations.length === 0) {
665
+ yield* Console.log(`${c.green}No issues found${c.reset}`);
666
+ return;
447
667
  }
448
- }),
449
- syncDependencies: publicProcedure.input(
450
- z5.object({
451
- dirPath: z5.string(),
452
- updates: z5.array(
453
- z5.object({
454
- dependencyName: z5.string(),
455
- versionRange: z5.string()
456
- })
457
- )
458
- })
459
- ).mutation(async ({ input }) => {
460
- const result = syncVersions(input.dirPath, input.updates);
461
- return result;
668
+ yield* Console.error(
669
+ `${c.red}Found ${violations.length} issues${c.reset}
670
+ `
671
+ );
672
+ yield* Console.error(
673
+ formatViolations(violations, analysis.workspaces, analysis.root)
674
+ );
675
+ yield* Effect.sync(() => process.exit(1));
462
676
  })
677
+ );
678
+ const monoverse = Command.make(
679
+ "monoverse",
680
+ {},
681
+ () => Console.log("Use --help to see available commands")
682
+ );
683
+ const command = monoverse.pipe(
684
+ Command.withSubcommands([tui, add, remove, rm, deleteCmd, format, lint])
685
+ );
686
+ const cli = Command.run(command, {
687
+ name: "monoverse",
688
+ version: "v0.0.12"
463
689
  });
464
-
465
- // cli.ts
466
- var __DIRNAME = fileURLToPath(new URL(".", import.meta.url));
467
- var app = express();
468
- app.use(
469
- "/api",
470
- trpcExpress.createExpressMiddleware({
471
- router: appRouter
690
+ const MainLayer = Layer.mergeAll(
691
+ NodeContext.layer,
692
+ Monoverse.Default,
693
+ CliConfig.layer({
694
+ isCaseSensitive: true,
695
+ showBuiltIns: false,
696
+ showTypes: false
472
697
  })
473
698
  );
474
- app.use(express.static("dist"));
475
- app.get("*", (_, res) => {
476
- res.sendFile(path4.join(__DIRNAME, "dist", "index.html"));
477
- });
478
- var port = 21212;
479
- app.listen(port, () => {
480
- console.log(`Server listening... http://localhost:${port}/`);
481
- });
699
+ cli(process.argv).pipe(Effect.provide(MainLayer), NodeRuntime.runMain);