@vltpkg/cli-sdk 1.5.0 → 1.6.1

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 (59) hide show
  1. package/dist/commands/build.d.ts +8 -3
  2. package/dist/commands/build.js +31 -3
  3. package/dist/commands/ci.d.ts +1 -1
  4. package/dist/commands/create.d.ts +2 -1
  5. package/dist/commands/create.js +2 -3
  6. package/dist/commands/exec.d.ts +2 -1
  7. package/dist/commands/exec.js +2 -3
  8. package/dist/commands/init.js +5 -2
  9. package/dist/commands/install/reporter.d.ts +1 -1
  10. package/dist/commands/install/reporter.js +45 -5
  11. package/dist/commands/install.d.ts +21 -3
  12. package/dist/commands/install.js +84 -30
  13. package/dist/commands/list.js +5 -1
  14. package/dist/commands/query.js +5 -1
  15. package/dist/commands/setup.d.ts +28 -2
  16. package/dist/commands/setup.js +185 -3
  17. package/dist/commands/skills.js +2 -3
  18. package/dist/commands/token.js +1 -1
  19. package/dist/commands/uninstall.d.ts +10 -3
  20. package/dist/commands/uninstall.js +28 -2
  21. package/dist/commands/update.d.ts +1 -1
  22. package/dist/config/definition.d.ts +32 -0
  23. package/dist/config/definition.js +60 -0
  24. package/dist/config/index.d.ts +26 -3
  25. package/dist/config/index.js +149 -7
  26. package/dist/global-bin-dir.d.ts +37 -0
  27. package/dist/global-bin-dir.js +138 -0
  28. package/dist/global-project.d.ts +18 -0
  29. package/dist/global-project.js +67 -0
  30. package/dist/global-state.d.ts +24 -0
  31. package/dist/global-state.js +94 -0
  32. package/dist/global.d.ts +78 -0
  33. package/dist/global.js +409 -0
  34. package/dist/index.js +7 -2
  35. package/dist/install-receipt.d.ts +40 -0
  36. package/dist/install-receipt.js +69 -0
  37. package/dist/output.js +13 -0
  38. package/dist/pack-tarball.d.ts +3 -1
  39. package/dist/pack-tarball.js +29 -19
  40. package/dist/parse-add-remove-args.js +2 -14
  41. package/dist/query-auth.d.ts +3 -2
  42. package/dist/query-auth.js +6 -4
  43. package/dist/reload-config.js +2 -1
  44. package/dist/require-registry.d.ts +7 -0
  45. package/dist/require-registry.js +13 -1
  46. package/dist/self-update.d.ts +74 -0
  47. package/dist/self-update.js +401 -0
  48. package/dist/shell-setup/env.d.ts +9 -0
  49. package/dist/shell-setup/env.js +25 -0
  50. package/dist/shell-setup/index.d.ts +69 -0
  51. package/dist/shell-setup/index.js +213 -0
  52. package/dist/shell-setup/posix.d.ts +36 -0
  53. package/dist/shell-setup/posix.js +158 -0
  54. package/dist/shell-setup/windows.d.ts +39 -0
  55. package/dist/shell-setup/windows.js +151 -0
  56. package/dist/shell.d.ts +18 -0
  57. package/dist/shell.js +110 -0
  58. package/dist/telemetry.js +8 -1
  59. package/package.json +30 -28
@@ -0,0 +1,78 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
2
+ import type { AddImportersDependenciesMap, Graph, RemoveImportersDependenciesMap } from '@vltpkg/graph';
3
+ import { RollbackRemove } from '@vltpkg/rollback-remove';
4
+ import { Spec } from '@vltpkg/spec';
5
+ import type { PathScurry } from 'path-scurry';
6
+ import type { LoadedConfig } from './config/index.ts';
7
+ import type { GlobalBinDirSource, ResolvedGlobalBinDir } from './global-bin-dir.ts';
8
+ import type { Shell, ShellOptions } from './shell.ts';
9
+ /**
10
+ * Bins linked by a global install. `conflicts` are names skipped because
11
+ * another global package or a foreign file owns them (`--force`
12
+ * overwrites).
13
+ */
14
+ export type GlobalBins = {
15
+ binDir: string;
16
+ source: GlobalBinDirSource;
17
+ bins: string[];
18
+ conflicts: string[];
19
+ inPath: boolean;
20
+ /** other bin dirs that had links removed */
21
+ otherDirs: string[];
22
+ /** self dir passed over as unwritable, when worth saying */
23
+ notWritable?: string;
24
+ };
25
+ export type LinkGlobalBinsOptions = {
26
+ projectRoot: string;
27
+ scurry: PathScurry;
28
+ force?: boolean;
29
+ binDir: ResolvedGlobalBinDir;
30
+ /** tried once when the self dir refuses writes */
31
+ fallback?: () => Promise<ResolvedGlobalBinDir>;
32
+ };
33
+ export type GlobalAddArgs = {
34
+ add: AddImportersDependenciesMap;
35
+ /** ws dirs created by this run, removed if the install fails */
36
+ created: string[];
37
+ /** importers to link bins for, undefined = all */
38
+ importers?: Set<DepID>;
39
+ };
40
+ export type GlobalRemoveArgs = {
41
+ remove: RemoveImportersDependenciesMap;
42
+ dirs: string[];
43
+ };
44
+ /** throws `EUSAGE` for options not supported with `--global` */
45
+ export declare const assertGlobalOptions: (conf: LoadedConfig) => void;
46
+ /**
47
+ * A positional as a named spec, file: paths made absolute. Once per
48
+ * conf: a nameless spec fetches its manifest.
49
+ */
50
+ export declare const globalSpec: (item: string, conf: LoadedConfig) => Promise<Spec>;
51
+ /** one workspace per positional, created if missing */
52
+ export declare const parseGlobalAddArgs: (conf: LoadedConfig) => Promise<GlobalAddArgs>;
53
+ /** the workspace of each positional, which must exist */
54
+ export declare const parseGlobalRemoveArgs: (conf: LoadedConfig) => GlobalRemoveArgs;
55
+ /** rm -rf the given workspace dirs */
56
+ export declare const removeGlobalWorkspaces: (dirs: string[]) => Promise<void>;
57
+ /**
58
+ * Remove bins in `binDir` that link into any of `dirs`.
59
+ * Returns the removed names, sorted.
60
+ */
61
+ export declare const unlinkGlobalBins: (binDir: string, dirs: string[], remover?: RollbackRemove) => Promise<string[]>;
62
+ /**
63
+ * Link the bins of the deps of global workspaces into `opts.binDir`,
64
+ * after removing the ones they owned there and in other bin dirs used
65
+ * before. All workspaces when no `importers` given.
66
+ */
67
+ export declare const linkGlobalBins: (graph: Graph, opts: LinkGlobalBinsOptions, importers?: Set<DepID>) => Promise<GlobalBins>;
68
+ /**
69
+ * Unlink the bins of the given workspace dirs from `binDir` (default
70
+ * `<root>/bin`) and other bin dirs used before, and remove them.
71
+ */
72
+ export declare const removeGlobalPackages: (projectRoot: string, dirs: string[], binDir?: string) => Promise<void>;
73
+ /** whether `dir` is in the `PATH` */
74
+ export declare const isInPath: (dir: string, PATH?: string) => boolean;
75
+ /** human lines about linked bins, with the `PATH` fix when needed */
76
+ export declare const globalBinsLines: (g: GlobalBins, opts?: ShellOptions & {
77
+ shell?: Shell;
78
+ }) => string[];
package/dist/global.js ADDED
@@ -0,0 +1,409 @@
1
+ import { cmdShimIfExists, findCmdShimIfExists, } from '@vltpkg/cmd-shim';
2
+ import { joinDepIDTuple } from '@vltpkg/dep-id';
3
+ import { error } from '@vltpkg/error-cause';
4
+ import { RollbackRemove } from '@vltpkg/rollback-remove';
5
+ import { Spec } from '@vltpkg/spec';
6
+ import { existsSync, readdirSync, realpathSync } from 'node:fs';
7
+ import { lstat, mkdir, readdir, realpath, rm, stat, symlink, writeFile, } from 'node:fs/promises';
8
+ import { homedir } from 'node:os';
9
+ import { basename, delimiter, isAbsolute, relative, resolve, sep, } from 'node:path';
10
+ import { globalBinDir, globalWorkspaceName, globalWorkspacePath, globalWorkspacesDir, isInside, samePath, } from "./global-project.js";
11
+ import { readGlobalState, updateGlobalState } from "./global-state.js";
12
+ import { parseSpecArg } from "./require-registry.js";
13
+ import { detectShell, prettyPath, shellPathCommand, shellRcFile, } from "./shell.js";
14
+ import { canSetupPath } from "./shell-setup/index.js";
15
+ const unsupported = [
16
+ 'workspace',
17
+ 'workspace-group',
18
+ 'lockfile-only',
19
+ ];
20
+ /** throws `EUSAGE` for options not supported with `--global` */
21
+ export const assertGlobalOptions = (conf) => {
22
+ for (const name of unsupported) {
23
+ const found = conf.values[name];
24
+ if (found !== undefined && found !== false) {
25
+ throw error(`--${name} is not supported with --global`, {
26
+ code: 'EUSAGE',
27
+ name,
28
+ found,
29
+ });
30
+ }
31
+ }
32
+ };
33
+ const workspaceId = (path) => joinDepIDTuple(['workspace', path]);
34
+ const specs = new WeakMap();
35
+ /**
36
+ * A positional as a named spec, file: paths made absolute. Once per
37
+ * conf: a nameless spec fetches its manifest.
38
+ */
39
+ export const globalSpec = (item, conf) => {
40
+ const known = specs.get(conf) ?? new Map();
41
+ specs.set(conf, known);
42
+ const spec = known.get(item) ?? namedSpec(item, conf);
43
+ known.set(item, spec);
44
+ return spec;
45
+ };
46
+ const namedSpec = async (item, conf) => {
47
+ const options = conf.options;
48
+ let spec = parseSpecArg(item, options);
49
+ if (spec.type === 'file' && spec.file && !isAbsolute(spec.file)) {
50
+ const file = resolve(process.cwd(), spec.file)
51
+ .split(sep)
52
+ .join('/');
53
+ spec = Spec.parse(spec.name, `file:${file}`, options);
54
+ }
55
+ // a bare name gets the default tag, like npm
56
+ if (spec.type === 'registry' && !spec.bareSpec) {
57
+ return Spec.parse(spec.name, options.tag, options);
58
+ }
59
+ if (spec.name !== '(unknown)')
60
+ return spec;
61
+ const { name } = await options.packageInfo.manifest(spec, {
62
+ from: process.cwd(),
63
+ });
64
+ if (!name) {
65
+ throw error('Could not determine package name', {
66
+ code: 'EUSAGE',
67
+ spec,
68
+ });
69
+ }
70
+ return Spec.parse(name, spec.bareSpec, options);
71
+ };
72
+ // `@s+p-ws` -> `@s/p`
73
+ const wsPackageName = (dir) => dir.replace(/-ws$/, '').replace('+', '/');
74
+ /**
75
+ * The workspace of `name`. Matched by exact case, since the fs may
76
+ * ignore it but workspace ids don't. `variant` is an installed name
77
+ * that differs only in case.
78
+ */
79
+ const findWorkspace = (projectRoot, name) => {
80
+ const path = globalWorkspacePath(name);
81
+ const dir = resolve(projectRoot, path);
82
+ const base = basename(path);
83
+ const entries = readdirSync(resolve(projectRoot, globalWorkspacesDir));
84
+ const variant = entries.find(e => e !== base && e.toLowerCase() === base.toLowerCase());
85
+ return {
86
+ path,
87
+ dir,
88
+ exists: entries.includes(base) &&
89
+ existsSync(resolve(dir, 'package.json')),
90
+ variant: variant && wsPackageName(variant),
91
+ };
92
+ };
93
+ /** one workspace per positional, created if missing */
94
+ export const parseGlobalAddArgs = async (conf) => {
95
+ const { positionals, projectRoot } = conf;
96
+ const add = Object.assign(new Map(), { modifiedDependencies: positionals.length > 0 });
97
+ const created = [];
98
+ const importers = new Set();
99
+ try {
100
+ for (const item of positionals) {
101
+ const spec = await globalSpec(item, conf);
102
+ const { path, dir, exists, variant } = findWorkspace(projectRoot, spec.name);
103
+ if (!exists) {
104
+ if (variant) {
105
+ throw error(`${spec.name} differs only in case from global package ${variant}`, { code: 'EUSAGE', found: spec.name });
106
+ }
107
+ await mkdir(dir, { recursive: true });
108
+ created.push(dir);
109
+ const ws = {
110
+ name: globalWorkspaceName(spec.name),
111
+ private: true,
112
+ };
113
+ await writeFile(resolve(dir, 'package.json'), JSON.stringify(ws, null, 2) + '\n');
114
+ }
115
+ const id = workspaceId(path);
116
+ importers.add(id);
117
+ const dep = { spec, type: 'implicit' };
118
+ add.set(id, new Map([[spec.name, dep]]));
119
+ }
120
+ }
121
+ catch (er) {
122
+ await removeGlobalWorkspaces(created);
123
+ throw er;
124
+ }
125
+ // reload the monorepo with the new workspaces
126
+ if (created.length)
127
+ conf.resetOptions(projectRoot);
128
+ return {
129
+ add,
130
+ created,
131
+ importers: positionals.length ? importers : undefined,
132
+ };
133
+ };
134
+ /** the workspace of each positional, which must exist */
135
+ export const parseGlobalRemoveArgs = (conf) => {
136
+ const { positionals, projectRoot } = conf;
137
+ if (!positionals.length) {
138
+ throw error('Missing package name(s) to uninstall', {
139
+ code: 'EUSAGE',
140
+ });
141
+ }
142
+ const remove = Object.assign(new Map(), { modifiedDependencies: true });
143
+ const dirs = [];
144
+ for (const name of positionals) {
145
+ const { path, dir, exists, variant } = findWorkspace(projectRoot, name);
146
+ const id = workspaceId(path);
147
+ if (remove.has(id))
148
+ continue;
149
+ if (!exists) {
150
+ throw error(`${name} is not installed globally`, {
151
+ code: 'EUSAGE',
152
+ found: name,
153
+ ...(variant ? { validOptions: [variant] } : null),
154
+ });
155
+ }
156
+ remove.set(id, new Set([name]));
157
+ dirs.push(dir);
158
+ }
159
+ return { remove, dirs };
160
+ };
161
+ /** rm -rf the given workspace dirs */
162
+ export const removeGlobalWorkspaces = async (dirs) => {
163
+ await Promise.all(dirs.map(dir => rm(dir, { recursive: true, force: true })));
164
+ };
165
+ const shimExts = ['', '.cmd', '.ps1', '.pwsh'];
166
+ // the files a bin link is made of
167
+ const linkFiles = (link) => process.platform === 'win32' ?
168
+ shimExts.map(ext => link + ext)
169
+ : [link];
170
+ const realOr = (p) => realpath(p).catch(() => p);
171
+ /**
172
+ * Remove bins in `binDir` that link into any of `dirs`.
173
+ * Returns the removed names, sorted.
174
+ */
175
+ export const unlinkGlobalBins = async (binDir, dirs, remover) => {
176
+ const entries = await readdir(binDir, {
177
+ withFileTypes: true,
178
+ }).catch(() => []);
179
+ const names = new Set();
180
+ for (const e of entries) {
181
+ const name = e.name.replace(/\.(cmd|ps1|pwsh)$/i, '');
182
+ // never read plain files, they may be big binaries
183
+ if (name !== e.name || e.isSymbolicLink())
184
+ names.add(name);
185
+ }
186
+ // links may use either spelling of a symlinked dir
187
+ const roots = [...dirs, ...(await Promise.all(dirs.map(realOr)))];
188
+ const removed = [];
189
+ for (const name of names) {
190
+ const link = resolve(binDir, name);
191
+ const found = await findCmdShimIfExists(link);
192
+ if (!found || !roots.some(dir => isInside(dir, found[1])))
193
+ continue;
194
+ await Promise.all(linkFiles(link).map(f => remover ? remover.rm(f) : rm(f, { force: true })));
195
+ removed.push(name);
196
+ }
197
+ return removed.sort((a, b) => a.localeCompare(b, 'en'));
198
+ };
199
+ const exists = async (link) => (await Promise.all(linkFiles(link).map(f => lstat(f).then(() => true, () => false)))).some(Boolean);
200
+ // bin names from a lockfile are not re-validated
201
+ const safeBin = (bin) => !!bin &&
202
+ bin === basename(bin) &&
203
+ !bin.includes('\\') &&
204
+ bin !== '.' &&
205
+ bin !== '..';
206
+ const sorted = (s) => [...s].sort((a, b) => a.localeCompare(b, 'en'));
207
+ const dedupe = (dirs) => dirs.filter((d, i) => dirs.findIndex(x => samePath(x, d)) === i);
208
+ // recorded dirs and `<root>/bin`, but not `current`
209
+ const otherBinDirs = async (root, current, state) => {
210
+ const g = await realpath(globalBinDir(root)).catch(() => undefined);
211
+ return dedupe([...(state.binDirs ?? []), ...(g ? [g] : [])]).filter(d => !samePath(d, current));
212
+ };
213
+ const isDir = (d) => stat(d).then(s => s.isDirectory(), () => false);
214
+ const isPermError = (er) => er instanceof Error &&
215
+ /^(EACCES|EPERM)$/.test(String(er.code));
216
+ // --force in a shared dir: only links and shims, never plain files
217
+ // (other tools' binaries) or the running vlt or node
218
+ const replaceable = async (link, keep) => {
219
+ if (process.platform !== 'win32' &&
220
+ !(await lstat(link)).isSymbolicLink()) {
221
+ return false;
222
+ }
223
+ const found = await findCmdShimIfExists(link);
224
+ if (!found)
225
+ return false;
226
+ const to = await realOr(found[1]);
227
+ return !keep.some(k => samePath(k, to));
228
+ };
229
+ const linkBins = async (graph, opts, importers) => {
230
+ const { projectRoot, scurry, force } = opts;
231
+ const { dir: binDir, source } = opts.binDir;
232
+ const state = await readGlobalState(projectRoot);
233
+ const targets = [...graph.importers]
234
+ .filter(i => i !== graph.mainImporter &&
235
+ (!importers || importers.has(i.id)))
236
+ .sort((a, b) => a.location.localeCompare(b.location, 'en'));
237
+ const owned = importers ?
238
+ targets.map(i => i.resolvedLocation(scurry))
239
+ : [resolve(projectRoot, globalWorkspacesDir)];
240
+ const keep = force && source !== 'global-dir' ?
241
+ await Promise.all([process.execPath, ...process.argv.slice(1, 2)].map(realOr))
242
+ : undefined;
243
+ const remover = new RollbackRemove();
244
+ const bins = new Set();
245
+ const conflicts = new Set();
246
+ const otherDirs = [];
247
+ const created = [];
248
+ try {
249
+ await unlinkGlobalBins(binDir, owned, remover);
250
+ for (const d of await otherBinDirs(projectRoot, binDir, state)) {
251
+ // best effort, an old dir may be gone or read-only
252
+ const removed = await unlinkGlobalBins(d, owned, remover).catch(() => []);
253
+ if (removed.length)
254
+ otherDirs.push(d);
255
+ }
256
+ await mkdir(binDir, { recursive: true });
257
+ for (const importer of targets) {
258
+ // canonical, like binDir: short relative links
259
+ const nm = await realOr(importer.nodeModules(scurry));
260
+ for (const edge of importer.edgesOut.values()) {
261
+ for (const [bin, path] of Object.entries(edge.to?.bins ?? {})) {
262
+ if (!safeBin(bin))
263
+ continue;
264
+ const target = resolve(nm, edge.spec.name, path);
265
+ // win32 shims need the target, posix links may dangle
266
+ const win32 = process.platform === 'win32';
267
+ if (win32 && !(await stat(target).catch(() => false))) {
268
+ continue;
269
+ }
270
+ const link = resolve(binDir, bin);
271
+ if ((await exists(link)) &&
272
+ (!force || (keep && !(await replaceable(link, keep))))) {
273
+ conflicts.add(bin);
274
+ continue;
275
+ }
276
+ if (win32) {
277
+ created.push(link);
278
+ await cmdShimIfExists(target, link, remover);
279
+ }
280
+ else {
281
+ await remover.rm(link);
282
+ // already moved aside, so ours from this run (--force)
283
+ await rm(link, { force: true });
284
+ created.push(link);
285
+ await symlink(relative(binDir, target), link);
286
+ }
287
+ bins.add(bin);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ catch (er) {
293
+ // the remover only restores what it moved aside
294
+ await Promise.all(created.flatMap(linkFiles).map(f => rm(f, { force: true })));
295
+ await remover.rollback();
296
+ throw er;
297
+ }
298
+ remover.confirm();
299
+ return {
300
+ binDir: opts.binDir,
301
+ state,
302
+ bins: sorted(bins),
303
+ conflicts: sorted(conflicts),
304
+ otherDirs,
305
+ };
306
+ };
307
+ // after linking, so a failed write never triggers the fallback
308
+ const recordBinDir = async (projectRoot, { binDir, state, bins, conflicts, otherDirs }) => {
309
+ const { dir, source, inPath, notWritable } = binDir;
310
+ // local-bin notice once, global-dir reason with every PATH warning
311
+ const shown = (bins.length > 0 || conflicts.length > 0) &&
312
+ (source === 'global-dir' || !state.localBinNoticed);
313
+ const why = shown ? notWritable : undefined;
314
+ const notice = !!why && source === 'local-bin';
315
+ const recorded = [];
316
+ for (const d of state.binDirs ?? []) {
317
+ if (await isDir(d))
318
+ recorded.push(d);
319
+ }
320
+ await updateGlobalState(projectRoot, s => ({
321
+ ...s,
322
+ binDirs: dedupe([...recorded, dir]),
323
+ ...(notice ? { localBinNoticed: true } : {}),
324
+ }));
325
+ return {
326
+ binDir: dir,
327
+ source,
328
+ bins,
329
+ conflicts,
330
+ inPath,
331
+ otherDirs,
332
+ ...(why ? { notWritable: why } : {}),
333
+ };
334
+ };
335
+ /**
336
+ * Link the bins of the deps of global workspaces into `opts.binDir`,
337
+ * after removing the ones they owned there and in other bin dirs used
338
+ * before. All workspaces when no `importers` given.
339
+ */
340
+ export const linkGlobalBins = async (graph, opts, importers) => {
341
+ let linked;
342
+ try {
343
+ linked = await linkBins(graph, opts, importers);
344
+ }
345
+ catch (er) {
346
+ const { binDir, fallback } = opts;
347
+ // access(W_OK) can pass where writes fail: win32, macOS TCC
348
+ if (binDir.source !== 'self' || !fallback || !isPermError(er)) {
349
+ throw er;
350
+ }
351
+ linked = await linkBins(graph, { ...opts, binDir: await fallback() }, importers);
352
+ }
353
+ return recordBinDir(opts.projectRoot, linked);
354
+ };
355
+ /**
356
+ * Unlink the bins of the given workspace dirs from `binDir` (default
357
+ * `<root>/bin`) and other bin dirs used before, and remove them.
358
+ */
359
+ export const removeGlobalPackages = async (projectRoot, dirs, binDir) => {
360
+ const current = binDir ?? globalBinDir(projectRoot);
361
+ await unlinkGlobalBins(current, dirs);
362
+ const state = await readGlobalState(projectRoot);
363
+ for (const d of await otherBinDirs(projectRoot, current, state)) {
364
+ await unlinkGlobalBins(d, dirs).catch(() => []);
365
+ }
366
+ await removeGlobalWorkspaces(dirs);
367
+ };
368
+ /** whether `dir` is in the `PATH` */
369
+ export const isInPath = (dir, PATH = process.env.PATH ?? '') => PATH.split(delimiter).some(p => !!p && samePath(p, dir));
370
+ // bin dirs are canonical, home may not be
371
+ const homeOf = (dir, home, win32) => {
372
+ if (prettyPath(dir, home, win32) !== dir)
373
+ return home;
374
+ try {
375
+ return realpathSync.native(home);
376
+ }
377
+ catch {
378
+ return home;
379
+ }
380
+ };
381
+ /** human lines about linked bins, with the `PATH` fix when needed */
382
+ export const globalBinsLines = (g, opts = {}) => {
383
+ const { shell = detectShell(opts.env, opts.platform), platform = process.platform, } = opts;
384
+ const win32 = platform === 'win32';
385
+ const home = homeOf(g.binDir, opts.home ?? homedir(), win32);
386
+ const pretty = (p) => prettyPath(p, home, win32);
387
+ const dir = pretty(g.binDir);
388
+ const lines = [];
389
+ if (g.notWritable) {
390
+ lines.push(`ℹ️ Using ${dir} because ${pretty(g.notWritable)} is not writable.`);
391
+ }
392
+ if (g.bins.length) {
393
+ lines.push(`🔗 Linked ${g.bins.join(', ')} in ${dir}`);
394
+ }
395
+ if (g.conflicts.length) {
396
+ lines.push(`⚠️ Skipped ${g.conflicts.join(', ')}: already in ${dir} (use --force to overwrite)`);
397
+ }
398
+ if (!g.inPath && (g.bins.length || g.conflicts.length)) {
399
+ const rc = shellRcFile(shell, opts);
400
+ const what = g.bins.length ? g.bins.join(', ') : 'global bins';
401
+ const fix = canSetupPath(shell, platform) ?
402
+ 'Run `vlt setup --path` to add it'
403
+ : 'Set --global-bin-dir to a folder on your PATH';
404
+ lines.push(
405
+ // a paragraph of its own: the install offer follows it
406
+ '', `⚠️ ${dir} is not on your PATH, so ${what} can't be run yet.`, ` ${fix}, or ${rc ? `add this line to ${rc}` : 'run'}:`, ` ${shellPathCommand(shell, g.binDir, { ...opts, home })}`);
407
+ }
408
+ return lines;
409
+ };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { format } from 'node:util';
2
2
  import { asRootError, findRootError } from '@vltpkg/output/error';
3
+ import { isError } from '@vltpkg/types';
3
4
  import { setVltJsonOnly } from '@vltpkg/workspaces';
4
5
  import { loadPackageJson } from 'package-json-from-dist';
5
6
  import { getSortedCliOptions, getSortedKeys, } from "./config/definition.js";
@@ -15,7 +16,7 @@ const loadVlt = async (cwd, argv) => {
15
16
  catch (e) {
16
17
  const code = findRootError(e, { code: 'ECONFIG' }) ? 'ECONFIG' : 'JACKSPEAK';
17
18
  const err = asRootError(e, { code });
18
- const { found, path, wanted, name } = err.cause;
19
+ const { found, path, wanted, name, cause } = err.cause;
19
20
  const isConfigFile = typeof path === 'string';
20
21
  const msg = isConfigFile ? `Problem in Config File ${path}`
21
22
  : code === 'ECONFIG' ? 'Config Error'
@@ -32,6 +33,9 @@ const loadVlt = async (cwd, argv) => {
32
33
  }
33
34
  if (wanted)
34
35
  stderr(indent(`Wanted: ${format(wanted)}`));
36
+ // eg the global project can't be created
37
+ if (isError(cause))
38
+ stderr(indent(`Cause: ${cause.message}`));
35
39
  if (validOptions) {
36
40
  stderr(indent('Valid Options:'));
37
41
  stderr(indent(validOptions.join('\n'), 4));
@@ -54,7 +58,8 @@ const run = async () => {
54
58
  setVltJsonOnly(vlt.command === 'ci');
55
59
  const { monorepo } = vlt.options;
56
60
  // Infer the workspace by being in that directory.
57
- if (vlt.get('workspace') === undefined) {
61
+ // Not for the global project, cwd is unrelated to it.
62
+ if (vlt.get('workspace') === undefined && !vlt.globalRoot) {
58
63
  const ws = monorepo?.get(cwd);
59
64
  if (ws) {
60
65
  vlt.values.workspace = [ws.path];
@@ -0,0 +1,40 @@
1
+ export declare const receiptName = "install-receipt.json";
2
+ /**
3
+ * Written by the official installer, in vlt's config dir or next to
4
+ * (or one dir above) the `vlt` it installed. vlt only updates
5
+ * `version` and `installedAt`.
6
+ */
7
+ export type ReceiptData = {
8
+ receiptVersion: 1;
9
+ /** 'npm' | 'standalone', anything else is a newer installer */
10
+ method: string;
11
+ /** absolute dir holding the entry points */
12
+ binDir: string;
13
+ installer?: {
14
+ name?: string;
15
+ version?: string;
16
+ };
17
+ version?: string;
18
+ /** dist-tag or version asked for */
19
+ channel?: string;
20
+ /** `<os>-<arch>[-<libc>]` of a standalone build */
21
+ target?: string;
22
+ binaries?: string[];
23
+ /** rc files the installer edited */
24
+ modifiedPath?: string[];
25
+ installedAt?: string;
26
+ };
27
+ export type InstallReceipt = {
28
+ file: string;
29
+ data: ReceiptData;
30
+ };
31
+ /**
32
+ * The receipt covering the `vlt` found at `found`: valid, and its
33
+ * `binDir` is the dir of `found`.
34
+ */
35
+ export declare const readInstallReceipt: (found: string, configDir?: string) => Promise<InstallReceipt | undefined>;
36
+ /** set `version` and `installedAt`, keeping all other fields */
37
+ export declare const writeInstallReceipt: (file: string, update: {
38
+ version: string;
39
+ installedAt?: string;
40
+ }) => Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { isObject } from '@vltpkg/types';
3
+ import { XDG } from '@vltpkg/xdg';
4
+ import { readFile, realpath } from 'node:fs/promises';
5
+ import { dirname, isAbsolute, resolve } from 'node:path';
6
+ import { samePath } from "./global-project.js";
7
+ import { writeJsonAtomic } from "./global-state.js";
8
+ const xdg = new XDG('vlt');
9
+ export const receiptName = 'install-receipt.json';
10
+ const isStr = (v) => typeof v === 'string';
11
+ const optStr = (v) => v === undefined || isStr(v);
12
+ const optStrs = (v) => v === undefined || (Array.isArray(v) && v.every(isStr));
13
+ const isReceipt = (d) => isObject(d) &&
14
+ d.receiptVersion === 1 &&
15
+ isStr(d.method) &&
16
+ !!d.method &&
17
+ isStr(d.binDir) &&
18
+ isAbsolute(d.binDir) &&
19
+ (d.installer === undefined ||
20
+ (isObject(d.installer) &&
21
+ optStr(d.installer.name) &&
22
+ optStr(d.installer.version))) &&
23
+ optStr(d.version) &&
24
+ optStr(d.channel) &&
25
+ optStr(d.target) &&
26
+ optStrs(d.binaries) &&
27
+ optStrs(d.modifiedPath) &&
28
+ optStr(d.installedAt);
29
+ /**
30
+ * The receipt covering the `vlt` found at `found`: valid, and its
31
+ * `binDir` is the dir of `found`.
32
+ */
33
+ export const readInstallReceipt = async (found, configDir = xdg.config()) => {
34
+ const dir = dirname(found);
35
+ const real = await realpath(dir).catch(() => undefined);
36
+ if (!real)
37
+ return;
38
+ for (const file of [
39
+ resolve(configDir, receiptName),
40
+ resolve(dir, receiptName),
41
+ resolve(dir, '..', receiptName),
42
+ ]) {
43
+ try {
44
+ const data = JSON.parse(await readFile(file, 'utf8'));
45
+ if (isReceipt(data) &&
46
+ samePath(await realpath(data.binDir), real))
47
+ return { file, data };
48
+ }
49
+ catch { }
50
+ }
51
+ };
52
+ /** set `version` and `installedAt`, keeping all other fields */
53
+ export const writeInstallReceipt = async (file, update) => {
54
+ let raw;
55
+ try {
56
+ raw = JSON.parse(await readFile(file, 'utf8'));
57
+ }
58
+ catch (cause) {
59
+ throw error('Invalid install receipt', { path: file, cause });
60
+ }
61
+ if (!isReceipt(raw)) {
62
+ throw error('Invalid install receipt', { path: file });
63
+ }
64
+ await writeJsonAtomic(file, {
65
+ ...raw,
66
+ version: update.version,
67
+ installedAt: update.installedAt ?? new Date().toISOString(),
68
+ });
69
+ };
package/dist/output.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { error } from '@vltpkg/error-cause';
1
2
  import { formatWithOptions, styleText as utilStyleText, } from 'node:util';
2
3
  import { createSupportsColor } from 'supports-color';
3
4
  import { defaultView } from "./config/definition.js";
5
+ import { globalCommands } from "./global-project.js";
6
+ import { sweepPendingRemove } from "./global-state.js";
4
7
  import { printErr, formatOptions } from "./print-err.js";
5
8
  import { hasConfiguredRegistry, hasNpmRegistry, missingNpmRegistryError, missingRegistryError, } from "./require-registry.js";
6
9
  import { isLazyView, isViewClass, loadLazyView } from "./view.js";
@@ -158,6 +161,14 @@ export const outputCommand = async (cliCommand, conf, { start, vltVersion } = {
158
161
  const telemetryEnabled = conf.values.telemetry;
159
162
  const commandName = conf.command;
160
163
  try {
164
+ // explicit only: `global` in the user config is ignored here
165
+ if (conf.explicit.global && !conf.globalRoot) {
166
+ throw error(`\`vlt ${conf.command}\` does not support --global`, {
167
+ code: 'EUSAGE',
168
+ found: conf.command,
169
+ validOptions: [...globalCommands],
170
+ });
171
+ }
161
172
  // checked here rather than in run() so that the missing-registry
162
173
  // error is rendered, tracked and exited like any other command
163
174
  // error, and so that the `--help` return above still works when
@@ -168,6 +179,8 @@ export const outputCommand = async (cliCommand, conf, { start, vltVersion } = {
168
179
  if (needsNpmRegistry && !hasNpmRegistry(conf)) {
169
180
  throw missingNpmRegistryError(conf.options['default-registry-alias']);
170
181
  }
182
+ if (conf.globalRoot)
183
+ await sweepPendingRemove(conf.globalRoot);
171
184
  const output = await onDone(await command(conf));
172
185
  stopRequestLog();
173
186
  const duration_ms = Date.now() - start;
@@ -1,4 +1,6 @@
1
1
  import type { NormalizedManifest } from '@vltpkg/types';
2
+ import type { Stats } from 'node:fs';
3
+ import type { ReadEntry } from 'tar';
2
4
  import type { LoadedConfig } from './config/index.ts';
3
5
  export type PackTarballResult = {
4
6
  name: string;
@@ -27,7 +29,7 @@ export type PackTarballResult = {
27
29
  * .npmrc, vlt.json, etc.) are excluded and always-included files (package.json,
28
30
  * README, LICENSE, etc.) are included.
29
31
  */
30
- export declare const buildPackFilter: (manifest: NormalizedManifest, packDir: string) => ((path: string) => boolean);
32
+ export declare const buildPackFilter: (manifest: NormalizedManifest, packDir: string) => ((path: string, entry: Stats | ReadEntry) => boolean);
31
33
  /**
32
34
  * Create a tarball from a package directory
33
35
  * @param {NormalizedManifest} manifest - The manifest of the package to pack