@vltpkg/cli-sdk 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/allow-skills.d.ts +36 -0
- package/dist/allow-skills.js +49 -0
- package/dist/commands/build.d.ts +15 -4
- package/dist/commands/build.js +50 -12
- package/dist/commands/ci.d.ts +2 -2
- package/dist/commands/ci.js +10 -2
- 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 +4 -3
- package/dist/commands/install/reporter.d.ts +1 -1
- package/dist/commands/install/reporter.js +91 -5
- package/dist/commands/install.d.ts +50 -4
- package/dist/commands/install.js +107 -25
- package/dist/commands/list.js +8 -2
- package/dist/commands/publish.d.ts +0 -1
- package/dist/commands/publish.js +27 -16
- package/dist/commands/query.js +8 -2
- package/dist/commands/setup.d.ts +28 -2
- package/dist/commands/setup.js +185 -3
- package/dist/commands/skills.d.ts +56 -0
- package/dist/commands/skills.js +267 -0
- package/dist/commands/token.js +1 -1
- package/dist/commands/uninstall.d.ts +15 -4
- package/dist/commands/uninstall.js +32 -4
- package/dist/commands/unpublish.d.ts +0 -1
- package/dist/commands/unpublish.js +17 -4
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +18 -1
- package/dist/config/definition.d.ts +52 -0
- package/dist/config/definition.js +109 -1
- package/dist/config/index.d.ts +26 -3
- package/dist/config/index.js +149 -7
- package/dist/custom-help.js +4 -0
- 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 +14 -3
- package/dist/install-receipt.d.ts +40 -0
- package/dist/install-receipt.js +69 -0
- package/dist/load-command.d.ts +2 -0
- package/dist/migrate-workspaces.d.ts +16 -0
- package/dist/migrate-workspaces.js +44 -0
- package/dist/output.js +13 -0
- package/dist/parse-add-remove-args.js +2 -14
- package/dist/print-err.js +4 -1
- package/dist/publish-config.d.ts +9 -0
- package/dist/publish-config.js +8 -0
- 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/skills-prompt.d.ts +28 -0
- package/dist/skills-prompt.js +78 -0
- package/package.json +30 -28
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { error } from '@vltpkg/error-cause';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
/** commands that support `--global` */
|
|
5
|
+
export const globalCommands = new Set([
|
|
6
|
+
'install',
|
|
7
|
+
'uninstall',
|
|
8
|
+
'list',
|
|
9
|
+
'query',
|
|
10
|
+
'build',
|
|
11
|
+
]);
|
|
12
|
+
/** folder holding one workspace per global package */
|
|
13
|
+
export const globalWorkspacesDir = 'packages';
|
|
14
|
+
/** fallback dir for bins of global packages */
|
|
15
|
+
export const globalBinDir = (root) => resolve(root, 'bin');
|
|
16
|
+
/** same path, case-insensitive on win32 */
|
|
17
|
+
export const samePath = (a, b) => {
|
|
18
|
+
const norm = (p) => {
|
|
19
|
+
const r = resolve(p);
|
|
20
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
21
|
+
};
|
|
22
|
+
return norm(a) === norm(b);
|
|
23
|
+
};
|
|
24
|
+
/** path is strictly inside dir */
|
|
25
|
+
export const isInside = (dir, path) => {
|
|
26
|
+
const rel = relative(dir, path);
|
|
27
|
+
return !!rel && !isAbsolute(rel) && rel.split(sep)[0] !== '..';
|
|
28
|
+
};
|
|
29
|
+
const validName = /^(?:@[a-z0-9][\w.~-]*\/)?[a-z0-9~][\w.~-]*$/i;
|
|
30
|
+
/** throws `EUSAGE` unless an unscoped or `@scope/name` pkg name */
|
|
31
|
+
export const assertGlobalName = (name) => {
|
|
32
|
+
if (!validName.test(name)) {
|
|
33
|
+
throw error('Invalid package name', {
|
|
34
|
+
code: 'EUSAGE',
|
|
35
|
+
found: name,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** `eslint` -> `eslint-ws` */
|
|
40
|
+
export const globalWorkspaceName = (name) => {
|
|
41
|
+
assertGlobalName(name);
|
|
42
|
+
return `${name}-ws`;
|
|
43
|
+
};
|
|
44
|
+
/** `eslint` -> `packages/eslint-ws`, `@s/p` -> `packages/@s+p-ws` */
|
|
45
|
+
export const globalWorkspacePath = (name) => `${globalWorkspacesDir}/${globalWorkspaceName(name).replace('/', '+')}`;
|
|
46
|
+
const writeIfMissing = (file, data) => {
|
|
47
|
+
try {
|
|
48
|
+
writeFileSync(file, JSON.stringify(data, null, 2) + '\n', {
|
|
49
|
+
flag: 'wx',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (er) {
|
|
53
|
+
if (er.code !== 'EEXIST')
|
|
54
|
+
throw er;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/** create the global project skeleton, keeping existing files */
|
|
58
|
+
export const ensureGlobalProject = (root) => {
|
|
59
|
+
mkdirSync(resolve(root, globalWorkspacesDir), { recursive: true });
|
|
60
|
+
writeIfMissing(resolve(root, 'package.json'), {
|
|
61
|
+
name: 'vlt-global',
|
|
62
|
+
private: true,
|
|
63
|
+
});
|
|
64
|
+
writeIfMissing(resolve(root, 'vlt.json'), {
|
|
65
|
+
workspaces: `${globalWorkspacesDir}/*`,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** state of the global project that is not config */
|
|
2
|
+
export type GlobalState = {
|
|
3
|
+
/** canonical dirs global bins were linked into */
|
|
4
|
+
binDirs?: string[];
|
|
5
|
+
/** store dirs of a replaced running vlt, for the next -g command */
|
|
6
|
+
pendingRemove?: string[];
|
|
7
|
+
/** the ~/.local/bin notice was shown */
|
|
8
|
+
localBinNoticed?: boolean;
|
|
9
|
+
/** 'declined': the install -g PATH offer was answered no */
|
|
10
|
+
pathSetup?: 'declined';
|
|
11
|
+
/** keys of other features, kept as-is */
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare const globalStateFile: (root: string) => string;
|
|
15
|
+
/** missing or malformed reads as `{}` */
|
|
16
|
+
export declare const readGlobalState: (root: string) => Promise<GlobalState>;
|
|
17
|
+
/** write `data` via temp file + rename, with `mode` if set */
|
|
18
|
+
export declare const writeFileAtomic: (file: string, data: string | Uint8Array, mode?: number) => Promise<void>;
|
|
19
|
+
/** write `data` as JSON via temp file + rename */
|
|
20
|
+
export declare const writeJsonAtomic: (file: string, data: unknown) => Promise<void>;
|
|
21
|
+
/** read, change and write the state file */
|
|
22
|
+
export declare const updateGlobalState: (root: string, fn: (state: GlobalState) => GlobalState) => Promise<GlobalState>;
|
|
23
|
+
/** rm dirs a running vlt could not remove from under itself */
|
|
24
|
+
export declare const sweepPendingRemove: (root: string) => Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isDepID } from '@vltpkg/dep-id';
|
|
2
|
+
import { isObject } from '@vltpkg/types';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { chmod, readFile, realpath, rename, rm, writeFile, } from 'node:fs/promises';
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
6
|
+
import { isInside, samePath } from "./global-project.js";
|
|
7
|
+
export const globalStateFile = (root) => resolve(root, 'global-state.json');
|
|
8
|
+
/** missing or malformed reads as `{}` */
|
|
9
|
+
export const readGlobalState = async (root) => {
|
|
10
|
+
let data;
|
|
11
|
+
try {
|
|
12
|
+
data = JSON.parse(await readFile(globalStateFile(root), 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
if (!isObject(data))
|
|
18
|
+
return {};
|
|
19
|
+
const state = { ...data };
|
|
20
|
+
const { binDirs, localBinNoticed } = state;
|
|
21
|
+
if (Array.isArray(binDirs)) {
|
|
22
|
+
state.binDirs = binDirs.filter((d) => typeof d === 'string' && isAbsolute(d));
|
|
23
|
+
}
|
|
24
|
+
else
|
|
25
|
+
delete state.binDirs;
|
|
26
|
+
if (Array.isArray(state.pendingRemove)) {
|
|
27
|
+
state.pendingRemove = state.pendingRemove.filter((d) => typeof d === 'string' && isAbsolute(d));
|
|
28
|
+
}
|
|
29
|
+
else
|
|
30
|
+
delete state.pendingRemove;
|
|
31
|
+
if (typeof localBinNoticed !== 'boolean')
|
|
32
|
+
delete state.localBinNoticed;
|
|
33
|
+
if (state.pathSetup !== 'declined')
|
|
34
|
+
delete state.pathSetup;
|
|
35
|
+
return state;
|
|
36
|
+
};
|
|
37
|
+
/** write `data` via temp file + rename, with `mode` if set */
|
|
38
|
+
export const writeFileAtomic = async (file, data, mode) => {
|
|
39
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
await writeFile(tmp, data, {
|
|
42
|
+
flag: 'wx',
|
|
43
|
+
...(mode === undefined ? {} : { mode }),
|
|
44
|
+
});
|
|
45
|
+
// umask-proof
|
|
46
|
+
if (mode !== undefined)
|
|
47
|
+
await chmod(tmp, mode);
|
|
48
|
+
await rename(tmp, file);
|
|
49
|
+
}
|
|
50
|
+
catch (er) {
|
|
51
|
+
await rm(tmp, { force: true });
|
|
52
|
+
throw er;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** write `data` as JSON via temp file + rename */
|
|
56
|
+
export const writeJsonAtomic = (file, data) => writeFileAtomic(file, JSON.stringify(data, null, 2) + '\n');
|
|
57
|
+
/** read, change and write the state file */
|
|
58
|
+
export const updateGlobalState = async (root, fn) => {
|
|
59
|
+
const state = fn(await readGlobalState(root));
|
|
60
|
+
await writeJsonAtomic(globalStateFile(root), state);
|
|
61
|
+
return state;
|
|
62
|
+
};
|
|
63
|
+
/** rm dirs a running vlt could not remove from under itself */
|
|
64
|
+
export const sweepPendingRemove = async (root) => {
|
|
65
|
+
try {
|
|
66
|
+
const { pendingRemove = [] } = await readGlobalState(root);
|
|
67
|
+
if (!pendingRemove.length)
|
|
68
|
+
return;
|
|
69
|
+
const store = resolve(await realpath(root), 'node_modules/.vlt');
|
|
70
|
+
const script = process.argv[1];
|
|
71
|
+
const running = script && (await realpath(script).catch(() => undefined));
|
|
72
|
+
const left = [];
|
|
73
|
+
for (const entry of pendingRemove) {
|
|
74
|
+
// only store dirs, whatever the file says
|
|
75
|
+
if (!samePath(dirname(entry), store) ||
|
|
76
|
+
!isDepID(basename(entry))) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (running && isInside(entry, running)) {
|
|
80
|
+
left.push(entry);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// win32: EBUSY while another vlt runs from it
|
|
84
|
+
await rm(entry, { recursive: true, force: true }).catch(() => left.push(entry));
|
|
85
|
+
}
|
|
86
|
+
if (left.length === pendingRemove.length)
|
|
87
|
+
return;
|
|
88
|
+
await updateGlobalState(root, s => ({
|
|
89
|
+
...s,
|
|
90
|
+
pendingRemove: left,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
};
|
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
|
+
};
|