submodule-version 2.3.2 → 3.0.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 (120) hide show
  1. package/README.md +103 -20
  2. package/dist/runner/handleErrors/handleErrors.d.ts +1 -1
  3. package/dist/runner/handleErrors/handleErrors.js +3 -10
  4. package/dist/runner/handleErrors/handleErrors.js.map +1 -1
  5. package/dist/runner/index.js +0 -0
  6. package/dist/runner/runner.d.ts +4 -0
  7. package/dist/runner/runner.js +91 -37
  8. package/dist/runner/runner.js.map +1 -1
  9. package/dist/src/PubGrub/PubGrub.d.ts +28 -0
  10. package/dist/src/PubGrub/PubGrub.js +366 -0
  11. package/dist/src/PubGrub/PubGrub.js.map +1 -0
  12. package/dist/src/PubGrub/index.d.ts +2 -0
  13. package/dist/src/PubGrub/index.js +7 -0
  14. package/dist/src/PubGrub/index.js.map +1 -0
  15. package/dist/src/PubGrub/types.d.ts +56 -0
  16. package/dist/src/PubGrub/types.js +3 -0
  17. package/dist/src/PubGrub/types.js.map +1 -0
  18. package/dist/src/entryStore.d.ts +16 -0
  19. package/dist/src/entryStore.js +152 -0
  20. package/dist/src/entryStore.js.map +1 -0
  21. package/dist/src/errors.d.ts +28 -0
  22. package/dist/src/errors.js +30 -0
  23. package/dist/src/errors.js.map +1 -0
  24. package/dist/src/git/api/GithubError.d.ts +4 -0
  25. package/dist/src/git/api/GithubError.js +11 -0
  26. package/dist/src/git/api/GithubError.js.map +1 -0
  27. package/dist/src/git/api/api.d.ts +8 -0
  28. package/dist/src/git/api/api.js +133 -0
  29. package/dist/src/git/api/api.js.map +1 -0
  30. package/dist/src/git/api/index.d.ts +3 -0
  31. package/dist/src/git/api/index.js +8 -0
  32. package/dist/src/git/api/index.js.map +1 -0
  33. package/dist/src/git/git.d.ts +30 -23
  34. package/dist/src/git/git.js +8 -258
  35. package/dist/src/git/git.js.map +1 -1
  36. package/dist/src/git/index.d.ts +1 -0
  37. package/dist/src/git/index.js +3 -1
  38. package/dist/src/git/index.js.map +1 -1
  39. package/dist/src/git/local/index.d.ts +1 -0
  40. package/dist/src/git/local/index.js +6 -0
  41. package/dist/src/git/local/index.js.map +1 -0
  42. package/dist/src/git/local/local.d.ts +26 -0
  43. package/dist/src/git/local/local.js +288 -0
  44. package/dist/src/git/local/local.js.map +1 -0
  45. package/dist/src/index.d.ts +7 -0
  46. package/dist/src/index.js +16 -1
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/printError.d.ts +1 -0
  49. package/dist/src/printError.js +158 -0
  50. package/dist/src/printError.js.map +1 -0
  51. package/dist/src/sv.d.ts +18 -8
  52. package/dist/src/sv.js +235 -131
  53. package/dist/src/sv.js.map +1 -1
  54. package/dist/src/versionUtil/index.d.ts +2 -0
  55. package/dist/src/versionUtil/index.js +6 -0
  56. package/dist/src/versionUtil/index.js.map +1 -0
  57. package/dist/src/versionUtil/versionUtil.d.ts +16 -0
  58. package/dist/src/versionUtil/versionUtil.js +70 -0
  59. package/dist/src/versionUtil/versionUtil.js.map +1 -0
  60. package/package.json +4 -2
  61. package/runner/handleErrors/handleErrors.ts +3 -10
  62. package/runner/runner.ts +128 -46
  63. package/src/PubGrub/PubGrub.ts +578 -0
  64. package/src/PubGrub/__tests__/PubGrub.test.ts +351 -0
  65. package/src/PubGrub/index.ts +12 -0
  66. package/src/PubGrub/types.ts +72 -0
  67. package/src/__tests__/entryStore.test.ts +248 -0
  68. package/src/__tests__/printError.test.ts +148 -0
  69. package/src/__tests__/sv.test.ts +441 -0
  70. package/src/entryStore.ts +212 -0
  71. package/src/errors.ts +39 -0
  72. package/src/git/api/GithubError.ts +5 -0
  73. package/src/git/api/api.ts +180 -0
  74. package/src/git/api/index.ts +3 -0
  75. package/src/git/git.ts +8 -396
  76. package/src/git/index.ts +1 -0
  77. package/src/git/local/index.ts +1 -0
  78. package/src/git/local/local.ts +430 -0
  79. package/src/index.ts +17 -0
  80. package/src/printError.ts +189 -0
  81. package/src/sv.ts +321 -134
  82. package/src/versionUtil/__tests__/versionUtil.test.ts +62 -0
  83. package/src/versionUtil/index.ts +2 -0
  84. package/src/versionUtil/versionUtil.ts +103 -0
  85. package/tsconfig.json +1 -1
  86. package/dist/runner/handleErrors/handleBuildGraphError.d.ts +0 -1
  87. package/dist/runner/handleErrors/handleBuildGraphError.js +0 -42
  88. package/dist/runner/handleErrors/handleBuildGraphError.js.map +0 -1
  89. package/dist/runner/handleErrors/handleGitError.d.ts +0 -1
  90. package/dist/runner/handleErrors/handleGitError.js +0 -27
  91. package/dist/runner/handleErrors/handleGitError.js.map +0 -1
  92. package/dist/runner/handleErrors/handleInstallError.d.ts +0 -1
  93. package/dist/runner/handleErrors/handleInstallError.js +0 -30
  94. package/dist/runner/handleErrors/handleInstallError.js.map +0 -1
  95. package/dist/src/__tests__/versionUtil.test.d.ts +0 -1
  96. package/dist/src/__tests__/versionUtil.test.js +0 -80
  97. package/dist/src/__tests__/versionUtil.test.js.map +0 -1
  98. package/dist/src/buildGraph/GraphError.d.ts +0 -11
  99. package/dist/src/buildGraph/GraphError.js +0 -11
  100. package/dist/src/buildGraph/GraphError.js.map +0 -1
  101. package/dist/src/buildGraph/buildGraph.d.ts +0 -8
  102. package/dist/src/buildGraph/buildGraph.js +0 -100
  103. package/dist/src/buildGraph/buildGraph.js.map +0 -1
  104. package/dist/src/buildGraph/index.d.ts +0 -2
  105. package/dist/src/buildGraph/index.js +0 -8
  106. package/dist/src/buildGraph/index.js.map +0 -1
  107. package/dist/src/git/__tests__/parseGitUrl.test.d.ts +0 -1
  108. package/dist/src/git/__tests__/parseGitUrl.test.js +0 -30
  109. package/dist/src/git/__tests__/parseGitUrl.test.js.map +0 -1
  110. package/dist/src/versionUtil.d.ts +0 -13
  111. package/dist/src/versionUtil.js +0 -79
  112. package/dist/src/versionUtil.js.map +0 -1
  113. package/runner/handleErrors/handleBuildGraphError.ts +0 -49
  114. package/runner/handleErrors/handleGitError.ts +0 -28
  115. package/runner/handleErrors/handleInstallError.ts +0 -26
  116. package/src/__tests__/versionUtil.test.ts +0 -96
  117. package/src/buildGraph/GraphError.ts +0 -12
  118. package/src/buildGraph/buildGraph.ts +0 -150
  119. package/src/buildGraph/index.ts +0 -2
  120. package/src/versionUtil.ts +0 -97
@@ -0,0 +1,430 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import { parseGitUrl } from '../parseGitUrl';
5
+ import { versionUtil } from '../../versionUtil';
6
+ import { RunOptions } from '../../runOptions';
7
+ import { GitError } from '../GitError';
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ /* stderr упавшей git-команды — единственный источник настоящей причины */
12
+ const stderrOf = (error: unknown): string => (
13
+ (error as { stderr?: string })?.stderr || ''
14
+ ).trim();
15
+
16
+ export const local = {
17
+ addSubmodule: async (url: string): Promise<void> => {
18
+ const { name } = parseGitUrl(url);
19
+ const module = path.join(RunOptions.modulesDir, name);
20
+
21
+ try {
22
+ await execAsync(
23
+ `git submodule add "${url}" "${module}"`,
24
+ { cwd: RunOptions.cwd },
25
+ );
26
+ } catch (error) {
27
+ throw new GitError('GIT_SUBMODULE_ADD_FAILED', {
28
+ url,
29
+ reason: stderrOf(error),
30
+ });
31
+ }
32
+ },
33
+
34
+ checkout: async (name: string, version: string): Promise<void> => {
35
+ const module = path.join(RunOptions.modulesDir, name);
36
+
37
+ try {
38
+ await execAsync(
39
+ `git -C "${module}" checkout ${version} -q`,
40
+ { cwd: RunOptions.cwd },
41
+ );
42
+ } catch (error) {
43
+ throw new GitError('GIT_MODULE_CHECKOUT_FAILED', {
44
+ name,
45
+ version,
46
+ reason: stderrOf(error),
47
+ });
48
+ }
49
+ },
50
+
51
+ listVersions: async (projectName: string): Promise<string[]> => {
52
+ const module = path.join(RunOptions.modulesDir, projectName);
53
+
54
+ return local.listTags(module);
55
+ },
56
+
57
+ status: async (dir: string): Promise<string> => {
58
+ const res = await execAsync(
59
+ `git -C "${dir}" status --porcelain`,
60
+ { cwd: RunOptions.cwd },
61
+ );
62
+
63
+ return res.stdout.toString();
64
+ },
65
+
66
+ hasChanges: async (dir: string): Promise<boolean> => {
67
+ const status = await local.status(dir);
68
+
69
+ return status.trim().length > 0;
70
+ },
71
+
72
+ hasUnpushedCommits: async (dir: string): Promise<boolean> => {
73
+ await local.fetch(dir);
74
+
75
+ const branch = await local.currentBranch(dir);
76
+
77
+ if (branch) {
78
+ let upstream: string | null = null;
79
+
80
+ try {
81
+ const res = await execAsync(
82
+ `git -C "${dir}" rev-parse --abbrev-ref --symbolic-full-name @{u}`,
83
+ { cwd: RunOptions.cwd },
84
+ );
85
+
86
+ upstream = res.stdout.toString().trim() || null;
87
+ } catch {
88
+ upstream = null;
89
+ }
90
+
91
+ if (!upstream) {
92
+ try {
93
+ await execAsync(
94
+ `git -C "${dir}" rev-parse --verify --quiet origin/${branch}`,
95
+ { cwd: RunOptions.cwd },
96
+ );
97
+ upstream = `origin/${branch}`;
98
+ } catch {
99
+ upstream = null;
100
+ }
101
+ }
102
+
103
+ /* Ветки на remote нет — всё локальное не опубликовано */
104
+ if (!upstream) return true;
105
+
106
+ const res = await execAsync(
107
+ `git -C "${dir}" rev-list ${upstream}..HEAD --count`,
108
+ { cwd: RunOptions.cwd },
109
+ );
110
+
111
+ return Number(res.stdout.toString().trim()) > 0;
112
+ }
113
+
114
+ /*
115
+ * Detached HEAD (checkout на тег): локальные теги не доказывают публикацию
116
+ * (могут быть не запушены) — сверяем HEAD с фактическими рефами remote.
117
+ */
118
+ let remoteOutput = '';
119
+
120
+ try {
121
+ const res = await execAsync(
122
+ `git -C "${dir}" ls-remote origin`,
123
+ { cwd: RunOptions.cwd },
124
+ );
125
+
126
+ remoteOutput = res.stdout.toString();
127
+ } catch {
128
+ /* Remote недоступен/не настроен — считаем всё неопубликованным */
129
+ return true;
130
+ }
131
+
132
+ const remoteShas = remoteOutput.split(/\r?\n/)
133
+ .map(line => line.split(/\s+/)[0])
134
+ .filter(sha => /^[0-9a-f]{40}$/.test(sha));
135
+
136
+ for (const sha of remoteShas) {
137
+ try {
138
+ await execAsync(
139
+ `git -C "${dir}" merge-base --is-ancestor HEAD ${sha}`,
140
+ { cwd: RunOptions.cwd },
141
+ );
142
+
143
+ /* HEAD достижим из remote-рефа — опубликован */
144
+ return false;
145
+ } catch {
146
+ // HEAD не предок этого рефа — проверяем остальные
147
+ }
148
+ }
149
+
150
+ return true;
151
+ },
152
+
153
+ isGitRepo: async (dir: string): Promise<boolean> => {
154
+ try {
155
+ await execAsync(
156
+ `git -C "${dir}" rev-parse --is-inside-work-tree`,
157
+ { cwd: RunOptions.cwd },
158
+ );
159
+
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ },
165
+
166
+ getRemote: async (dir: string): Promise<string | null> => {
167
+ try {
168
+ const res = await execAsync(
169
+ `git -C "${dir}" remote get-url origin`,
170
+ { cwd: RunOptions.cwd },
171
+ );
172
+
173
+ return res.stdout.toString().trim() || null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ },
178
+
179
+ init: async (dir: string): Promise<void> => {
180
+ await execAsync(
181
+ `git -C "${dir}" init`,
182
+ { cwd: RunOptions.cwd },
183
+ );
184
+ },
185
+
186
+ addRemote: async (dir: string, url: string): Promise<void> => {
187
+ const existing = await local.getRemote(dir);
188
+
189
+ const command = existing
190
+ ? `git -C "${dir}" remote set-url origin "${url}"`
191
+ : `git -C "${dir}" remote add origin "${url}"`;
192
+
193
+ await execAsync(command, { cwd: RunOptions.cwd });
194
+ },
195
+
196
+ currentBranch: async (dir: string): Promise<string | null> => {
197
+ try {
198
+ const res = await execAsync(
199
+ `git -C "${dir}" symbolic-ref --short -q HEAD`,
200
+ { cwd: RunOptions.cwd },
201
+ );
202
+
203
+ return res.stdout.toString().trim() || null;
204
+ } catch {
205
+ return null;
206
+ }
207
+ },
208
+
209
+ defaultRemoteBranch: async (dir: string): Promise<string | null> => {
210
+ try {
211
+ const res = await execAsync(
212
+ `git -C "${dir}" symbolic-ref --short refs/remotes/origin/HEAD`,
213
+ { cwd: RunOptions.cwd },
214
+ );
215
+
216
+ return res.stdout.toString().trim().replace(/^origin\//, '') || null;
217
+ } catch {
218
+ // origin/HEAD not set - probe common defaults
219
+ }
220
+
221
+ for (const candidate of ['main', 'master']) {
222
+ try {
223
+ await execAsync(
224
+ `git -C "${dir}" rev-parse --verify --quiet origin/${candidate}`,
225
+ { cwd: RunOptions.cwd },
226
+ );
227
+
228
+ return candidate;
229
+ } catch {
230
+ // No such remote branch
231
+ }
232
+ }
233
+
234
+ return null;
235
+ },
236
+
237
+ /*
238
+ * Аддоны checkout'нуты на тег (detached HEAD). Перед коммитом поднимаем
239
+ * локальную ветку на текущем коммите и привязываем её к remote-ветке,
240
+ * чтобы sync/push работали по ветке, а не в оторванной голове.
241
+ */
242
+ ensureBranch: async (dir: string): Promise<void> => {
243
+ const branch = await local.currentBranch(dir);
244
+ if (branch) return;
245
+
246
+ await local.fetch(dir);
247
+ const remoteBranch = await local.defaultRemoteBranch(dir);
248
+ const target = remoteBranch || 'main';
249
+
250
+ await execAsync(
251
+ `git -C "${dir}" checkout -B ${target}`,
252
+ { cwd: RunOptions.cwd },
253
+ );
254
+
255
+ if (remoteBranch) {
256
+ await execAsync(
257
+ // eslint-disable-next-line max-len
258
+ `git -C "${dir}" branch --set-upstream-to=origin/${remoteBranch} ${target}`,
259
+ { cwd: RunOptions.cwd },
260
+ );
261
+ }
262
+ },
263
+
264
+ commitAll: async (dir: string, message: string): Promise<void> => {
265
+ await execAsync(
266
+ `git -C "${dir}" add -A`,
267
+ { cwd: RunOptions.cwd },
268
+ );
269
+
270
+ await execAsync(
271
+ `git -C "${dir}" commit -m ${JSON.stringify(message)}`,
272
+ { cwd: RunOptions.cwd },
273
+ );
274
+ },
275
+
276
+ addTag: async (dir: string, version: string): Promise<void> => {
277
+ await execAsync(
278
+ `git -C "${dir}" tag ${version}`,
279
+ { cwd: RunOptions.cwd },
280
+ );
281
+ },
282
+
283
+ push: async (dir: string, tag?: string): Promise<void> => {
284
+ await execAsync(
285
+ `git -C "${dir}" push -u origin HEAD`,
286
+ { cwd: RunOptions.cwd },
287
+ );
288
+
289
+ if (tag) {
290
+ await execAsync(
291
+ `git -C "${dir}" push origin ${tag}`,
292
+ { cwd: RunOptions.cwd },
293
+ );
294
+ }
295
+ },
296
+
297
+ fetch: async (dir: string): Promise<void> => {
298
+ try {
299
+ await execAsync(
300
+ `git -C "${dir}" fetch origin`,
301
+ { cwd: RunOptions.cwd },
302
+ );
303
+ } catch {
304
+ // Fresh repo without upstream yet - nothing to fetch
305
+ }
306
+ },
307
+
308
+ isRemoteAhead: async (dir: string): Promise<boolean> => {
309
+ await local.fetch(dir);
310
+
311
+ try {
312
+ const res = await execAsync(
313
+ `git -C "${dir}" rev-list HEAD..@{u} --count`,
314
+ { cwd: RunOptions.cwd },
315
+ );
316
+
317
+ return Number(res.stdout.toString().trim()) > 0;
318
+ } catch {
319
+ // No upstream configured - remote is not ahead
320
+ return false;
321
+ }
322
+ },
323
+
324
+ pullRebaseAutostash: async (dir: string): Promise<void> => {
325
+ try {
326
+ await execAsync(
327
+ `git -C "${dir}" pull --rebase --autostash`,
328
+ { cwd: RunOptions.cwd },
329
+ );
330
+ } catch {
331
+ try {
332
+ await execAsync(
333
+ `git -C "${dir}" rebase --abort`,
334
+ { cwd: RunOptions.cwd },
335
+ );
336
+ } catch {
337
+ // Best effort - no rebase in progress
338
+ }
339
+
340
+ try {
341
+ await execAsync(
342
+ `git -C "${dir}" stash pop`,
343
+ { cwd: RunOptions.cwd },
344
+ );
345
+ } catch {
346
+ // Best effort - nothing stashed
347
+ }
348
+
349
+ throw new GitError('GIT_SYNC_CONFLICT');
350
+ }
351
+ },
352
+
353
+ tagsAtHead: async (dir: string): Promise<string[]> => {
354
+ try {
355
+ const res = await execAsync(
356
+ `git -C "${dir}" tag --points-at HEAD`,
357
+ { cwd: RunOptions.cwd },
358
+ );
359
+
360
+ return res.stdout.toString().split(/\r?\n/)
361
+ .filter(tag => tag && versionUtil.validate(tag));
362
+ } catch {
363
+ return [];
364
+ }
365
+ },
366
+
367
+ currentVersion: async (name: string): Promise<string | null> => {
368
+ const module = path.join(RunOptions.modulesDir, name);
369
+ const tags = await local.tagsAtHead(module);
370
+
371
+ return versionUtil.latest(tags) || null;
372
+ },
373
+
374
+ listTags: async (dir: string): Promise<string[]> => {
375
+ try {
376
+ await execAsync(
377
+ `git -C "${dir}" fetch --tags`,
378
+ { cwd: RunOptions.cwd },
379
+ );
380
+ } catch {
381
+ // Repo without remote - local tags only
382
+ }
383
+
384
+ const res = await execAsync(
385
+ `git -C "${dir}" tag -l`,
386
+ { cwd: RunOptions.cwd },
387
+ );
388
+
389
+ const tagList = res.stdout.toString().split(/\r?\n/);
390
+
391
+ return tagList.filter(tag => {
392
+ if (!tag) return false;
393
+
394
+ return versionUtil.validate(tag);
395
+ });
396
+ },
397
+
398
+ /*
399
+ * package.json сабмодуля на произвольном рефе (теге), не трогая рабочую
400
+ * копию. Нет файла или рефа — null (ленивое чтение depsByVersion).
401
+ */
402
+ readJSONAtRef: async (name: string, ref: string): Promise<any | null> => {
403
+ const module = path.join(RunOptions.modulesDir, name);
404
+
405
+ try {
406
+ const res = await execAsync(
407
+ `git -C "${module}" show ${ref}:package.json`,
408
+ { cwd: RunOptions.cwd },
409
+ );
410
+
411
+ return JSON.parse(res.stdout.toString());
412
+ } catch {
413
+ return null;
414
+ }
415
+ },
416
+
417
+ rm: async (submoduleName: string) => {
418
+ const modulePath = path.join(RunOptions.modulesDir, submoduleName);
419
+
420
+ await execAsync(
421
+ `git rm -rf "${modulePath}"`,
422
+ { cwd: RunOptions.cwd },
423
+ );
424
+
425
+ await execAsync(
426
+ `rm -rf "${path.join('.git/modules', modulePath)}"`,
427
+ { cwd: RunOptions.cwd },
428
+ );
429
+ },
430
+ };
package/src/index.ts CHANGED
@@ -1,2 +1,19 @@
1
1
  export { SV } from './sv';
2
2
  export { versionUtil } from './versionUtil';
3
+ export { PubGrub, pubGrub } from './PubGrub';
4
+ export type {
5
+ IEntrySource,
6
+ IIncompatibility,
7
+ IOverride,
8
+ IPackageEntry,
9
+ IRequirement,
10
+ IResolvedPackage,
11
+ ITerm,
12
+ TDependencies,
13
+ TPubGrubResult,
14
+ } from './PubGrub';
15
+ export { EntryStore, entryStore } from './entryStore';
16
+ export { printError } from './printError';
17
+ export { git, GitError, GithubError } from './git';
18
+ export { SVError, ERR } from './errors';
19
+ export type { TSVErrorCode, TSVErrorDetails } from './errors';
@@ -0,0 +1,189 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ import { ERR, SVError } from './errors';
4
+ import { GitError, GithubError } from './git';
5
+ import { RunOptions } from './runOptions';
6
+ import { versionUtil } from './versionUtil';
7
+
8
+ const ROOT = '<root>';
9
+
10
+ const asList = (value: unknown): string[] => (
11
+ Array.isArray(value) ? value.map(String) : []
12
+ );
13
+
14
+ /* Имя проекта из корневого package.json для отображения вместо <root> */
15
+ const projectRootName = (): string => {
16
+ try {
17
+ const pkg = JSON.parse(
18
+ readFileSync(path.join(RunOptions.cwd, 'package.json'), 'utf-8'),
19
+ );
20
+
21
+ return pkg.name || 'the project root';
22
+ } catch {
23
+ return 'the project root';
24
+ }
25
+ };
26
+
27
+ const printVersionConflict = (
28
+ details: Record<string, unknown>,
29
+ ): string => {
30
+ const parents = (details.parents || {}) as Record<string, string>;
31
+ const versions = asList(details.versions);
32
+
33
+ const constraints = (
34
+ Array.isArray(details.constraints) ? details.constraints : []
35
+ ) as Array<{
36
+ range: string,
37
+ positive: boolean,
38
+ requiredBy: string,
39
+ }>;
40
+
41
+ /*
42
+ * Ни одна доступная версия не подходит под одно из требований —
43
+ * это не конфликт между родителями, а запрос несуществующей версии.
44
+ */
45
+ const unsatisfiable = constraints.find(requirement => (
46
+ requirement.positive
47
+ && !versions.some(version => (
48
+ versionUtil.satisfies(version, requirement.range)
49
+ ))
50
+ ));
51
+
52
+ if (unsatisfiable) {
53
+ const from = unsatisfiable.requiredBy === ROOT
54
+ ? projectRootName()
55
+ : unsatisfiable.requiredBy;
56
+
57
+ return `Package ${String(details.name)} does not contain a version`
58
+ + ` matching ${unsatisfiable.range} required by ${from}`
59
+ + `. Use one of [${versions.join(', ')}]`;
60
+ }
61
+
62
+ const chain = asList(details.chain)
63
+ .map(name => (name === ROOT ? projectRootName() : name));
64
+
65
+ const lines = [
66
+ `Version conflict for ${String(details.name)}:`,
67
+ ...Object.entries(parents).map(([parent, range]) => {
68
+ const from = parent === ROOT ? projectRootName() : parent;
69
+
70
+ return ` - ${range} required by ${from}`;
71
+ }),
72
+ ];
73
+
74
+ if (chain.length) lines.push(` chain: ${chain.join(' -> ')}`);
75
+
76
+ if (versions.length) {
77
+ lines.push(` available versions: ${versions.join(', ')}`);
78
+ }
79
+
80
+ return lines.join('\n');
81
+ };
82
+
83
+ const printSVError = (error: SVError): string => {
84
+ const { error: code, details } = error;
85
+
86
+ switch (code) {
87
+ case ERR.NOT_A_NPM:
88
+ return 'Not an npm project (package.json not found)';
89
+ case ERR.NOT_A_GIT_REPO:
90
+ return 'Not a git repository';
91
+ case ERR.NOT_A_GIT_URL:
92
+ return `${String(details.url || '')} - is not a git url!`;
93
+ case ERR.NOT_INITIALIZED:
94
+ return 'SV is not initialized - call init() first';
95
+ case ERR.NOT_IMPLEMENTED:
96
+ return 'This command is not implemented yet';
97
+ case ERR.PATH_NOT_FOUND:
98
+ return `Path ${String(details.path)} is not found`;
99
+ case ERR.ADDON_NOT_FOUND: {
100
+ const urls = asList(details.urls);
101
+
102
+ const suffix = urls.length
103
+ ? ` (conflicting urls: ${urls.join(', ')})`
104
+ : '';
105
+
106
+ return `Module ${String(details.name)} is not found${suffix}`;
107
+ }
108
+ case ERR.REQUESTED_VERSION_NOT_EXISTS: {
109
+ const versions = asList(details.versions).join(', ');
110
+
111
+ return `Package ${String(details.name)} does not contain version`
112
+ + ` ${String(details.requestedVersion)}. Use one of [${versions}]`;
113
+ }
114
+ case ERR.VERSION_CONFLICT:
115
+ return printVersionConflict(details);
116
+ case ERR.CIRCULAR_DEPENDENCY:
117
+ return `Circular dependency: ${asList(details.chain).join(' -> ')}`;
118
+ case ERR.UNKNOWN_VERSION:
119
+ return `Unknown version ${String(details.version)}`
120
+ + ` for ${String(details.name)} in ${String(details.parent)}`;
121
+ case ERR.GIT_DIRTY_SWITCH_CONFLICT:
122
+ return 'Checkout failed: uncommitted changes cannot be carried over'
123
+ + ' - move them manually';
124
+ default:
125
+ return code;
126
+ }
127
+ };
128
+
129
+ const printGitError = (error: GitError): string => {
130
+ const reason = error.details.reason
131
+ ? `: ${String(error.details.reason)}`
132
+ : '';
133
+
134
+ switch (error.message) {
135
+ case 'GIT_NOT_A_REPO':
136
+ return 'Not a git repository';
137
+ case 'GIT_SUBMODULE_ADD_FAILED':
138
+ return `Failed to add submodule ${String(error.details.url)}${reason}`;
139
+ case 'GIT_MODULE_CHECKOUT_FAILED':
140
+ return `Checkout ${String(error.details.version || '')}`
141
+ + ` failed${reason}`;
142
+ case 'GIT_REPO_URL_REQUIRED':
143
+ return 'Module is not published yet'
144
+ + ' - pass --repo-url for the first publish';
145
+ case 'GIT_NOT_LATEST_VERSION':
146
+ return 'Publish is only possible from the latest version';
147
+ case 'GIT_SYNC_CONFLICT':
148
+ return 'Sync with remote failed - resolve conflicts manually';
149
+ default:
150
+ return error.message;
151
+ }
152
+ };
153
+
154
+ const printGithubError = (error: GithubError): string => {
155
+ const { message, details } = error;
156
+
157
+ switch (message) {
158
+ case 'GITHUB_NOT_A_REPO_URL':
159
+ return `Not a GitHub repo url: ${String(details.url)}`;
160
+ case 'GITHUB_TOKEN_REQUIRED':
161
+ return 'GitHub GraphQL API requires a token: set GITHUB_TOKEN'
162
+ + ' or pass { githubToken } to new SV()';
163
+ case 'GITHUB_API_FAILED': {
164
+ const hints: Record<number, string> = {
165
+ 404: 'repo not found or private (set GITHUB_TOKEN)',
166
+ 403: 'rate limit or forbidden (set GITHUB_TOKEN)',
167
+ };
168
+
169
+ const status = details.status as number;
170
+ const hint = hints[status];
171
+ const target = details.url ? ` for ${String(details.url)}` : '';
172
+ const errors = asList(details.errors).join('; ');
173
+
174
+ return `GitHub API failed (${status})${target}`
175
+ + `${errors ? `: ${errors}` : ''}${hint ? `: ${hint}` : ''}`;
176
+ }
177
+ default:
178
+ return message;
179
+ }
180
+ };
181
+
182
+ /* Человекочитаемый текст ошибок SV/PubGrub, git-слоя и GitHub API */
183
+ export const printError = (error: Error): string => {
184
+ if (error instanceof SVError) return printSVError(error);
185
+ if (error instanceof GitError) return printGitError(error);
186
+ if (error instanceof GithubError) return printGithubError(error);
187
+
188
+ return `Unhandled error: ${error.message}`;
189
+ };