@vltpkg/cli-sdk 1.0.0-rc.22 → 1.0.0-rc.24

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 (103) hide show
  1. package/dist/commands/bugs.d.ts +17 -0
  2. package/dist/commands/bugs.js +163 -0
  3. package/dist/commands/build.d.ts +24 -0
  4. package/dist/commands/build.js +101 -0
  5. package/dist/commands/cache.d.ts +64 -0
  6. package/dist/commands/cache.js +256 -0
  7. package/dist/commands/ci.d.ts +10 -0
  8. package/dist/commands/ci.js +40 -0
  9. package/dist/commands/config.d.ts +5 -0
  10. package/dist/commands/config.js +429 -0
  11. package/dist/commands/create.d.ts +8 -0
  12. package/dist/commands/create.js +102 -0
  13. package/dist/commands/docs.d.ts +17 -0
  14. package/dist/commands/docs.js +153 -0
  15. package/dist/commands/exec-cache.d.ts +48 -0
  16. package/dist/commands/exec-cache.js +145 -0
  17. package/dist/commands/exec-local.d.ts +5 -0
  18. package/dist/commands/exec-local.js +46 -0
  19. package/dist/commands/exec.d.ts +8 -0
  20. package/dist/commands/exec.js +161 -0
  21. package/dist/commands/help.d.ts +3 -0
  22. package/dist/commands/help.js +43 -0
  23. package/dist/commands/init.d.ts +7 -0
  24. package/dist/commands/init.js +116 -0
  25. package/dist/commands/install/reporter.d.ts +10 -0
  26. package/dist/commands/install/reporter.js +93 -0
  27. package/dist/commands/install.d.ts +27 -0
  28. package/dist/commands/install.js +80 -0
  29. package/dist/commands/list.d.ts +17 -0
  30. package/dist/commands/list.js +197 -0
  31. package/dist/commands/login.d.ts +3 -0
  32. package/dist/commands/login.js +22 -0
  33. package/dist/commands/logout.d.ts +3 -0
  34. package/dist/commands/logout.js +22 -0
  35. package/dist/commands/pack.d.ts +31 -0
  36. package/dist/commands/pack.js +205 -0
  37. package/dist/commands/ping.d.ts +17 -0
  38. package/dist/commands/ping.js +114 -0
  39. package/dist/commands/pkg.d.ts +6 -0
  40. package/dist/commands/pkg.js +232 -0
  41. package/dist/commands/publish.d.ts +21 -0
  42. package/dist/commands/publish.js +282 -0
  43. package/dist/commands/query.d.ts +18 -0
  44. package/dist/commands/query.js +216 -0
  45. package/dist/commands/repo.d.ts +17 -0
  46. package/dist/commands/repo.js +157 -0
  47. package/dist/commands/run-exec.d.ts +5 -0
  48. package/dist/commands/run-exec.js +40 -0
  49. package/dist/commands/run.d.ts +5 -0
  50. package/dist/commands/run.js +62 -0
  51. package/dist/commands/token.d.ts +3 -0
  52. package/dist/commands/token.js +39 -0
  53. package/dist/commands/uninstall.d.ts +15 -0
  54. package/dist/commands/uninstall.js +39 -0
  55. package/dist/commands/update.d.ts +13 -0
  56. package/dist/commands/update.js +46 -0
  57. package/dist/commands/version.d.ts +25 -0
  58. package/dist/commands/version.js +252 -0
  59. package/dist/commands/view.d.ts +22 -0
  60. package/dist/commands/view.js +334 -0
  61. package/dist/commands/whoami.d.ts +12 -0
  62. package/dist/commands/whoami.js +28 -0
  63. package/dist/config/definition.d.ts +407 -0
  64. package/dist/config/definition.js +684 -0
  65. package/dist/config/index.d.ts +218 -0
  66. package/dist/config/index.js +488 -0
  67. package/dist/config/merge.d.ts +3 -0
  68. package/dist/config/merge.js +27 -0
  69. package/dist/config/usage.d.ts +18 -0
  70. package/dist/config/usage.js +39 -0
  71. package/dist/custom-help.d.ts +8 -0
  72. package/dist/custom-help.js +419 -0
  73. package/dist/exec-command.d.ts +52 -0
  74. package/dist/exec-command.js +313 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.js +72 -0
  77. package/dist/load-command.d.ts +15 -0
  78. package/dist/load-command.js +20 -0
  79. package/dist/mermaid-image-view.d.ts +18 -0
  80. package/dist/mermaid-image-view.js +36 -0
  81. package/dist/output.d.ts +20 -0
  82. package/dist/output.js +125 -0
  83. package/dist/pack-tarball.d.ts +23 -0
  84. package/dist/pack-tarball.js +256 -0
  85. package/dist/parse-add-remove-args.d.ts +28 -0
  86. package/dist/parse-add-remove-args.js +103 -0
  87. package/dist/print-err.d.ts +13 -0
  88. package/dist/print-err.js +193 -0
  89. package/dist/query-diff-files.d.ts +17 -0
  90. package/dist/query-diff-files.js +63 -0
  91. package/dist/query-host-contexts.d.ts +15 -0
  92. package/dist/query-host-contexts.js +136 -0
  93. package/dist/read-password.d.ts +7 -0
  94. package/dist/read-password.js +32 -0
  95. package/dist/read-project-folders.d.ts +17 -0
  96. package/dist/read-project-folders.js +100 -0
  97. package/dist/reload-config.d.ts +2 -0
  98. package/dist/reload-config.js +11 -0
  99. package/dist/render-mermaid.d.ts +22 -0
  100. package/dist/render-mermaid.js +68 -0
  101. package/dist/view.d.ts +29 -0
  102. package/dist/view.js +30 -0
  103. package/package.json +30 -30
@@ -0,0 +1,256 @@
1
+ import { create as tarCreate, list as tarList } from 'tar';
2
+ import { minimatch } from 'minimatch';
3
+ import { error } from '@vltpkg/error-cause';
4
+ import * as ssri from 'ssri';
5
+ import assert from 'node:assert';
6
+ import { existsSync, statSync } from 'node:fs';
7
+ import { Spec } from '@vltpkg/spec';
8
+ import { join } from 'node:path';
9
+ import { parse, stringify } from 'polite-json';
10
+ /**
11
+ * Replace workspace: and catalog: specs with actual versions
12
+ * @param {NormalizedManifest} manifest_ - The manifest to process
13
+ * @param {LoadedConfig} config - The loaded configuration containing project root, monorepo, and catalog data
14
+ * @returns {NormalizedManifest} The manifest with replaced specs
15
+ */
16
+ const replaceWorkspaceAndCatalogSpecs = (manifest_, config) => {
17
+ // Create a json copy of the manifest to avoid modifying the original
18
+ // preserves original formatting symbols from polite-json
19
+ const manifest = parse(stringify(manifest_));
20
+ // Get workspace and catalog configuration from config
21
+ const { monorepo, catalog = {}, catalogs = {} } = config.options;
22
+ // Process dependency types
23
+ const depTypes = [
24
+ 'dependencies',
25
+ 'devDependencies',
26
+ 'optionalDependencies',
27
+ 'peerDependencies',
28
+ ];
29
+ for (const depType of depTypes) {
30
+ const deps = manifest[depType];
31
+ /* c8 ignore next */
32
+ if (!deps || typeof deps !== 'object')
33
+ continue;
34
+ const depsObj = deps;
35
+ for (const [depName, depSpec] of Object.entries(depsObj)) {
36
+ /* c8 ignore next */
37
+ if (typeof depSpec !== 'string')
38
+ continue;
39
+ const spec = Spec.parse(`${depName}@${depSpec}`, {
40
+ catalog,
41
+ catalogs,
42
+ });
43
+ switch (spec.type) {
44
+ case 'workspace': {
45
+ assert(monorepo, error(`No workspace configuration found for ${depName}`, {
46
+ found: depName,
47
+ }));
48
+ const workspaceName = spec.workspace;
49
+ assert(workspaceName, error(`No workspace name found for ${depName}`, {
50
+ found: depName,
51
+ }));
52
+ const workspace = monorepo.get(workspaceName);
53
+ assert(workspace, error(`Workspace '${workspaceName}' not found`, {
54
+ found: workspaceName,
55
+ validOptions: Array.from(monorepo.keys()),
56
+ }));
57
+ const actualVersion = workspace.manifest.version;
58
+ assert(actualVersion, error(`No version found for workspace '${workspaceName}'`, {
59
+ found: workspaceName,
60
+ wanted: 'package version',
61
+ }));
62
+ depsObj[depName] = actualVersion;
63
+ break;
64
+ }
65
+ case 'catalog': {
66
+ const catalogName = spec.catalog || '';
67
+ const targetCatalog = catalogName ? catalogs[catalogName] : catalog;
68
+ assert(targetCatalog, error(`Catalog '${catalogName}' not found`, {
69
+ found: catalogName,
70
+ validOptions: Object.keys(catalogs),
71
+ }));
72
+ const actualVersion = targetCatalog[depName];
73
+ assert(actualVersion, error(`Package '${depName}' not found in catalog '${catalogName || 'default'}'`, {
74
+ found: depName,
75
+ validOptions: Object.keys(targetCatalog),
76
+ }));
77
+ depsObj[depName] = actualVersion;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return manifest;
84
+ };
85
+ /**
86
+ * Create a tarball from a package directory
87
+ * @param {NormalizedManifest} manifest - The manifest of the package to pack
88
+ * @param {string} dir - The directory containing the package to pack
89
+ * @param {LoadedConfig} [config] - The loaded configuration (for workspace/catalog resolution)
90
+ * @returns {Promise<PackTarballResult>} The manifest, filename, and tarball data (unless dry run)
91
+ */
92
+ export const packTarball = async (manifest, dir, config) => {
93
+ let packDir = dir;
94
+ // Check if publishDirectory is configured via CLI flag or package.json publishConfig.directory
95
+ const cliPublishDir = config.get('publish-directory');
96
+ const manifestPublishDir = manifest.publishConfig?.directory;
97
+ const publishDirectory = cliPublishDir ?? manifestPublishDir;
98
+ if (publishDirectory) {
99
+ // CLI flag paths are used as-is; publishConfig.directory is relative to the package dir
100
+ const resolvedPublishDir = cliPublishDir ? publishDirectory : join(dir, publishDirectory);
101
+ // Validate that the publish directory exists and is a directory
102
+ assert(existsSync(resolvedPublishDir), error(`Publish directory does not exist: ${resolvedPublishDir}`, {
103
+ found: resolvedPublishDir,
104
+ }));
105
+ assert(statSync(resolvedPublishDir).isDirectory(), error(`Publish directory is not a directory: ${resolvedPublishDir}`, {
106
+ found: resolvedPublishDir,
107
+ wanted: 'directory',
108
+ }));
109
+ if (existsSync(join(resolvedPublishDir, 'package.json'))) {
110
+ manifest = config.options.packageJson.read(resolvedPublishDir);
111
+ }
112
+ packDir = resolvedPublishDir;
113
+ }
114
+ assert(manifest.name && manifest.version, error('Package must have a name and version'));
115
+ const processedManifest = replaceWorkspaceAndCatalogSpecs(manifest, config);
116
+ const filename = `${manifest.name.replace('@', '').replace('/', '-')}-${manifest.version}.tgz`;
117
+ const tarballName = `${manifest.name}-${manifest.version}.tgz`;
118
+ try {
119
+ config.options.packageJson.write(packDir, processedManifest);
120
+ const tarballData = await tarCreate({
121
+ cwd: packDir,
122
+ gzip: true,
123
+ portable: true,
124
+ prefix: 'package/',
125
+ filter: (path) => {
126
+ // Normalize path - remove leading './'
127
+ const normalizedPath = path.replace(/^\.\//, '');
128
+ // Always include root directory
129
+ if (path === '.' || normalizedPath === '') {
130
+ return true;
131
+ }
132
+ // Always exclude certain files/directories
133
+ const alwaysExcludePatterns = [
134
+ /^\.?\/?\.git(\/|$)/,
135
+ /^\.?\/?node_modules(\/|$)/,
136
+ /^\.?\/?\.nyc_output(\/|$)/,
137
+ /^\.?\/?coverage(\/|$)/,
138
+ /^\.?\/?\.DS_Store$/,
139
+ /^\.?\/?\.npmrc$/,
140
+ /^\.?\/?package-lock\.json$/,
141
+ /^\.?\/?yarn\.lock$/,
142
+ /^\.?\/?pnpm-lock\.yaml$/,
143
+ /^\.?\/?bun\.lockb$/,
144
+ /^\.?\/?bun\.lock$/,
145
+ /^\.?\/?vlt-lock\.json$/,
146
+ /~$/,
147
+ /\.swp$/,
148
+ /\.tgz$/,
149
+ ];
150
+ if (alwaysExcludePatterns.some(pattern => pattern.test(normalizedPath))) {
151
+ return false;
152
+ }
153
+ // Always include certain files
154
+ const alwaysIncludePatterns = [
155
+ /^README(\..*)?$/i,
156
+ /^CHANGELOG(\..*)?$/i,
157
+ /^HISTORY(\..*)?$/i,
158
+ /^LICENSE(\..*)?$/i,
159
+ /^LICENCE(\..*)?$/i,
160
+ ];
161
+ if (alwaysIncludePatterns.some(pattern => pattern.test(normalizedPath))) {
162
+ return true;
163
+ }
164
+ // Always include package.json
165
+ if (normalizedPath === 'package.json') {
166
+ return true;
167
+ }
168
+ // If files field is specified in package.json, use it for inclusion
169
+ const manifestWithFiles = manifest;
170
+ if (manifestWithFiles.files &&
171
+ Array.isArray(manifestWithFiles.files)) {
172
+ // Empty files array means exclude everything except always-included files
173
+ if (manifestWithFiles.files.length === 0) {
174
+ return false;
175
+ }
176
+ return manifestWithFiles.files.some((pattern) => {
177
+ if (pattern.endsWith('/')) {
178
+ const dirName = pattern.slice(0, -1);
179
+ const globPattern = pattern.replace(/\/$/, '/**');
180
+ const matchesDir = normalizedPath === dirName;
181
+ const matchesContents = minimatch(normalizedPath, globPattern, {
182
+ dot: true,
183
+ });
184
+ return matchesDir || matchesContents;
185
+ }
186
+ const directMatch = minimatch(normalizedPath, pattern, {
187
+ dot: true,
188
+ });
189
+ // Check if this path is a parent directory of the pattern
190
+ const isParentDir = pattern.includes('/') &&
191
+ pattern.startsWith(normalizedPath + '/');
192
+ // Check if this path is inside a directory matched by the pattern
193
+ // (e.g. files: ["dist"] should include dist/index.js)
194
+ const isChildOfPattern = minimatch(normalizedPath, pattern + '/**', { dot: true });
195
+ return directMatch || isParentDir || isChildOfPattern;
196
+ });
197
+ }
198
+ // Default behavior when no files field - exclude common development files
199
+ const defaultExcludePatterns = [
200
+ /^\.?\/?\.vscode(\/|$)/,
201
+ /^\.?\/?\.idea(\/|$)/,
202
+ /^\.?\/?\.gitignore$/,
203
+ /^\.?\/?\.npmignore$/,
204
+ /^\.?\/?\.editorconfig$/,
205
+ ];
206
+ return !defaultExcludePatterns.some(pattern => pattern.test(normalizedPath));
207
+ },
208
+ }, ['.']).concat();
209
+ let unpackedSize = 0;
210
+ const files = [];
211
+ await new Promise((resolve, reject) => {
212
+ const stream = tarList({
213
+ onentry: entry => {
214
+ if (entry.type === 'File') {
215
+ unpackedSize += entry.size;
216
+ // Remove the package/ prefix for cleaner file listing
217
+ const cleanPath = entry.path.replace(/^[^/]+\//, '');
218
+ if (cleanPath) {
219
+ // Skip empty paths
220
+ files.push(cleanPath);
221
+ }
222
+ }
223
+ },
224
+ });
225
+ stream
226
+ .on('end', () => resolve())
227
+ .on('error', reject)
228
+ .write(tarballData);
229
+ stream.end();
230
+ });
231
+ const integrityMap = ssri.fromData(tarballData, {
232
+ algorithms: [...new Set(['sha1', 'sha512'])],
233
+ });
234
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
235
+ const integrity = integrityMap.sha512?.[0]?.toString();
236
+ // @ts-expect-error -- types from DT are missing hexDigest
237
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
238
+ const shasum = integrityMap.sha1?.[0]?.hexDigest();
239
+ return {
240
+ name: manifest.name,
241
+ version: manifest.version,
242
+ filename,
243
+ tarballName,
244
+ tarballData,
245
+ unpackedSize,
246
+ files,
247
+ integrity,
248
+ shasum,
249
+ resolvedManifest: processedManifest,
250
+ };
251
+ }
252
+ finally {
253
+ // Restore the original package.json to the pack directory
254
+ config.options.packageJson.write(packDir, manifest);
255
+ }
256
+ };
@@ -0,0 +1,28 @@
1
+ import type { PathScurry } from 'path-scurry';
2
+ import type { AddImportersDependenciesMap, RemoveImportersDependenciesMap } from '@vltpkg/graph';
3
+ import type { Monorepo } from '@vltpkg/workspaces';
4
+ import type { LoadedConfig } from './config/index.ts';
5
+ export type ParsedAddArgs = {
6
+ add: AddImportersDependenciesMap;
7
+ };
8
+ export type ParsedRemoveArgs = {
9
+ remove: RemoveImportersDependenciesMap;
10
+ };
11
+ export type SaveTypes = {
12
+ 'save-dev'?: boolean;
13
+ 'save-optional'?: boolean;
14
+ 'save-peer'?: boolean;
15
+ 'save-prod'?: boolean;
16
+ };
17
+ export type WorkspaceTypes = {
18
+ workspace?: string[];
19
+ 'workspace-group'?: string[];
20
+ };
21
+ /**
22
+ * Parses the positional arguments into {@link AddImportersDependenciesMap}.
23
+ */
24
+ export declare const parseAddArgs: (config: LoadedConfig, scurry: PathScurry, monorepo?: Monorepo) => ParsedAddArgs;
25
+ /**
26
+ * Parses the positional arguments into {@link RemoveImportersDependenciesMap}.
27
+ */
28
+ export declare const parseRemoveArgs: (config: LoadedConfig, scurry: PathScurry, monorepo?: Monorepo) => ParsedRemoveArgs;
@@ -0,0 +1,103 @@
1
+ import { asDependency } from '@vltpkg/graph';
2
+ import { joinDepIDTuple } from '@vltpkg/dep-id';
3
+ import { Spec } from '@vltpkg/spec';
4
+ const rootDepID = joinDepIDTuple(['file', '.']);
5
+ /**
6
+ * Compute a DepID for the current working directory relative to the project
7
+ * root. Returns the root DepID if cwd is the project root, otherwise returns
8
+ * the computed DepID.
9
+ */
10
+ const getCwdDepID = (scurry) => {
11
+ const cwd = process.cwd();
12
+ const relPath = scurry.relativePosix(cwd);
13
+ // If cwd is the project root or outside it, return root DepID
14
+ if (!relPath || relPath.startsWith('..')) {
15
+ return rootDepID;
16
+ }
17
+ // Return a DepID for the nested folder (posix-style path)
18
+ return joinDepIDTuple(['file', relPath.split('\\').join('/')]);
19
+ };
20
+ /**
21
+ * Get the list of importers that are currently selected
22
+ * in {@link WorkspaceTypes}.
23
+ */
24
+ const getWorkspaceImporters = (opts, monorepo) => {
25
+ const res = new Set();
26
+ // collects DepID references to any selected workspace
27
+ if (monorepo) {
28
+ for (const ws of monorepo.filter(opts)) {
29
+ res.add(ws.id);
30
+ }
31
+ }
32
+ return res;
33
+ };
34
+ const getType = (opts) => opts['save-prod'] ? 'prod'
35
+ : opts['save-dev'] ? 'dev'
36
+ : opts['save-peer'] ?
37
+ opts['save-optional'] ?
38
+ 'peerOptional'
39
+ : 'peer'
40
+ : opts['save-optional'] ? 'optional'
41
+ : 'implicit';
42
+ class AddImportersDependenciesMapImpl extends Map {
43
+ modifiedDependencies = false;
44
+ }
45
+ class RemoveImportersDependenciesMapImpl extends Map {
46
+ modifiedDependencies = false;
47
+ }
48
+ /**
49
+ * Parses the positional arguments into {@link AddImportersDependenciesMap}.
50
+ */
51
+ export const parseAddArgs = (config, scurry, monorepo) => {
52
+ const add = new AddImportersDependenciesMapImpl();
53
+ const items = config.positionals;
54
+ const type = getType(config.values);
55
+ const importers = getWorkspaceImporters(config.values, monorepo);
56
+ const newDependencies = new Map();
57
+ const specOptions = config.options;
58
+ // nameless spec definitions will need to use their full
59
+ // stringified spec result instead of their name in order
60
+ // to have an unique key name in the resulting Map
61
+ const getName = (s) => s.name === '(unknown)' ? s.spec : s.name;
62
+ // parses each positional argument into a Spec and
63
+ // adds it to the new dependencies Map
64
+ for (const item of items) {
65
+ const spec = Spec.parseArgs(item, specOptions);
66
+ newDependencies.set(getName(spec), asDependency({ spec, type }));
67
+ add.modifiedDependencies = true;
68
+ }
69
+ // assigns the new dependencies to each selected workspace importer
70
+ for (const importer of importers) {
71
+ add.set(importer, newDependencies);
72
+ }
73
+ // if no workspaces were selected, default to the cwd importer which
74
+ // can be either the root or a nested folder in case the user is installing
75
+ // from a subfolder that is also a file: type dependency
76
+ if (!importers.size) {
77
+ const cwdDepID = getCwdDepID(scurry);
78
+ add.set(cwdDepID, newDependencies);
79
+ }
80
+ return {
81
+ add,
82
+ };
83
+ };
84
+ /**
85
+ * Parses the positional arguments into {@link RemoveImportersDependenciesMap}.
86
+ */
87
+ export const parseRemoveArgs = (config, scurry, monorepo) => {
88
+ const remove = new RemoveImportersDependenciesMapImpl();
89
+ const importers = getWorkspaceImporters(config.values, monorepo);
90
+ for (const importer of importers) {
91
+ remove.set(importer, new Set(config.positionals));
92
+ remove.modifiedDependencies = true;
93
+ }
94
+ // if no workspaces were selected, default to the cwd importer
95
+ if (!importers.size) {
96
+ const cwdDepID = getCwdDepID(scurry);
97
+ remove.set(cwdDepID, new Set(config.positionals));
98
+ remove.modifiedDependencies = true;
99
+ }
100
+ return {
101
+ remove,
102
+ };
103
+ };
@@ -0,0 +1,13 @@
1
+ import type { InspectOptions } from 'node:util';
2
+ import type { CommandUsage } from './index.ts';
3
+ export declare const formatOptions: {
4
+ readonly depth: number;
5
+ readonly maxArrayLength: number;
6
+ readonly maxStringLength: number;
7
+ };
8
+ export type ErrorFormatOptions = InspectOptions & {
9
+ maxLines?: number;
10
+ };
11
+ export type Formatter = (arg: unknown, options?: ErrorFormatOptions) => string;
12
+ export declare const indent: (lines: string, num?: number) => string;
13
+ export declare const printErr: (e: unknown, usage: CommandUsage, stderr: (...a: string[]) => void, baseOpts?: ErrorFormatOptions) => void;
@@ -0,0 +1,193 @@
1
+ import { splitDepID } from '@vltpkg/dep-id';
2
+ import { parseError } from '@vltpkg/output/error';
3
+ import { isErrorWithCause, isObject } from '@vltpkg/types';
4
+ import { XDG } from '@vltpkg/xdg';
5
+ import { isGraphRunError } from '@vltpkg/graph-run';
6
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { formatWithOptions } from 'node:util';
9
+ export const formatOptions = {
10
+ depth: Infinity,
11
+ maxArrayLength: Infinity,
12
+ maxStringLength: Infinity,
13
+ };
14
+ const isNonEmptyString = (v) => !!v && typeof v === 'string';
15
+ const formatURL = (v, format) => v instanceof URL ? v.toString() : /* c8 ignore next */ format(v);
16
+ const formatArray = (v, format, joiner = ', ') => Array.isArray(v) ? v.join(joiner) : /* c8 ignore next */ format(v);
17
+ export const indent = (lines, num = 2) => lines
18
+ .split('\n')
19
+ .map(l => ' '.repeat(num) + l)
20
+ .join('\n');
21
+ const writeErrorLog = (e, format) => {
22
+ try {
23
+ const dir = new XDG('vlt/error-logs').data();
24
+ const file = join(dir, `error-${process.pid}.log`);
25
+ mkdirSync(dir, { recursive: true });
26
+ writeFileSync(file, format(e, {
27
+ colors: false,
28
+ maxLines: Infinity,
29
+ }));
30
+ return file;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ };
36
+ const readErrorLog = (file) => {
37
+ try {
38
+ return readFileSync(file, 'utf8');
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ };
44
+ export const printErr = (e, usage, stderr, baseOpts) => {
45
+ const format = (arg, opts) => {
46
+ const { maxLines = 200, ...rest } = {
47
+ ...formatOptions,
48
+ ...baseOpts,
49
+ ...opts,
50
+ };
51
+ const lines = formatWithOptions(rest, arg).split('\n');
52
+ const totalLines = lines.length;
53
+ if (totalLines > maxLines) {
54
+ lines.length = maxLines;
55
+ lines.push(`... ${totalLines - maxLines} lines hidden ...`);
56
+ }
57
+ return lines.join('\n');
58
+ };
59
+ const err = parseError(e);
60
+ const knownError = printCode(err, usage, stderr, format);
61
+ const fileWritten = !knownError || knownError.file ? writeErrorLog(e, format) : null;
62
+ // We could not write an error log and its not a know error,
63
+ // so we print the entire formatted value.
64
+ if (!fileWritten && !knownError) {
65
+ return stderr(format(e));
66
+ }
67
+ if (err && !knownError) {
68
+ stderr(`${err.name}: ${err.message}`);
69
+ }
70
+ if (fileWritten) {
71
+ stderr('');
72
+ stderr(`Full details written to: ${fileWritten}`);
73
+ if (process.env.CI) {
74
+ const fullErrorLog = readErrorLog(fileWritten);
75
+ if (fullErrorLog) {
76
+ stderr('');
77
+ stderr('Full details:');
78
+ stderr(fullErrorLog);
79
+ }
80
+ }
81
+ }
82
+ if (!knownError || knownError.bug) {
83
+ stderr('');
84
+ stderr('Open an issue with the full error details at:');
85
+ stderr(indent('https://github.com/vltpkg/vltpkg/issues/new'));
86
+ }
87
+ };
88
+ const printCode = (err, usage, stderr, format) => {
89
+ if (!err)
90
+ return;
91
+ switch (err.cause?.code) {
92
+ case 'GRAPHRUN_TRAVERSAL': {
93
+ if (!isGraphRunError(err))
94
+ break;
95
+ const { node, path, cause } = err.cause;
96
+ stderr(`Graph traversal failure at: ${splitDepID(node.id).join(' ')}`);
97
+ if (Array.isArray(path) && path.length) {
98
+ stderr(indent(`Path: ${path.map(n => n.id).join(',')}`));
99
+ }
100
+ if (isErrorWithCause(cause) &&
101
+ isObject(cause.cause) &&
102
+ 'command' in cause.cause &&
103
+ 'stdout' in cause.cause &&
104
+ 'stderr' in cause.cause &&
105
+ 'status' in cause.cause &&
106
+ 'signal' in cause.cause &&
107
+ 'cwd' in cause.cause) {
108
+ const { command, args, cwd, stdout: cmdStdout, stderr: cmdStderr, status, signal, } = cause.cause;
109
+ stderr(`Command: ${command}`);
110
+ if (args && Array.isArray(args) && args.length) {
111
+ stderr(`Args: ${args.map(a => JSON.stringify(a)).join(', ')}`);
112
+ }
113
+ stderr(`Cwd: ${cwd}`);
114
+ if (cmdStderr || cmdStdout) {
115
+ stderr('');
116
+ if (isNonEmptyString(cmdStderr)) {
117
+ stderr(cmdStderr);
118
+ }
119
+ if (isNonEmptyString(cmdStdout)) {
120
+ stderr(cmdStdout);
121
+ }
122
+ stderr('');
123
+ }
124
+ if (signal !== null)
125
+ stderr(`Signal: ${format(signal)}`);
126
+ if (status !== null)
127
+ stderr(`Status: ${format(status)}`);
128
+ }
129
+ return { file: true };
130
+ }
131
+ case 'EUSAGE': {
132
+ const { found, validOptions } = err.cause;
133
+ stderr(usage().usage());
134
+ stderr(`Usage Error: ${err.message}`);
135
+ if (found) {
136
+ stderr(indent(`Found: ${format(found)}`));
137
+ }
138
+ if (validOptions) {
139
+ stderr(indent(`Valid options: ${formatArray(validOptions, format)}`));
140
+ }
141
+ return {};
142
+ }
143
+ case 'ERESOLVE': {
144
+ const { url, from, response, spec } = err.cause;
145
+ stderr(`Resolve Error: ${err.message}`);
146
+ if (url) {
147
+ stderr(indent(`While fetching: ${formatURL(url, format)}`));
148
+ }
149
+ if (spec) {
150
+ stderr(indent(`To satisfy: ${format(spec)}`));
151
+ }
152
+ if (from) {
153
+ stderr(indent(`From: ${format(from)}`));
154
+ }
155
+ if (response) {
156
+ stderr(indent(`Response: ${format(response)}`));
157
+ }
158
+ return { file: true };
159
+ }
160
+ case 'EREQUEST': {
161
+ const { url, method } = err.cause;
162
+ const { code, syscall } = err.cause.cause ?? {};
163
+ stderr(`Request Error: ${err.message}`);
164
+ if (code) {
165
+ stderr(indent(`Code: ${format(code)}`));
166
+ }
167
+ if (syscall) {
168
+ stderr(indent(`Syscall: ${format(syscall)}`));
169
+ }
170
+ if (url) {
171
+ stderr(indent(`URL: ${formatURL(url, format)}`));
172
+ }
173
+ if (method) {
174
+ stderr(indent(`Method: ${format(method)}`));
175
+ }
176
+ return { file: true };
177
+ }
178
+ case 'ECONFIG': {
179
+ const { found, wanted, validOptions } = err.cause;
180
+ stderr(`Config Error: ${err.message}`);
181
+ if (found) {
182
+ stderr(indent(`Found: ${format(found)}`));
183
+ }
184
+ if (wanted) {
185
+ stderr(indent(`Wanted: ${format(wanted)}`));
186
+ }
187
+ if (validOptions) {
188
+ stderr(indent(`Valid Options: ${format(validOptions)}`));
189
+ }
190
+ return {};
191
+ }
192
+ }
193
+ };
@@ -0,0 +1,17 @@
1
+ import type { DiffFilesProvider } from '@vltpkg/query';
2
+ /**
3
+ * Validates that a commitish string is safe to use
4
+ * in a git command.
5
+ */
6
+ export declare const validateCommitish: (commitish: string) => string;
7
+ /**
8
+ * Creates a {@link DiffFilesProvider} that uses
9
+ * `git diff --name-only` to determine changed files.
10
+ * @param {string} projectRoot - The absolute path to the
11
+ * project root directory where git commands will be
12
+ * executed.
13
+ * @returns {DiffFilesProvider} A function that takes a
14
+ * commitish and returns a Set of changed file paths
15
+ * relative to the project root.
16
+ */
17
+ export declare const createDiffFilesProvider: (projectRoot: string) => DiffFilesProvider;
@@ -0,0 +1,63 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { error } from '@vltpkg/error-cause';
3
+ /**
4
+ * Pattern to validate commitish arguments.
5
+ * Allows branch names, tags, SHAs, HEAD~N, HEAD^N, etc.
6
+ * Rejects shell metacharacters to prevent command injection.
7
+ */
8
+ const VALID_COMMITISH = /^[a-zA-Z0-9_./@^~:#{}-]+$/;
9
+ /**
10
+ * Validates that a commitish string is safe to use
11
+ * in a git command.
12
+ */
13
+ export const validateCommitish = (commitish) => {
14
+ if (!commitish) {
15
+ throw error('Missing commitish argument for :diff() selector');
16
+ }
17
+ if (!VALID_COMMITISH.test(commitish)) {
18
+ throw error('Invalid commitish argument for :diff() selector', {
19
+ found: commitish,
20
+ });
21
+ }
22
+ return commitish;
23
+ };
24
+ /**
25
+ * Creates a {@link DiffFilesProvider} that uses
26
+ * `git diff --name-only` to determine changed files.
27
+ * @param {string} projectRoot - The absolute path to the
28
+ * project root directory where git commands will be
29
+ * executed.
30
+ * @returns {DiffFilesProvider} A function that takes a
31
+ * commitish and returns a Set of changed file paths
32
+ * relative to the project root.
33
+ */
34
+ export const createDiffFilesProvider = (projectRoot) => {
35
+ // Cache results per commitish to avoid repeated git calls
36
+ const cache = new Map();
37
+ return (commitish) => {
38
+ const cached = cache.get(commitish);
39
+ if (cached)
40
+ return cached;
41
+ const safeCommitish = validateCommitish(commitish);
42
+ try {
43
+ const stdout = execSync(`git diff --name-only ${safeCommitish}`, {
44
+ cwd: projectRoot,
45
+ encoding: 'utf8',
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ timeout: 30_000,
48
+ });
49
+ const files = new Set();
50
+ for (const line of stdout.split('\n')) {
51
+ const trimmed = line.trim();
52
+ if (trimmed) {
53
+ files.add(trimmed);
54
+ }
55
+ }
56
+ cache.set(commitish, files);
57
+ return files;
58
+ }
59
+ catch (err) {
60
+ throw error(`Failed to run git diff for commitish: ${safeCommitish}`, { cause: err });
61
+ }
62
+ };
63
+ };