@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.
- package/dist/commands/build.d.ts +8 -3
- package/dist/commands/build.js +31 -3
- package/dist/commands/ci.d.ts +1 -1
- package/dist/commands/create.d.ts +2 -1
- package/dist/commands/create.js +2 -3
- package/dist/commands/exec.d.ts +2 -1
- package/dist/commands/exec.js +2 -3
- package/dist/commands/init.js +5 -2
- package/dist/commands/install/reporter.d.ts +1 -1
- package/dist/commands/install/reporter.js +45 -5
- package/dist/commands/install.d.ts +21 -3
- package/dist/commands/install.js +84 -30
- package/dist/commands/list.js +5 -1
- package/dist/commands/query.js +5 -1
- package/dist/commands/setup.d.ts +28 -2
- package/dist/commands/setup.js +185 -3
- package/dist/commands/skills.js +2 -3
- package/dist/commands/token.js +1 -1
- package/dist/commands/uninstall.d.ts +10 -3
- package/dist/commands/uninstall.js +28 -2
- package/dist/commands/update.d.ts +1 -1
- package/dist/config/definition.d.ts +32 -0
- package/dist/config/definition.js +60 -0
- package/dist/config/index.d.ts +26 -3
- package/dist/config/index.js +149 -7
- package/dist/global-bin-dir.d.ts +37 -0
- package/dist/global-bin-dir.js +138 -0
- package/dist/global-project.d.ts +18 -0
- package/dist/global-project.js +67 -0
- package/dist/global-state.d.ts +24 -0
- package/dist/global-state.js +94 -0
- package/dist/global.d.ts +78 -0
- package/dist/global.js +409 -0
- package/dist/index.js +7 -2
- package/dist/install-receipt.d.ts +40 -0
- package/dist/install-receipt.js +69 -0
- package/dist/output.js +13 -0
- package/dist/pack-tarball.d.ts +3 -1
- package/dist/pack-tarball.js +29 -19
- package/dist/parse-add-remove-args.js +2 -14
- package/dist/query-auth.d.ts +3 -2
- package/dist/query-auth.js +6 -4
- package/dist/reload-config.js +2 -1
- package/dist/require-registry.d.ts +7 -0
- package/dist/require-registry.js +13 -1
- package/dist/self-update.d.ts +74 -0
- package/dist/self-update.js +401 -0
- package/dist/shell-setup/env.d.ts +9 -0
- package/dist/shell-setup/env.js +25 -0
- package/dist/shell-setup/index.d.ts +69 -0
- package/dist/shell-setup/index.js +213 -0
- package/dist/shell-setup/posix.d.ts +36 -0
- package/dist/shell-setup/posix.js +158 -0
- package/dist/shell-setup/windows.d.ts +39 -0
- package/dist/shell-setup/windows.js +151 -0
- package/dist/shell.d.ts +18 -0
- package/dist/shell.js +110 -0
- package/dist/telemetry.js +8 -1
- package/package.json +30 -28
package/dist/global.d.ts
ADDED
|
@@ -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
|
-
|
|
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;
|
package/dist/pack-tarball.d.ts
CHANGED
|
@@ -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
|