@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.
Files changed (55) 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/install/reporter.d.ts +1 -1
  9. package/dist/commands/install/reporter.js +45 -5
  10. package/dist/commands/install.d.ts +21 -3
  11. package/dist/commands/install.js +84 -30
  12. package/dist/commands/list.js +5 -1
  13. package/dist/commands/query.js +5 -1
  14. package/dist/commands/setup.d.ts +28 -2
  15. package/dist/commands/setup.js +185 -3
  16. package/dist/commands/skills.js +2 -3
  17. package/dist/commands/token.js +1 -1
  18. package/dist/commands/uninstall.d.ts +10 -3
  19. package/dist/commands/uninstall.js +28 -2
  20. package/dist/commands/update.d.ts +1 -1
  21. package/dist/config/definition.d.ts +32 -0
  22. package/dist/config/definition.js +60 -0
  23. package/dist/config/index.d.ts +26 -3
  24. package/dist/config/index.js +149 -7
  25. package/dist/global-bin-dir.d.ts +37 -0
  26. package/dist/global-bin-dir.js +138 -0
  27. package/dist/global-project.d.ts +18 -0
  28. package/dist/global-project.js +67 -0
  29. package/dist/global-state.d.ts +24 -0
  30. package/dist/global-state.js +94 -0
  31. package/dist/global.d.ts +78 -0
  32. package/dist/global.js +409 -0
  33. package/dist/index.js +7 -2
  34. package/dist/install-receipt.d.ts +40 -0
  35. package/dist/install-receipt.js +69 -0
  36. package/dist/output.js +13 -0
  37. package/dist/parse-add-remove-args.js +2 -14
  38. package/dist/query-auth.d.ts +3 -2
  39. package/dist/query-auth.js +6 -4
  40. package/dist/reload-config.js +2 -1
  41. package/dist/require-registry.d.ts +7 -0
  42. package/dist/require-registry.js +13 -1
  43. package/dist/self-update.d.ts +74 -0
  44. package/dist/self-update.js +401 -0
  45. package/dist/shell-setup/env.d.ts +9 -0
  46. package/dist/shell-setup/env.js +25 -0
  47. package/dist/shell-setup/index.d.ts +69 -0
  48. package/dist/shell-setup/index.js +213 -0
  49. package/dist/shell-setup/posix.d.ts +36 -0
  50. package/dist/shell-setup/posix.js +158 -0
  51. package/dist/shell-setup/windows.d.ts +39 -0
  52. package/dist/shell-setup/windows.js +151 -0
  53. package/dist/shell.d.ts +18 -0
  54. package/dist/shell.js +110 -0
  55. package/package.json +30 -28
@@ -2,7 +2,7 @@ import { addKey, asDependency } from '@vltpkg/graph';
2
2
  import { joinDepIDTuple, splitDepID } from '@vltpkg/dep-id';
3
3
  import { Spec } from '@vltpkg/spec';
4
4
  import { isAbsolute, relative, resolve } from 'node:path';
5
- import { asUnknownSpecPrefix } from "./require-registry.js";
5
+ import { parseSpecArg } from "./require-registry.js";
6
6
  const rootDepID = joinDepIDTuple(['file', '.']);
7
7
  /**
8
8
  * Compute a DepID for the current working directory relative to the project
@@ -77,18 +77,6 @@ const rebaseFileSpec = (spec, importerPath, scurryCwd, specOptions, monorepo) =>
77
77
  return spec;
78
78
  return Spec.parseArgs(rebasedPath, specOptions);
79
79
  };
80
- /**
81
- * Parse a positional arg, turning an unknown `name:` prefix into an
82
- * `ECONFIG` that says how to define it.
83
- */
84
- const parseArg = (item, options) => {
85
- try {
86
- return Spec.parseArgs(item, options);
87
- }
88
- catch (er) {
89
- throw asUnknownSpecPrefix(er);
90
- }
91
- };
92
80
  /**
93
81
  * Parses the positional arguments into {@link AddImportersDependenciesMap}.
94
82
  */
@@ -103,7 +91,7 @@ export const parseAddArgs = (config, scurry, monorepo) => {
103
91
  const parsedItems = [];
104
92
  let hasFileSpecs = false;
105
93
  for (const item of items) {
106
- const spec = parseArg(item, specOptions);
94
+ const spec = parseSpecArg(item, specOptions);
107
95
  parsedItems.push({ spec, type });
108
96
  if (spec.type === 'file')
109
97
  hasFileSpecs = true;
@@ -3,7 +3,8 @@ import type { LoadedConfig } from './config/index.ts';
3
3
  /**
4
4
  * Returns a {@link GetAuthHeader} implementation that looks up
5
5
  * authorization tokens from the local keychain / env (incl.
6
- * `VLT_TOKEN_<key>` for configured registries), used by
7
- * network-reliant query selectors, e.g: `:outdated()`.
6
+ * `VLT_TOKEN` for the default registry and `VLT_TOKEN_<key>` for
7
+ * configured registries), used by network-reliant query selectors,
8
+ * e.g: `:outdated()`.
8
9
  */
9
10
  export declare const createGetAuthHeader: (conf: LoadedConfig) => GetAuthHeader;
@@ -1,11 +1,13 @@
1
- import { getTokenByURL, registryKeys } from '@vltpkg/registry-client';
1
+ import { defaultRegistryKey, getTokenByURL, registryKeys, } from '@vltpkg/registry-client';
2
2
  /**
3
3
  * Returns a {@link GetAuthHeader} implementation that looks up
4
4
  * authorization tokens from the local keychain / env (incl.
5
- * `VLT_TOKEN_<key>` for configured registries), used by
6
- * network-reliant query selectors, e.g: `:outdated()`.
5
+ * `VLT_TOKEN` for the default registry and `VLT_TOKEN_<key>` for
6
+ * configured registries), used by network-reliant query selectors,
7
+ * e.g: `:outdated()`.
7
8
  */
8
9
  export const createGetAuthHeader = (conf) => {
9
10
  const keys = registryKeys(conf.options);
10
- return async (url) => getTokenByURL(url, conf.options.identity, keys);
11
+ const def = defaultRegistryKey(conf.options);
12
+ return async (url) => getTokenByURL(url, conf.options.identity, keys, def);
11
13
  };
@@ -7,5 +7,6 @@ export const reloadConfig = async (folder) => {
7
7
  }
8
8
  catch { }
9
9
  const { Config } = await import("./config/index.js");
10
- return Config.load(folder, process.argv, true);
10
+ // host contexts never switch to the global project
11
+ return Config.load(folder, process.argv, true, false);
11
12
  };
@@ -1,3 +1,5 @@
1
+ import { Spec } from '@vltpkg/spec';
2
+ import type { SpecOptions } from '@vltpkg/spec';
1
3
  import type { RegistryCandidate } from './select-registry.ts';
2
4
  import type { LoadedConfig } from './config/index.ts';
3
5
  /**
@@ -17,6 +19,11 @@ export declare const unknownSpecPrefixError: (prefix: string, spec: string, vali
17
19
  * {@link unknownSpecPrefixError}. Other values are returned as is.
18
20
  */
19
21
  export declare const asUnknownSpecPrefix: (er: unknown) => unknown;
22
+ /**
23
+ * Parse a positional arg, turning an unknown `name:` prefix into an
24
+ * `ECONFIG` that says how to define it.
25
+ */
26
+ export declare const parseSpecArg: (item: string, options: SpecOptions) => Spec;
20
27
  /**
21
28
  * Install-related commands need the alias that bare specs resolve
22
29
  * through (not merely some other configured registry) so packages and
@@ -1,6 +1,6 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { isErrorWithCause, isObject } from '@vltpkg/types';
3
- import { defaultRegistries, defaultRegistryName } from '@vltpkg/spec';
3
+ import { defaultRegistries, defaultRegistryName, Spec, } from '@vltpkg/spec';
4
4
  import { selectRegistry } from "./select-registry.js";
5
5
  /**
6
6
  * There is no default registry. This is the single message used
@@ -41,6 +41,18 @@ export const asUnknownSpecPrefix = (er) => {
41
41
  unknownSpecPrefixError(found.slice(0, -1), spec, validOptions)
42
42
  : er;
43
43
  };
44
+ /**
45
+ * Parse a positional arg, turning an unknown `name:` prefix into an
46
+ * `ECONFIG` that says how to define it.
47
+ */
48
+ export const parseSpecArg = (item, options) => {
49
+ try {
50
+ return Spec.parseArgs(item, options);
51
+ }
52
+ catch (er) {
53
+ throw asUnknownSpecPrefix(er);
54
+ }
55
+ };
44
56
  /**
45
57
  * Install-related commands need the alias that bare specs resolve
46
58
  * through (not merely some other configured registry) so packages and
@@ -0,0 +1,74 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
2
+ import type { Graph } from '@vltpkg/graph';
3
+ import type { LoadedConfig } from './config/index.ts';
4
+ import type { GlobalBins } from './global.ts';
5
+ export type SelfUpdateCase = 'standalone' | 'npm' | 'global-project' | 'bootstrap';
6
+ /** `vlt install -g vlt` that ran, or had nothing to do */
7
+ export type SelfUpdated = {
8
+ case: SelfUpdateCase;
9
+ status: 'updated' | 'up-to-date' | 'newer-running' | 'failed';
10
+ /** running version */
11
+ from: string;
12
+ /** resolved version */
13
+ to: string;
14
+ /** dir of the updated vlt */
15
+ dir: string;
16
+ /** newer-running: dist-tag of the implicit spec */
17
+ tag?: string;
18
+ /** bootstrap: the dir that is not writable */
19
+ notWritable?: string;
20
+ /** bootstrap: PATH dir whose vlt still runs first */
21
+ shadowedBy?: string;
22
+ };
23
+ /** vlt can't update this vlt, `message` says how to */
24
+ export type SelfRefused = {
25
+ status: 'refused';
26
+ /** running version */
27
+ from: string;
28
+ /** dir of the running vlt */
29
+ dir: string;
30
+ message: string;
31
+ };
32
+ /** result of `vlt install -g vlt` */
33
+ export type SelfUpdate = SelfUpdated | SelfRefused;
34
+ export type SelfUpdatePlan = {
35
+ done: SelfUpdate;
36
+ }
37
+ /** caller runs the -g install, then `finish` */
38
+ | {
39
+ selfUnwritable: boolean;
40
+ finish: (global?: GlobalBins) => Promise<SelfUpdated>;
41
+ };
42
+ /** `checksum` of the file written to `dest`, `sha512-<base64>` */
43
+ export type Release = {
44
+ checksum: string;
45
+ };
46
+ /** download one standalone entry point to `dest` */
47
+ export type FetchRelease = (o: {
48
+ version: string;
49
+ target: string;
50
+ channel?: string;
51
+ bin: string;
52
+ dest: string;
53
+ }) => Promise<Release>;
54
+ /** `vlt install -g vlt...`, for the human view */
55
+ export declare const isSelfUpdateArgs: (conf: Pick<LoadedConfig, "command" | "globalRoot" | "positionals">) => boolean;
56
+ /**
57
+ * `vlt install -g vlt`: update the vlt on the PATH the way it was
58
+ * installed. Undefined when vlt is not a positional.
59
+ */
60
+ export declare const selfUpdate: (conf: LoadedConfig, { fetchRelease }?: {
61
+ fetchRelease?: FetchRelease;
62
+ }) => Promise<SelfUpdatePlan | undefined>;
63
+ /** `vlt uninstall -g vlt` of a vlt that is not a global package */
64
+ export declare const refuseSelfUninstall: (conf: LoadedConfig) => Promise<SelfRefused | undefined>;
65
+ /** store id of the running vlt, when it runs from the global project */
66
+ export declare const runningStoreDep: (root: string) => Promise<DepID | undefined>;
67
+ /**
68
+ * Record the running vlt's store dir for removal by a later -g
69
+ * command when the graph no longer has it, and forget listed dirs
70
+ * the graph has again.
71
+ */
72
+ export declare const deferSelfRemove: (root: string, graph: Pick<Graph, "nodes">, keep?: DepID) => Promise<void>;
73
+ /** human lines for a self-update */
74
+ export declare const selfUpdateLines: (s: SelfUpdate) => string[];
@@ -0,0 +1,401 @@
1
+ import { findCmdShimIfExists } from '@vltpkg/cmd-shim';
2
+ import { isDepID } from '@vltpkg/dep-id';
3
+ import { error } from '@vltpkg/error-cause';
4
+ import { lt, valid } from '@vltpkg/semver';
5
+ import { Spec } from '@vltpkg/spec';
6
+ import { which } from '@vltpkg/which';
7
+ import { spawn } from 'node:child_process';
8
+ import { createHash } from 'node:crypto';
9
+ import { constants, existsSync } from 'node:fs';
10
+ import { access, chmod, lstat, readFile, realpath, rename, rm, stat, } from 'node:fs/promises';
11
+ import { basename, dirname, extname, join, relative, resolve, sep, } from 'node:path';
12
+ import { loadPackageJson } from 'package-json-from-dist';
13
+ import { findSelf } from "./global-bin-dir.js";
14
+ import { globalSpec } from "./global.js";
15
+ import { globalWorkspacePath, isInside, samePath, } from "./global-project.js";
16
+ import { readGlobalState, updateGlobalState } from "./global-state.js";
17
+ import { writeInstallReceipt } from "./install-receipt.js";
18
+ import { parseSpecArg } from "./require-registry.js";
19
+ import { prettyPath } from "./shell.js";
20
+ const { version: running } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_CLI_PACKAGE_JSON);
21
+ const installerLine = 'Re-run the installer to update vlt:\n curl -fsSL https://install.vlt.sh | bash';
22
+ const writable = (dir) => access(dir, constants.W_OK).then(() => true, () => false);
23
+ // exit 1 without usage: the usage was fine
24
+ const refuse = (self, message) => {
25
+ process.exitCode = 1;
26
+ return { status: 'refused', from: running, dir: self.dir, message };
27
+ };
28
+ const isFile = (f) => stat(f).then(s => s.isFile(), () => false);
29
+ const cellars = () => [
30
+ process.env.HOMEBREW_CELLAR,
31
+ '/opt/homebrew/Cellar',
32
+ '/usr/local/Cellar',
33
+ '/home/linuxbrew/.linuxbrew/Cellar',
34
+ ].filter((c) => !!c);
35
+ const whichNpm = async () => {
36
+ try {
37
+ const found = await which('npm', { nothrow: true });
38
+ if (!found)
39
+ return;
40
+ const real = await realpath((await findCmdShimIfExists(found))?.[1] ?? found);
41
+ return basename(real) === 'npm-cli.js' ? real : undefined;
42
+ }
43
+ catch { }
44
+ };
45
+ // npm's -g layout: <prefix>/lib/node_modules, win32 <prefix>/node_modules
46
+ const findNpm = async (self) => {
47
+ const i = self.target.lastIndexOf(`${sep}node_modules${sep}vlt${sep}`);
48
+ if (i === -1)
49
+ return;
50
+ const nm = self.target.slice(0, i) + sep + 'node_modules';
51
+ const win32 = process.platform === 'win32';
52
+ const up = dirname(nm);
53
+ const prefix = win32 ?
54
+ // vlt.cmd is in the prefix
55
+ samePath(up, self.dir) && up
56
+ : basename(up) === 'lib' && dirname(up);
57
+ if (!prefix)
58
+ return;
59
+ const node = dirname(process.execPath);
60
+ const candidates = [
61
+ join(nm, 'npm/bin/npm-cli.js'),
62
+ win32 ?
63
+ join(node, 'node_modules/npm/bin/npm-cli.js')
64
+ : join(dirname(node), 'lib/node_modules/npm/bin/npm-cli.js'),
65
+ ];
66
+ for (const c of candidates) {
67
+ if (await isFile(c))
68
+ return { npmCli: c, prefix };
69
+ }
70
+ const npmCli = await whichNpm();
71
+ return npmCli && { npmCli, prefix };
72
+ };
73
+ const classify = async (self, root) => {
74
+ // before the receipt: it can outlive the vlt it was written for
75
+ const g = await realpath(root).catch(() => resolve(root));
76
+ if (isInside(g, self.target))
77
+ return { kind: 'global-project' };
78
+ const { receipt } = self;
79
+ if (receipt?.data.method === 'standalone') {
80
+ if (self.standalone)
81
+ return { kind: 'standalone-receipt', receipt };
82
+ }
83
+ else if (receipt) {
84
+ const npm = receipt.data.method === 'npm' && (await findNpm(self));
85
+ return npm ?
86
+ { kind: 'npm', ...npm, receipt }
87
+ : { kind: 'installer' };
88
+ }
89
+ // before npm: a node formula has npm's layout
90
+ if (cellars().some(c => isInside(c, self.target))) {
91
+ return { kind: 'homebrew' };
92
+ }
93
+ if (self.standalone)
94
+ return { kind: 'standalone' };
95
+ const npm = await findNpm(self);
96
+ return npm ? { kind: 'npm', ...npm } : { kind: 'unknown' };
97
+ };
98
+ const sha512 = async (file) => `sha512-${createHash('sha512')
99
+ .update(await readFile(file))
100
+ .digest('base64')}`;
101
+ const safeBin = (b) => /^[a-z0-9][\w.-]*$/i.test(b);
102
+ /** a refusal message, or undefined once replaced */
103
+ const updateStandalone = async (self, receipt, to, fetchRelease) => {
104
+ const { target, channel, binaries = [] } = receipt.data;
105
+ if (!fetchRelease || !target)
106
+ return installerLine;
107
+ const win32 = process.platform === 'win32';
108
+ const at = prettyPath(self.dir);
109
+ // stat().uid is 0 on win32
110
+ if (!win32 &&
111
+ (await stat(self.found)).uid === 0 &&
112
+ process.getuid?.() !== 0) {
113
+ return `vlt at ${at} is owned by root. Update it the way you installed it, or run this as root.`;
114
+ }
115
+ if (!(await writable(self.dir))) {
116
+ return `vlt at ${at} is not writable. Update it the way you installed it.`;
117
+ }
118
+ const ext = extname(self.found).toLowerCase() === '.exe' ? '.exe' : '';
119
+ const entries = [];
120
+ for (const bin of new Set(binaries)) {
121
+ if (bin === 'vlt' || !safeBin(bin))
122
+ continue;
123
+ const file = join(self.dir, bin + ext);
124
+ const st = await lstat(file).catch(() => undefined);
125
+ // links follow vlt
126
+ if (st && !st.isSymbolicLink())
127
+ entries.push({ bin, file });
128
+ }
129
+ // last, so a failure part-way leaves a working vlt
130
+ entries.push({
131
+ bin: 'vlt',
132
+ file: join(self.dir, basename(self.found)),
133
+ });
134
+ const tmps = [];
135
+ const cleanup = () => Promise.all(tmps.map(f => rm(f, { force: true })));
136
+ const ready = [];
137
+ try {
138
+ for (const { bin, file } of entries) {
139
+ // a running vlt.exe.old: the rename below says so
140
+ if (win32)
141
+ await rm(`${file}.old`, { force: true }).catch(() => { });
142
+ // same dir: rename is atomic
143
+ const dest = join(self.dir, `.${bin}-${to}.tmp`);
144
+ tmps.push(dest);
145
+ const { checksum } = await fetchRelease({
146
+ version: to,
147
+ target,
148
+ channel,
149
+ bin,
150
+ dest,
151
+ });
152
+ const found = await sha512(dest);
153
+ if (found !== checksum) {
154
+ throw error(`Checksum mismatch for ${bin} ${to}`, {
155
+ code: 'EINTEGRITY',
156
+ wanted: checksum,
157
+ found,
158
+ });
159
+ }
160
+ await chmod(dest, (await stat(file)).mode & 0o777);
161
+ ready.push({ file, dest });
162
+ }
163
+ }
164
+ catch (er) {
165
+ await cleanup();
166
+ throw er;
167
+ }
168
+ // never write into a running file: rename only
169
+ for (const { file, dest } of ready) {
170
+ try {
171
+ if (win32) {
172
+ // a running .exe can be renamed, not replaced
173
+ await rename(file, `${file}.old`);
174
+ await rename(dest, file).catch(async (er) => {
175
+ await rename(`${file}.old`, file).catch(() => { });
176
+ throw er;
177
+ });
178
+ }
179
+ else {
180
+ await rename(dest, file);
181
+ }
182
+ }
183
+ catch (cause) {
184
+ await cleanup();
185
+ throw error(`Could not replace ${file}. Close other vlt processes and editors, and check permissions.`, { path: file, cause });
186
+ }
187
+ }
188
+ };
189
+ const runNpm = (npmCli, prefix, arg) => new Promise((res, rej) => {
190
+ // stdout to stderr: keeps --view=json parseable
191
+ const child = spawn(process.execPath, [npmCli, 'install', '--global', '--prefix', prefix, arg], { stdio: ['inherit', 2, 2] });
192
+ child.on('error', rej);
193
+ child.on('close', (code) => res(code ?? 1));
194
+ });
195
+ const rewriteReceipt = (receipt, to) => receipt &&
196
+ writeInstallReceipt(receipt.file, { version: to }).catch(() => { });
197
+ // vlt, vlt@..., a vlt tarball
198
+ const vltArg = /^vlt(@|$)|(^|[/\\:])vlt-\d[^/\\]*\.tgz$/;
199
+ /** `vlt install -g vlt...`, for the human view */
200
+ export const isSelfUpdateArgs = (conf) => conf.command === 'install' &&
201
+ !!conf.globalRoot &&
202
+ conf.positionals.some(p => vltArg.test(p));
203
+ /**
204
+ * `vlt install -g vlt`: update the vlt on the PATH the way it was
205
+ * installed. Undefined when vlt is not a positional.
206
+ */
207
+ export const selfUpdate = async (conf, { fetchRelease } = {}) => {
208
+ const { globalRoot, positionals } = conf;
209
+ if (!globalRoot || !positionals.length)
210
+ return;
211
+ const specs = await Promise.all(positionals.map(p => globalSpec(p, conf)));
212
+ const [spec] = specs;
213
+ if (!spec || !specs.some(s => s.name === 'vlt'))
214
+ return;
215
+ if (specs.length > 1) {
216
+ throw error('Update vlt on its own, without other packages', {
217
+ code: 'EUSAGE',
218
+ found: positionals.join(' '),
219
+ });
220
+ }
221
+ const self = await findSelf();
222
+ if (!self) {
223
+ throw error(`Can't tell which vlt to update: run the vlt on your PATH, not ${process.argv[1] ?? process.execPath}`, { code: 'EUSAGE' });
224
+ }
225
+ const owner = await classify(self, globalRoot);
226
+ const at = prettyPath(self.dir);
227
+ const foreign = `vlt at ${at} is not managed by vlt or npm. Update it the way you installed it.`;
228
+ if (owner.kind === 'installer') {
229
+ return { done: refuse(self, installerLine) };
230
+ }
231
+ if (owner.kind === 'homebrew') {
232
+ return {
233
+ done: refuse(self, `vlt at ${at} was installed by Homebrew. Run \`brew upgrade vlt\`.`),
234
+ };
235
+ }
236
+ if (owner.kind === 'standalone') {
237
+ return { done: refuse(self, foreign) };
238
+ }
239
+ // npm's node_modules not writable: npm's EACCES says so
240
+ const bootstrap = (owner.kind === 'npm' || owner.kind === 'unknown') &&
241
+ !(await writable(self.dir));
242
+ if (owner.kind === 'unknown' && !bootstrap) {
243
+ return { done: refuse(self, foreign) };
244
+ }
245
+ const raw = String(positionals[0]);
246
+ const implicit = !parseSpecArg(raw, conf.options).bareSpec;
247
+ const receipt = 'receipt' in owner ? owner.receipt : undefined;
248
+ const channel = receipt?.data.channel;
249
+ const tag = channel ?? conf.options.tag;
250
+ const { name, version: to } = await conf.options.packageInfo.manifest(implicit && channel ?
251
+ Spec.parse('vlt', tag, conf.options)
252
+ : spec);
253
+ if (name !== 'vlt' || !to) {
254
+ throw error(`${raw} is not a version of vlt`, {
255
+ code: 'EUSAGE',
256
+ found: name,
257
+ });
258
+ }
259
+ const kind = bootstrap ? 'bootstrap'
260
+ : owner.kind === 'standalone-receipt' ? 'standalone'
261
+ : owner.kind === 'npm' ? 'npm'
262
+ : 'global-project';
263
+ const base = { case: kind, from: running, to, dir: self.dir };
264
+ if (to === running)
265
+ return { done: { ...base, status: 'up-to-date' } };
266
+ // `latest` may lag a new major
267
+ if (implicit && lt(to, running)) {
268
+ return { done: { ...base, status: 'newer-running', tag } };
269
+ }
270
+ const registry = spec.final.type === 'registry';
271
+ if (owner.kind === 'standalone-receipt') {
272
+ // releases are registry versions
273
+ if (!registry || !valid(to)) {
274
+ throw error(`${raw} is not a vlt release`, {
275
+ code: 'EUSAGE',
276
+ found: raw,
277
+ });
278
+ }
279
+ const refused = await updateStandalone(self, owner.receipt, to, fetchRelease);
280
+ if (refused)
281
+ return { done: refuse(self, refused) };
282
+ await rewriteReceipt(owner.receipt, to);
283
+ return { done: { ...base, status: 'updated' } };
284
+ }
285
+ if (owner.kind === 'npm' && !bootstrap) {
286
+ const code = await runNpm(owner.npmCli, owner.prefix, registry ? `vlt@${to}` : `vlt@${spec.bareSpec}`);
287
+ // npm already said why
288
+ if (code !== 0) {
289
+ process.exitCode = code;
290
+ return { done: { ...base, status: 'failed' } };
291
+ }
292
+ await rewriteReceipt(receipt, to);
293
+ return { done: { ...base, status: 'updated' } };
294
+ }
295
+ // the ws pins what was resolved
296
+ if (registry)
297
+ conf.positionals = [`vlt@${to}`];
298
+ return {
299
+ selfUnwritable: bootstrap,
300
+ finish: async (global) => {
301
+ const u = {
302
+ ...base,
303
+ status: 'updated',
304
+ dir: global?.binDir ?? self.dir,
305
+ ...(bootstrap ? { notWritable: self.dir } : {}),
306
+ };
307
+ if (bootstrap &&
308
+ global?.inPath &&
309
+ global.bins.includes('vlt')) {
310
+ const now = await which('vlt', { nothrow: true }).catch(() => null);
311
+ const first = now && dirname(now);
312
+ if (first &&
313
+ !samePath(await realpath(first).catch(() => first), global.binDir)) {
314
+ u.shadowedBy = first;
315
+ }
316
+ }
317
+ return u;
318
+ },
319
+ };
320
+ };
321
+ /** `vlt uninstall -g vlt` of a vlt that is not a global package */
322
+ export const refuseSelfUninstall = async (conf) => {
323
+ const { globalRoot, positionals } = conf;
324
+ if (!globalRoot || !positionals.includes('vlt'))
325
+ return;
326
+ const ws = resolve(globalRoot, globalWorkspacePath('vlt'));
327
+ if (existsSync(resolve(ws, 'package.json')))
328
+ return;
329
+ const self = await findSelf();
330
+ if (!self)
331
+ return;
332
+ const { kind } = await classify(self, globalRoot);
333
+ return refuse(self, kind === 'npm' ?
334
+ 'vlt was installed by npm. Run `npm uninstall -g vlt` to remove it.'
335
+ : kind === 'homebrew' ?
336
+ 'vlt was installed by Homebrew. Run `brew uninstall vlt` to remove it.'
337
+ : `vlt at ${prettyPath(self.dir)} is not managed by vlt. Remove it the way you installed it.`);
338
+ };
339
+ /** store id of the running vlt, when it runs from the global project */
340
+ export const runningStoreDep = async (root) => {
341
+ const script = process.argv[1];
342
+ if (!script)
343
+ return;
344
+ try {
345
+ const [real, store] = await Promise.all([
346
+ realpath(script),
347
+ realpath(resolve(root, 'node_modules/.vlt')),
348
+ ]);
349
+ const [id] = relative(store, real).split(sep);
350
+ return isDepID(id) ? id : undefined;
351
+ }
352
+ catch { }
353
+ };
354
+ /**
355
+ * Record the running vlt's store dir for removal by a later -g
356
+ * command when the graph no longer has it, and forget listed dirs
357
+ * the graph has again.
358
+ */
359
+ export const deferSelfRemove = async (root, graph, keep) => {
360
+ const store = resolve(await realpath(root), 'node_modules/.vlt');
361
+ const add = keep && !graph.nodes.has(keep) ? [resolve(store, keep)] : [];
362
+ const { pendingRemove = [] } = await readGlobalState(root);
363
+ const kept = pendingRemove.filter(p => {
364
+ const id = basename(p);
365
+ return !(isDepID(id) && graph.nodes.has(id));
366
+ });
367
+ if (!add.length && kept.length === pendingRemove.length)
368
+ return;
369
+ await updateGlobalState(root, s => ({
370
+ ...s,
371
+ pendingRemove: [...new Set([...kept, ...add])],
372
+ }));
373
+ };
374
+ /** human lines for a self-update */
375
+ export const selfUpdateLines = (s) => {
376
+ if (s.status === 'refused')
377
+ return [s.message];
378
+ if (s.status === 'up-to-date') {
379
+ return [`✅ vlt ${s.to} is already up to date`];
380
+ }
381
+ if (s.status === 'newer-running') {
382
+ return [
383
+ `ℹ️ vlt ${s.from} is running but "${s.tag}" is ${s.to}; run \`vlt i -g vlt@${s.tag}\` to downgrade`,
384
+ ];
385
+ }
386
+ if (s.status === 'failed')
387
+ return [];
388
+ if (s.case === 'bootstrap') {
389
+ return [
390
+ `ℹ️ ${prettyPath(s.notWritable ?? s.dir)} is not writable, so vlt ${s.to} was installed as a global package.`,
391
+ ...(s.shadowedBy ?
392
+ [
393
+ `⚠️ ${prettyPath(s.shadowedBy)} is earlier in your PATH, so \`vlt\` still runs vlt ${s.from}.`,
394
+ ]
395
+ : []),
396
+ ];
397
+ }
398
+ return [
399
+ `⬆️ Updated vlt ${s.from} → ${s.to} in ${prettyPath(s.dir)}${s.case === 'npm' ? ' (via npm)' : ''}`,
400
+ ];
401
+ };
@@ -0,0 +1,9 @@
1
+ import type { ShellOptions } from '../shell.ts';
2
+ /** `<global-dir>/env`, sourced by sh-family rc files */
3
+ export declare const envFile: (globalDir: string) => string;
4
+ /** `<global-dir>/env.fish`, sourced by fish */
5
+ export declare const envFishFile: (globalDir: string) => string;
6
+ /** sh script adding `dir` to the `PATH` once */
7
+ export declare const renderEnv: (dir: string, o?: ShellOptions) => string;
8
+ /** fish script adding `dir` to the `PATH` of the session */
9
+ export declare const renderEnvFish: (dir: string, o?: ShellOptions) => string;
@@ -0,0 +1,25 @@
1
+ import { resolve } from 'node:path';
2
+ import { fishPath, posixPath, shellPathCommand } from "../shell.js";
3
+ /** `<global-dir>/env`, sourced by sh-family rc files */
4
+ export const envFile = (globalDir) => resolve(globalDir, 'env');
5
+ /** `<global-dir>/env.fish`, sourced by fish */
6
+ export const envFishFile = (globalDir) => resolve(globalDir, 'env.fish');
7
+ /** sh script adding `dir` to the `PATH` once */
8
+ export const renderEnv = (dir, o = {}) => [
9
+ '# vlt global bin dir; sourced from the shell rc file. Edit with `vlt setup --path`.',
10
+ 'case ":$PATH:" in',
11
+ ` *:${posixPath(dir, o)}:*) ;;`,
12
+ ` *) ${shellPathCommand('sh', dir, o)} ;;`,
13
+ 'esac',
14
+ '',
15
+ ].join('\n');
16
+ /** fish script adding `dir` to the `PATH` of the session */
17
+ export const renderEnvFish = (dir, o = {}) => {
18
+ const d = fishPath(dir, o);
19
+ return [
20
+ '# vlt global bin dir; sourced from fish config. Edit with `vlt setup --path`.',
21
+ // session only, and fish_add_path needs fish 3.2
22
+ `contains -- ${d} $PATH; or set -gx PATH ${d} $PATH`,
23
+ '',
24
+ ].join('\n');
25
+ };