@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.
Files changed (74) hide show
  1. package/dist/allow-skills.d.ts +36 -0
  2. package/dist/allow-skills.js +49 -0
  3. package/dist/commands/build.d.ts +15 -4
  4. package/dist/commands/build.js +50 -12
  5. package/dist/commands/ci.d.ts +2 -2
  6. package/dist/commands/ci.js +10 -2
  7. package/dist/commands/create.d.ts +2 -1
  8. package/dist/commands/create.js +2 -3
  9. package/dist/commands/exec.d.ts +2 -1
  10. package/dist/commands/exec.js +2 -3
  11. package/dist/commands/init.js +4 -3
  12. package/dist/commands/install/reporter.d.ts +1 -1
  13. package/dist/commands/install/reporter.js +91 -5
  14. package/dist/commands/install.d.ts +50 -4
  15. package/dist/commands/install.js +107 -25
  16. package/dist/commands/list.js +8 -2
  17. package/dist/commands/publish.d.ts +0 -1
  18. package/dist/commands/publish.js +27 -16
  19. package/dist/commands/query.js +8 -2
  20. package/dist/commands/setup.d.ts +28 -2
  21. package/dist/commands/setup.js +185 -3
  22. package/dist/commands/skills.d.ts +56 -0
  23. package/dist/commands/skills.js +267 -0
  24. package/dist/commands/token.js +1 -1
  25. package/dist/commands/uninstall.d.ts +15 -4
  26. package/dist/commands/uninstall.js +32 -4
  27. package/dist/commands/unpublish.d.ts +0 -1
  28. package/dist/commands/unpublish.js +17 -4
  29. package/dist/commands/update.d.ts +13 -1
  30. package/dist/commands/update.js +18 -1
  31. package/dist/config/definition.d.ts +52 -0
  32. package/dist/config/definition.js +109 -1
  33. package/dist/config/index.d.ts +26 -3
  34. package/dist/config/index.js +149 -7
  35. package/dist/custom-help.js +4 -0
  36. package/dist/global-bin-dir.d.ts +37 -0
  37. package/dist/global-bin-dir.js +138 -0
  38. package/dist/global-project.d.ts +18 -0
  39. package/dist/global-project.js +67 -0
  40. package/dist/global-state.d.ts +24 -0
  41. package/dist/global-state.js +94 -0
  42. package/dist/global.d.ts +78 -0
  43. package/dist/global.js +409 -0
  44. package/dist/index.js +14 -3
  45. package/dist/install-receipt.d.ts +40 -0
  46. package/dist/install-receipt.js +69 -0
  47. package/dist/load-command.d.ts +2 -0
  48. package/dist/migrate-workspaces.d.ts +16 -0
  49. package/dist/migrate-workspaces.js +44 -0
  50. package/dist/output.js +13 -0
  51. package/dist/parse-add-remove-args.js +2 -14
  52. package/dist/print-err.js +4 -1
  53. package/dist/publish-config.d.ts +9 -0
  54. package/dist/publish-config.js +8 -0
  55. package/dist/query-auth.d.ts +3 -2
  56. package/dist/query-auth.js +6 -4
  57. package/dist/reload-config.js +2 -1
  58. package/dist/require-registry.d.ts +7 -0
  59. package/dist/require-registry.js +13 -1
  60. package/dist/self-update.d.ts +74 -0
  61. package/dist/self-update.js +401 -0
  62. package/dist/shell-setup/env.d.ts +9 -0
  63. package/dist/shell-setup/env.js +25 -0
  64. package/dist/shell-setup/index.d.ts +69 -0
  65. package/dist/shell-setup/index.js +213 -0
  66. package/dist/shell-setup/posix.d.ts +36 -0
  67. package/dist/shell-setup/posix.js +158 -0
  68. package/dist/shell-setup/windows.d.ts +39 -0
  69. package/dist/shell-setup/windows.js +151 -0
  70. package/dist/shell.d.ts +18 -0
  71. package/dist/shell.js +110 -0
  72. package/dist/skills-prompt.d.ts +28 -0
  73. package/dist/skills-prompt.js +78 -0
  74. package/package.json +30 -28
@@ -46,6 +46,7 @@ const canonicalCommands = {
46
46
  'run-exec': 'run-exec',
47
47
  run: 'run',
48
48
  setup: 'setup',
49
+ skills: 'skills',
49
50
  token: 'token',
50
51
  uninstall: 'uninstall',
51
52
  unpublish: 'unpublish',
@@ -473,6 +474,66 @@ export const definition = j
473
474
  description: `Specify named workspace group names to load and operate on
474
475
  when doing recursive operations on workspaces.`,
475
476
  },
477
+ })
478
+ .flag({
479
+ global: {
480
+ short: 'g',
481
+ description: `Operate on the global project instead of the current
482
+ one. Supported by \`install\`, \`uninstall\`, \`list\`,
483
+ \`query\` and \`build\`.
484
+
485
+ Each global package is installed in its own workspace of
486
+ the project in \`--global-dir\`, and its bins are linked
487
+ into \`--global-bin-dir\`.`,
488
+ },
489
+ })
490
+ .opt({
491
+ 'global-dir': {
492
+ hint: 'path',
493
+ default: xdg.data('global'),
494
+ description: `Location of the global project used by \`--global\`.
495
+ Defaults to \`vlt/global\` in the XDG data directory.
496
+
497
+ Only read from the command line, environment or the user
498
+ config, never from a project \`vlt.json\`. A leading
499
+ \`~\` is the home directory. Must be absolute in the
500
+ user config.`,
501
+ },
502
+ })
503
+ .opt({
504
+ 'global-bin-dir': {
505
+ hint: 'path',
506
+ description: `Folder where bins of global packages are linked.
507
+
508
+ Defaults to the folder of the \`vlt\` found in your
509
+ \`PATH\` when it is writable, so bins run right away.
510
+ If it is not writable, \`~/.local/bin\` is used when
511
+ it is in your \`PATH\` (not on Windows). Otherwise
512
+ \`<global-dir>/bin\`.
513
+
514
+ Only read from the command line, environment or the
515
+ user config, never from a project \`vlt.json\`. A
516
+ leading \`~\` is the home directory. Must be absolute
517
+ in the user config.`,
518
+ },
519
+ })
520
+ .flag({
521
+ path: {
522
+ description: `With \`vlt setup\`, add the global bin folder
523
+ (\`--global-bin-dir\`) to your \`PATH\` by editing
524
+ your shell config (your user \`Path\` on Windows).
525
+ Asks first, unless \`--yes\`. Command line only.`,
526
+ },
527
+ 'no-modify-path': {
528
+ description: `Never edit shell config files (or the user
529
+ \`Path\` on Windows) to add a folder to your
530
+ \`PATH\`: turns off \`vlt setup --path\` edits
531
+ and the \`vlt install -g\` offer (printing the line
532
+ to add instead), and the \`vlt setup\` PATH prompt.
533
+ Set \`VLT_NO_MODIFY_PATH=1\` (any value but \`0\`)
534
+ in images or on machines whose dotfiles are
535
+ managed elsewhere.`,
536
+ },
476
537
  })
477
538
  .opt({
478
539
  scope: {
@@ -525,7 +586,9 @@ export const definition = j
525
586
  config: {
526
587
  hint: 'all | user | project',
527
588
  description: `Specify which configuration to show or operate on when running
528
- \`vlt config\` commands. For read operations (get, pick, list):
589
+ \`vlt config\` commands, and which file
590
+ \`vlt skills link\` saves \`allow-skills\` to. For read
591
+ operations (get, pick, list):
529
592
  \`all\` shows merged configuration from both user and project
530
593
  files (default). For write operations (set, delete, edit):
531
594
  defaults to \`project\`. \`user\` shows/modifies only user-level
@@ -729,6 +792,17 @@ export const definition = j
729
792
  \`add\` or \`update\`. Values a config file already
730
793
  has are left alone.`,
731
794
  },
795
+ 'migrate-workspaces': {
796
+ description: `Save workspaces read from the \`workspaces\` field of
797
+ the root \`package.json\` or from
798
+ \`pnpm-workspace.yaml\` to the project \`vlt.json\` on
799
+ \`vlt install\`. \`vlt.json\` then takes precedence.
800
+
801
+ Set \`--no-migrate-workspaces\` to skip this; an
802
+ already saved \`workspaces\` field stays until
803
+ removed.`,
804
+ default: true,
805
+ },
732
806
  })
733
807
  .opt({
734
808
  'save-prefix': {
@@ -796,6 +870,40 @@ export const definition = j
796
870
  Runs scripts only for direct dependencies of the current project and any occurrences
797
871
  of a specific dependency with the name "my-package" anywhere in the dependency graph.`,
798
872
  },
873
+ })
874
+ .opt({
875
+ 'allow-skills': {
876
+ hint: 'query',
877
+ description: `Link the agent skills (folders with a \`SKILL.md\`) shipped by
878
+ installed packages matching this DSS query into
879
+ the project's agent skill dirs (see \`skills-dir\`)
880
+ after \`install\`, \`ci\`, \`update\` or
881
+ \`uninstall\`. End a selector with
882
+ \`:skill(<name>, …)\` to pick single skills (\`*\`
883
+ globs, double-quoted names). Unset (the default) or
884
+ \`:not(*)\` links nothing and skips all skills work.
885
+
886
+ \`vlt install --allow-skills=<query>\` saves the query
887
+ to the project \`vlt.json\`; \`vlt skills link
888
+ [<query>]\` sets, saves and applies it.
889
+
890
+ Example: --allow-skills="#agent-browser:skill(agent-browser)"
891
+ Links the agent-browser skill from a package with the same name.`,
892
+ },
893
+ })
894
+ .optList({
895
+ 'skills-dir': {
896
+ hint: 'path',
897
+ description: `Dirs to link agent skills into (see \`allow-skills\`),
898
+ relative to the project root. Replaces detection:
899
+ \`.agents/skills\`, plus \`<folder>/skills\` for each
900
+ agent folder at the project root (e.g. \`.claude/\`),
901
+ \`.claude/skills\` inside Claude Code, and existing
902
+ \`.<name>/skills\` dirs. An empty list (\`[]\` in
903
+ vlt.json, or \`--skills-dir=\`) links nothing. Each
904
+ dir must be inside the project, not in
905
+ node_modules.`,
906
+ },
799
907
  })
800
908
  .opt({
801
909
  access: {
@@ -77,7 +77,7 @@ export type ConfigFileData = ConfigFileDataNoCommand & {
77
77
  export type ConfigOptionsNoExtras = {
78
78
  [k in keyof OptionsResults<ConfigDefinitions>]: k extends (RecordField) ? RecordString : k extends 'command' ? never : OptionsResults<ConfigDefinitions>[k];
79
79
  };
80
- export type ConfigOptions = ConfigOptionsNoExtras & Pick<SpecOptions, 'catalog' | 'catalogs'> & {
80
+ export type ConfigOptions = Omit<ConfigOptionsNoExtras, 'path'> & Pick<SpecOptions, 'catalog' | 'catalogs'> & {
81
81
  packageJson: PackageJson;
82
82
  scurry: PathScurry;
83
83
  projectRoot: string;
@@ -142,6 +142,24 @@ export declare class Config {
142
142
  * `~/projects/xyz`, then the highest dir it will check is `~/projects`
143
143
  */
144
144
  projectRoot: string;
145
+ /**
146
+ * Global project root, set when `--global` switched to it.
147
+ */
148
+ globalRoot?: string;
149
+ /**
150
+ * `--global-bin-dir` from cli, env or user config, set with
151
+ * `globalRoot`.
152
+ */
153
+ globalBinDir?: string;
154
+ /**
155
+ * The global project (`--global-dir`, `--global-bin-dir`: cli > env >
156
+ * user config, never a project `vlt.json`): the one switched to by
157
+ * `--global`, or the one `vlt setup` offers PATH setup for.
158
+ */
159
+ globalProject(): {
160
+ root: string;
161
+ binDir?: string;
162
+ } | undefined;
145
163
  /**
146
164
  * `Record<alias, canonical name>` to dereference command aliases.
147
165
  */
@@ -221,8 +239,11 @@ export declare class Config {
221
239
  /**
222
240
  * Find the local config file and load both it and the user-level config in
223
241
  * the XDG config home.
242
+ *
243
+ * When `args` select a `--global` command, the global project is used
244
+ * instead of the current one.
224
245
  */
225
- loadConfigFile(): Promise<this>;
246
+ loadConfigFile(args?: string[]): Promise<this>;
226
247
  /**
227
248
  * Force a complete reload of config files from disk.
228
249
  * This clears all caches and re-reads config files.
@@ -238,7 +259,9 @@ export declare class Config {
238
259
  * only used in tests, resets the memoization
239
260
  * @internal
240
261
  */
241
- reload?: boolean): Promise<LoadedConfig>;
262
+ reload?: boolean,
263
+ /** switch to the global project on `--global` */
264
+ allowGlobal?: boolean): Promise<LoadedConfig>;
242
265
  }
243
266
  export type ParsedConfig = Config & {
244
267
  command: NonNullable<Config['command']>;
@@ -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
  }
@@ -183,6 +183,10 @@ const commandHelp = {
183
183
  desc: 'Configure your vlt.io account registries',
184
184
  defaultOrder: 0,
185
185
  },
186
+ skills: {
187
+ args: '[list|link]',
188
+ desc: 'Manage agent skills from installed packages',
189
+ },
186
190
  token: {
187
191
  args: '[add|rm]',
188
192
  desc: 'Manage authentication tokens',
@@ -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;