@vltpkg/cli-sdk 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/commands/build.d.ts +8 -3
  2. package/dist/commands/build.js +31 -3
  3. package/dist/commands/ci.d.ts +1 -1
  4. package/dist/commands/create.d.ts +2 -1
  5. package/dist/commands/create.js +2 -3
  6. package/dist/commands/exec.d.ts +2 -1
  7. package/dist/commands/exec.js +2 -3
  8. package/dist/commands/init.js +5 -2
  9. package/dist/commands/install/reporter.d.ts +1 -1
  10. package/dist/commands/install/reporter.js +45 -5
  11. package/dist/commands/install.d.ts +21 -3
  12. package/dist/commands/install.js +84 -30
  13. package/dist/commands/list.js +5 -1
  14. package/dist/commands/query.js +5 -1
  15. package/dist/commands/setup.d.ts +28 -2
  16. package/dist/commands/setup.js +185 -3
  17. package/dist/commands/skills.js +2 -3
  18. package/dist/commands/token.js +1 -1
  19. package/dist/commands/uninstall.d.ts +10 -3
  20. package/dist/commands/uninstall.js +28 -2
  21. package/dist/commands/update.d.ts +1 -1
  22. package/dist/config/definition.d.ts +32 -0
  23. package/dist/config/definition.js +60 -0
  24. package/dist/config/index.d.ts +26 -3
  25. package/dist/config/index.js +149 -7
  26. package/dist/global-bin-dir.d.ts +37 -0
  27. package/dist/global-bin-dir.js +138 -0
  28. package/dist/global-project.d.ts +18 -0
  29. package/dist/global-project.js +67 -0
  30. package/dist/global-state.d.ts +24 -0
  31. package/dist/global-state.js +94 -0
  32. package/dist/global.d.ts +78 -0
  33. package/dist/global.js +409 -0
  34. package/dist/index.js +7 -2
  35. package/dist/install-receipt.d.ts +40 -0
  36. package/dist/install-receipt.js +69 -0
  37. package/dist/output.js +13 -0
  38. package/dist/pack-tarball.d.ts +3 -1
  39. package/dist/pack-tarball.js +29 -19
  40. package/dist/parse-add-remove-args.js +2 -14
  41. package/dist/query-auth.d.ts +3 -2
  42. package/dist/query-auth.js +6 -4
  43. package/dist/reload-config.js +2 -1
  44. package/dist/require-registry.d.ts +7 -0
  45. package/dist/require-registry.js +13 -1
  46. package/dist/self-update.d.ts +74 -0
  47. package/dist/self-update.js +401 -0
  48. package/dist/shell-setup/env.d.ts +9 -0
  49. package/dist/shell-setup/env.js +25 -0
  50. package/dist/shell-setup/index.d.ts +69 -0
  51. package/dist/shell-setup/index.js +213 -0
  52. package/dist/shell-setup/posix.d.ts +36 -0
  53. package/dist/shell-setup/posix.js +158 -0
  54. package/dist/shell-setup/windows.d.ts +39 -0
  55. package/dist/shell-setup/windows.js +151 -0
  56. package/dist/shell.d.ts +18 -0
  57. package/dist/shell.js +110 -0
  58. package/dist/telemetry.js +8 -1
  59. package/package.json +30 -28
@@ -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 { dirname } from 'node:path';
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
+ };