@vltpkg/cli-sdk 1.5.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/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/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/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/package.json +30 -28
package/dist/config/index.js
CHANGED
|
@@ -28,13 +28,15 @@ import { cloneAvailable } from '@vltpkg/tar/clonefile';
|
|
|
28
28
|
import { resetCaches } from '@vltpkg/dep-id';
|
|
29
29
|
import { getOptions } from '@vltpkg/spec';
|
|
30
30
|
import { assertRecordStringString, assertRecordStringT, isObject, isRecordStringString, } from '@vltpkg/types';
|
|
31
|
-
import { find, load, reload, save } from '@vltpkg/vlt-json';
|
|
32
|
-
import { Monorepo } from '@vltpkg/workspaces';
|
|
31
|
+
import { find, load, reload, save, unload } from '@vltpkg/vlt-json';
|
|
32
|
+
import { Monorepo, workspaceCache } from '@vltpkg/workspaces';
|
|
33
33
|
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
34
|
-
import {
|
|
34
|
+
import { homedir } from 'node:os';
|
|
35
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
35
36
|
import { PathScurry } from 'path-scurry';
|
|
36
37
|
import { commands, definition, defaultValues, getCommand, isRecordField, recordFields, storeLinkers, } from "./definition.js";
|
|
37
38
|
import { merge } from "./merge.js";
|
|
39
|
+
import { ensureGlobalProject, globalCommands, } from "../global-project.js";
|
|
38
40
|
import { cloneLayer, mergeLayers } from "./merge-layers.js";
|
|
39
41
|
import { assertSpecAliases } from "./spec-aliases.js";
|
|
40
42
|
export { commands, definition, isRecordField, recordFields, };
|
|
@@ -117,6 +119,8 @@ export const recordsToPairs = (obj) => {
|
|
|
117
119
|
const kRecord = Symbol('parsed key=value record');
|
|
118
120
|
// jackspeak's env var name for a field
|
|
119
121
|
const envKey = (k) => `VLT_${k.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase()}`;
|
|
122
|
+
// `~` and `~/x` (shells leave `--opt=~/x` alone)
|
|
123
|
+
const expandHome = (p) => p.replace(/^~(?=$|[\\/])/, homedir());
|
|
120
124
|
// `parse()` writes every resolved value to `VLT_*`, so a nested vlt
|
|
121
125
|
// can't tell inherited env from env set for it. this holds the
|
|
122
126
|
// parent's explicit values and the `VLT_*` env it wrote.
|
|
@@ -171,7 +175,7 @@ export class Config {
|
|
|
171
175
|
return this.#options;
|
|
172
176
|
const scurry = new PathScurry(this.projectRoot);
|
|
173
177
|
const packageJson = new PackageJson();
|
|
174
|
-
const asRecords = pairsToRecords(this.parse().values);
|
|
178
|
+
const { path: _, ...asRecords } = pairsToRecords(this.parse().values);
|
|
175
179
|
const extras = {
|
|
176
180
|
projectRoot: this.projectRoot,
|
|
177
181
|
signal: this.#abort.signal,
|
|
@@ -214,6 +218,8 @@ export class Config {
|
|
|
214
218
|
this.projectRoot = projectRoot;
|
|
215
219
|
this.#options = undefined;
|
|
216
220
|
resetCaches();
|
|
221
|
+
// cached manifests belong to the old packageJson
|
|
222
|
+
workspaceCache.clear();
|
|
217
223
|
}
|
|
218
224
|
// memoized options() getter value
|
|
219
225
|
#options;
|
|
@@ -240,6 +246,28 @@ export class Config {
|
|
|
240
246
|
* `~/projects/xyz`, then the highest dir it will check is `~/projects`
|
|
241
247
|
*/
|
|
242
248
|
projectRoot;
|
|
249
|
+
/**
|
|
250
|
+
* Global project root, set when `--global` switched to it.
|
|
251
|
+
*/
|
|
252
|
+
globalRoot;
|
|
253
|
+
/**
|
|
254
|
+
* `--global-bin-dir` from cli, env or user config, set with
|
|
255
|
+
* `globalRoot`.
|
|
256
|
+
*/
|
|
257
|
+
globalBinDir;
|
|
258
|
+
// the global project of the `setup` wizard, not switched to
|
|
259
|
+
#setupGlobal;
|
|
260
|
+
/**
|
|
261
|
+
* The global project (`--global-dir`, `--global-bin-dir`: cli > env >
|
|
262
|
+
* user config, never a project `vlt.json`): the one switched to by
|
|
263
|
+
* `--global`, or the one `vlt setup` offers PATH setup for.
|
|
264
|
+
*/
|
|
265
|
+
globalProject() {
|
|
266
|
+
if (this.globalRoot) {
|
|
267
|
+
return { root: this.globalRoot, binDir: this.globalBinDir };
|
|
268
|
+
}
|
|
269
|
+
return this.#setupGlobal;
|
|
270
|
+
}
|
|
243
271
|
/**
|
|
244
272
|
* `Record<alias, canonical name>` to dereference command aliases.
|
|
245
273
|
*/
|
|
@@ -268,6 +296,12 @@ export class Config {
|
|
|
268
296
|
!storeLinkers.includes(badLinker);
|
|
269
297
|
if (fixLinker)
|
|
270
298
|
process.env.VLT_STORE_LINKER = 'unpack';
|
|
299
|
+
// an opt-out of dotfile edits: any value but 0 (or empty) is set
|
|
300
|
+
const noModifyPath = process.env.VLT_NO_MODIFY_PATH;
|
|
301
|
+
if (noModifyPath !== undefined) {
|
|
302
|
+
process.env.VLT_NO_MODIFY_PATH =
|
|
303
|
+
noModifyPath === '' || noModifyPath === '0' ? '0' : '1';
|
|
304
|
+
}
|
|
271
305
|
const envKeys = Object.keys(defaultValues).filter(k => process.env[envKey(k)] !== undefined);
|
|
272
306
|
const parent = parentEnv();
|
|
273
307
|
// file layer values, before env replaces them
|
|
@@ -333,7 +367,27 @@ export class Config {
|
|
|
333
367
|
if (cmdOrFallback !== 'config') {
|
|
334
368
|
assertSpecAliases(pairsToRecords(p.values));
|
|
335
369
|
}
|
|
370
|
+
// a project vlt.json global-bin-dir must not reach a nested vlt -g
|
|
371
|
+
const binDirKey = envKey('global-bin-dir');
|
|
372
|
+
const keepBinDir = !this.globalRoot && !cliKeys.includes('global-bin-dir');
|
|
373
|
+
const binDirEnv = process.env[binDirKey];
|
|
336
374
|
this.jack.writeEnv(p);
|
|
375
|
+
// scripts and nested vlt act on their own project
|
|
376
|
+
delete process.env[envKey('global')];
|
|
377
|
+
// but use the same global dir, even from another cwd
|
|
378
|
+
if (this.globalRoot) {
|
|
379
|
+
process.env[envKey('global-dir')] = this.globalRoot;
|
|
380
|
+
if (this.globalBinDir)
|
|
381
|
+
process.env[binDirKey] = this.globalBinDir;
|
|
382
|
+
else
|
|
383
|
+
delete process.env[binDirKey];
|
|
384
|
+
}
|
|
385
|
+
else if (keepBinDir) {
|
|
386
|
+
if (binDirEnv === undefined)
|
|
387
|
+
delete process.env[binDirKey];
|
|
388
|
+
else
|
|
389
|
+
process.env[binDirKey] = binDirEnv;
|
|
390
|
+
}
|
|
337
391
|
process.env[kParentEnv] = JSON.stringify({
|
|
338
392
|
explicit,
|
|
339
393
|
env: Object.fromEntries(Object.keys(defaultValues).flatMap(k => {
|
|
@@ -641,13 +695,99 @@ export class Config {
|
|
|
641
695
|
/**
|
|
642
696
|
* Find the local config file and load both it and the user-level config in
|
|
643
697
|
* the XDG config home.
|
|
698
|
+
*
|
|
699
|
+
* When `args` select a `--global` command, the global project is used
|
|
700
|
+
* instead of the current one.
|
|
644
701
|
*/
|
|
645
|
-
async loadConfigFile() {
|
|
702
|
+
async loadConfigFile(args) {
|
|
646
703
|
await this.#maybeLoadConfigFile('user');
|
|
704
|
+
const g = args && this.#globalProject(args);
|
|
705
|
+
if (g?.switch) {
|
|
706
|
+
const { root } = g;
|
|
707
|
+
try {
|
|
708
|
+
ensureGlobalProject(root);
|
|
709
|
+
}
|
|
710
|
+
catch (cause) {
|
|
711
|
+
throw error('Could not create the global project', {
|
|
712
|
+
code: 'ECONFIG',
|
|
713
|
+
found: root,
|
|
714
|
+
cause,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
this.globalBinDir = g.binDir;
|
|
718
|
+
unload('project');
|
|
719
|
+
this.globalRoot = this.projectRoot = root;
|
|
720
|
+
}
|
|
721
|
+
else if (g) {
|
|
722
|
+
this.#setupGlobal = { root: g.root, binDir: g.binDir };
|
|
723
|
+
}
|
|
647
724
|
this.projectRoot = dirname(find('project', this.projectRoot));
|
|
648
725
|
await this.#maybeLoadConfigFile('project');
|
|
649
726
|
return this;
|
|
650
727
|
}
|
|
728
|
+
// global project if `args` run a global command with --global, or
|
|
729
|
+
// `setup`. cli > env > user config, project config is not loaded yet.
|
|
730
|
+
// `switch`: use it as the project (not for the setup wizard)
|
|
731
|
+
#globalProject(args) {
|
|
732
|
+
const { values, positionals } = this.jack.parseRaw(args);
|
|
733
|
+
const cmd = getCommand(positionals[0]);
|
|
734
|
+
if (values.help || !cmd)
|
|
735
|
+
return;
|
|
736
|
+
// `setup --path` acts on the global project, the wizard offers to
|
|
737
|
+
const setup = cmd === 'setup';
|
|
738
|
+
if (!setup && !globalCommands.has(cmd))
|
|
739
|
+
return;
|
|
740
|
+
if (!setup) {
|
|
741
|
+
const env = process.env[envKey('global')];
|
|
742
|
+
const global = values.global ??
|
|
743
|
+
(env !== undefined ?
|
|
744
|
+
env === '1'
|
|
745
|
+
: this.#defaults().global === true);
|
|
746
|
+
if (!global)
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
root: this.#globalDir(values['global-dir']),
|
|
751
|
+
binDir: this.#globalBinDir(args),
|
|
752
|
+
switch: !setup || values.path === true,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
#globalDir(cli) {
|
|
756
|
+
// empty means unset, never the cwd
|
|
757
|
+
const dir = cli || process.env[envKey('global-dir')];
|
|
758
|
+
if (dir)
|
|
759
|
+
return resolve(expandHome(dir));
|
|
760
|
+
const user = expandHome(String(this.#defaults()['global-dir'] || defaultValues['global-dir']));
|
|
761
|
+
// would differ per cwd
|
|
762
|
+
if (!isAbsolute(user)) {
|
|
763
|
+
throw error('global-dir in the user config must be absolute', {
|
|
764
|
+
code: 'ECONFIG',
|
|
765
|
+
found: user,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
// normalize separators (`~/g` on win32)
|
|
769
|
+
return resolve(user);
|
|
770
|
+
}
|
|
771
|
+
// cli > env > user config, like `#globalProject`
|
|
772
|
+
#globalBinDir(args) {
|
|
773
|
+
const { values } = this.jack.parseRaw(args);
|
|
774
|
+
// empty means unset
|
|
775
|
+
const dir = values['global-bin-dir'] ||
|
|
776
|
+
process.env[envKey('global-bin-dir')];
|
|
777
|
+
if (dir)
|
|
778
|
+
return resolve(expandHome(dir));
|
|
779
|
+
const user = this.#defaults()['global-bin-dir'];
|
|
780
|
+
if (typeof user !== 'string' || !user)
|
|
781
|
+
return;
|
|
782
|
+
const p = expandHome(user);
|
|
783
|
+
if (!isAbsolute(p)) {
|
|
784
|
+
throw error('global-bin-dir in the user config must be absolute', {
|
|
785
|
+
code: 'ECONFIG',
|
|
786
|
+
found: p,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return resolve(p);
|
|
790
|
+
}
|
|
651
791
|
/**
|
|
652
792
|
* Clear cached config values to force re-reading from updated files.
|
|
653
793
|
* @internal
|
|
@@ -694,11 +834,13 @@ export class Config {
|
|
|
694
834
|
* only used in tests, resets the memoization
|
|
695
835
|
* @internal
|
|
696
836
|
*/
|
|
697
|
-
reload = false
|
|
837
|
+
reload = false,
|
|
838
|
+
/** switch to the global project on `--global` */
|
|
839
|
+
allowGlobal = true) {
|
|
698
840
|
if (this.#loaded && !reload)
|
|
699
841
|
return this.#loaded;
|
|
700
842
|
const a = new Config(definition, projectRoot);
|
|
701
|
-
const b = await a.loadConfigFile();
|
|
843
|
+
const b = await a.loadConfigFile(allowGlobal ? argv : undefined);
|
|
702
844
|
this.#loaded = b.parse(argv);
|
|
703
845
|
return this.#loaded;
|
|
704
846
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { LoadedConfig } from './config/index.ts';
|
|
2
|
+
import type { InstallReceipt } from './install-receipt.ts';
|
|
3
|
+
export type GlobalBinDirSource = 'config' | 'self' | 'local-bin' | 'global-dir';
|
|
4
|
+
export type ResolvedGlobalBinDir = {
|
|
5
|
+
/** canonical (native realpath) */
|
|
6
|
+
dir: string;
|
|
7
|
+
source: GlobalBinDirSource;
|
|
8
|
+
inPath: boolean;
|
|
9
|
+
/** self dir passed over because it is not writable */
|
|
10
|
+
notWritable?: string;
|
|
11
|
+
};
|
|
12
|
+
/** the `vlt` on PATH, when it is the running program */
|
|
13
|
+
export type Self = {
|
|
14
|
+
/** what `which` found: `<dir>/vlt`, `vlt.cmd`, `vlt.exe` */
|
|
15
|
+
found: string;
|
|
16
|
+
/** canonical dirname of `found` */
|
|
17
|
+
dir: string;
|
|
18
|
+
/** realpath of what `found` runs */
|
|
19
|
+
target: string;
|
|
20
|
+
/** `found` is `process.execPath`: a single executable */
|
|
21
|
+
standalone: boolean;
|
|
22
|
+
receipt?: InstallReceipt;
|
|
23
|
+
};
|
|
24
|
+
export type ResolveOptions = {
|
|
25
|
+
/** skip the self dir, eg after a write there failed */
|
|
26
|
+
selfUnwritable?: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** `isInPath`, also matching symlinked entries */
|
|
29
|
+
export declare const isInPathRealpath: (dir: string, PATH?: string) => Promise<boolean>;
|
|
30
|
+
/** the `vlt` found in the `PATH`, if it runs this process */
|
|
31
|
+
export declare const findSelf: () => Promise<Self | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Where to link global bins: `--global-bin-dir`, else the dir of the
|
|
34
|
+
* running `vlt` when writable, else `~/.local/bin` when in the `PATH`
|
|
35
|
+
* (not win32), else `<global-dir>/bin`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const resolveGlobalBinDir: (conf: Pick<LoadedConfig, "projectRoot" | "globalBinDir">, { selfUnwritable }?: ResolveOptions) => Promise<ResolvedGlobalBinDir>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { findCmdShimIfExists } from '@vltpkg/cmd-shim';
|
|
2
|
+
import { error } from '@vltpkg/error-cause';
|
|
3
|
+
import { which } from '@vltpkg/which';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { access, mkdir, realpath, stat } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { basename, delimiter, dirname, isAbsolute, resolve, } from 'node:path';
|
|
8
|
+
import { isInPath } from "./global.js";
|
|
9
|
+
import { globalBinDir, samePath } from "./global-project.js";
|
|
10
|
+
import { readInstallReceipt } from "./install-receipt.js";
|
|
11
|
+
// native realpath: also fixes case on case-insensitive fs
|
|
12
|
+
const real = (p) => realpath(p).catch(() => undefined);
|
|
13
|
+
const writable = (dir) => access(dir, constants.W_OK).then(() => true, () => false);
|
|
14
|
+
/** `isInPath`, also matching symlinked entries */
|
|
15
|
+
export const isInPathRealpath = async (dir, PATH = process.env.PATH ?? '') => {
|
|
16
|
+
if (isInPath(dir, PATH))
|
|
17
|
+
return true;
|
|
18
|
+
const r = await real(dir);
|
|
19
|
+
if (!r)
|
|
20
|
+
return false;
|
|
21
|
+
for (const p of PATH.split(delimiter)) {
|
|
22
|
+
const rp = p && (await real(p));
|
|
23
|
+
if (rp && samePath(rp, r))
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
// npx, vlx, run scripts put a project's `.bin` first
|
|
29
|
+
const isDotBin = (d) => {
|
|
30
|
+
const name = (p) => process.platform === 'win32' ?
|
|
31
|
+
basename(p).toLowerCase()
|
|
32
|
+
: basename(p);
|
|
33
|
+
return name(d) === '.bin' && name(dirname(d)) === 'node_modules';
|
|
34
|
+
};
|
|
35
|
+
/** the `vlt` found in the `PATH`, if it runs this process */
|
|
36
|
+
export const findSelf = async () => {
|
|
37
|
+
const found = await which('vlt', { nothrow: true }).catch(() => null);
|
|
38
|
+
if (!found || !isAbsolute(found))
|
|
39
|
+
return;
|
|
40
|
+
// win32 looks in the cwd first
|
|
41
|
+
const foundDir = dirname(found);
|
|
42
|
+
if (!isInPath(foundDir) || isDotBin(foundDir))
|
|
43
|
+
return;
|
|
44
|
+
const dir = await real(foundDir);
|
|
45
|
+
if (!dir || isDotBin(dir))
|
|
46
|
+
return;
|
|
47
|
+
const shim = /\.(cmd|ps1)$/i.test(found) ?
|
|
48
|
+
(await findCmdShimIfExists(found))?.[1]
|
|
49
|
+
: found;
|
|
50
|
+
const target = shim && (await real(shim));
|
|
51
|
+
if (!target)
|
|
52
|
+
return;
|
|
53
|
+
const exec = await real(process.execPath);
|
|
54
|
+
const standalone = !!exec && samePath(target, exec);
|
|
55
|
+
if (!standalone) {
|
|
56
|
+
const script = process.argv[1] && (await real(process.argv[1]));
|
|
57
|
+
if (!script || !samePath(target, script))
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const receipt = await readInstallReceipt(found);
|
|
61
|
+
return {
|
|
62
|
+
found,
|
|
63
|
+
dir,
|
|
64
|
+
target,
|
|
65
|
+
standalone,
|
|
66
|
+
...(receipt ? { receipt } : {}),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const resolveDir = async (conf, selfUnwritable) => {
|
|
70
|
+
const configured = conf.globalBinDir;
|
|
71
|
+
if (configured) {
|
|
72
|
+
try {
|
|
73
|
+
await mkdir(configured, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
throw error('Could not create global-bin-dir', {
|
|
77
|
+
code: 'ECONFIG',
|
|
78
|
+
found: configured,
|
|
79
|
+
cause,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await access(configured, constants.W_OK);
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
throw error('global-bin-dir is not writable', {
|
|
87
|
+
code: 'ECONFIG',
|
|
88
|
+
found: configured,
|
|
89
|
+
cause,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const dir = await realpath(configured);
|
|
93
|
+
return {
|
|
94
|
+
dir,
|
|
95
|
+
source: 'config',
|
|
96
|
+
inPath: await isInPathRealpath(dir),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const self = await findSelf();
|
|
100
|
+
if (self && !selfUnwritable && (await writable(self.dir))) {
|
|
101
|
+
return { dir: self.dir, source: 'self', inPath: true };
|
|
102
|
+
}
|
|
103
|
+
const notWritable = self?.dir;
|
|
104
|
+
if (notWritable && process.platform !== 'win32') {
|
|
105
|
+
// never created: only in the PATH if it existed at login
|
|
106
|
+
const local = resolve(homedir(), '.local/bin');
|
|
107
|
+
const st = await stat(local).catch(() => undefined);
|
|
108
|
+
if (st?.isDirectory() &&
|
|
109
|
+
(await isInPathRealpath(local)) &&
|
|
110
|
+
(await writable(local))) {
|
|
111
|
+
const dir = await realpath(local);
|
|
112
|
+
// it may be the self dir that refused a write
|
|
113
|
+
if (!samePath(dir, notWritable)) {
|
|
114
|
+
return { dir, source: 'local-bin', inPath: true, notWritable };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const g = globalBinDir(conf.projectRoot);
|
|
119
|
+
await mkdir(g, { recursive: true });
|
|
120
|
+
const dir = await realpath(g);
|
|
121
|
+
return {
|
|
122
|
+
dir,
|
|
123
|
+
source: 'global-dir',
|
|
124
|
+
inPath: await isInPathRealpath(dir),
|
|
125
|
+
...(notWritable ? { notWritable } : {}),
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Where to link global bins: `--global-bin-dir`, else the dir of the
|
|
130
|
+
* running `vlt` when writable, else `~/.local/bin` when in the `PATH`
|
|
131
|
+
* (not win32), else `<global-dir>/bin`.
|
|
132
|
+
*/
|
|
133
|
+
export const resolveGlobalBinDir = async (conf, { selfUnwritable = false } = {}) => {
|
|
134
|
+
const r = await resolveDir(conf, selfUnwritable);
|
|
135
|
+
// nested vlt agrees
|
|
136
|
+
process.env.VLT_GLOBAL_BIN_DIR = r.dir;
|
|
137
|
+
return r;
|
|
138
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** commands that support `--global` */
|
|
2
|
+
export declare const globalCommands: ReadonlySet<string>;
|
|
3
|
+
/** folder holding one workspace per global package */
|
|
4
|
+
export declare const globalWorkspacesDir = "packages";
|
|
5
|
+
/** fallback dir for bins of global packages */
|
|
6
|
+
export declare const globalBinDir: (root: string) => string;
|
|
7
|
+
/** same path, case-insensitive on win32 */
|
|
8
|
+
export declare const samePath: (a: string, b: string) => boolean;
|
|
9
|
+
/** path is strictly inside dir */
|
|
10
|
+
export declare const isInside: (dir: string, path: string) => boolean;
|
|
11
|
+
/** throws `EUSAGE` unless an unscoped or `@scope/name` pkg name */
|
|
12
|
+
export declare const assertGlobalName: (name: string) => void;
|
|
13
|
+
/** `eslint` -> `eslint-ws` */
|
|
14
|
+
export declare const globalWorkspaceName: (name: string) => string;
|
|
15
|
+
/** `eslint` -> `packages/eslint-ws`, `@s/p` -> `packages/@s+p-ws` */
|
|
16
|
+
export declare const globalWorkspacePath: (name: string) => string;
|
|
17
|
+
/** create the global project skeleton, keeping existing files */
|
|
18
|
+
export declare const ensureGlobalProject: (root: string) => void;
|
|
@@ -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
|
+
};
|