@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.
- 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/init.js +5 -2
- 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/pack-tarball.d.ts +3 -1
- package/dist/pack-tarball.js +29 -19
- 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/dist/telemetry.js +8 -1
- package/package.json +30 -28
package/dist/pack-tarball.js
CHANGED
|
@@ -3,7 +3,7 @@ import { minimatch } from 'minimatch';
|
|
|
3
3
|
import { error } from '@vltpkg/error-cause';
|
|
4
4
|
import * as ssri from 'ssri';
|
|
5
5
|
import assert from 'node:assert';
|
|
6
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, statSync, } from 'node:fs';
|
|
7
7
|
import { Spec } from '@vltpkg/spec';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { parse, stringify } from 'polite-json';
|
|
@@ -155,7 +155,24 @@ export const buildPackFilter = (manifest, packDir) => {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
|
|
158
|
+
// `files` entries are normalized like npm does: a leading `./` or `/`
|
|
159
|
+
// and trailing slashes are dropped, empty entries are skipped
|
|
160
|
+
const files = (manifestWithFiles.files ?? [])
|
|
161
|
+
.map(pattern => pattern.replace(/^\.?\//, '').replace(/\/+$/, ''))
|
|
162
|
+
.filter(Boolean);
|
|
163
|
+
// dirs that only partially match a `files` glob (e.g. `dist/nested` for
|
|
164
|
+
// `dist/**/*.js`) are descended, unless unreadable: npm's glob expansion
|
|
165
|
+
// skips those too
|
|
166
|
+
const isReadableDir = (dir) => {
|
|
167
|
+
try {
|
|
168
|
+
readdirSync(join(packDir, dir));
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
return (path, entry) => {
|
|
159
176
|
const normalizedPath = path.replace(/^\.\//, '');
|
|
160
177
|
if (path === '.' || normalizedPath === '') {
|
|
161
178
|
return true;
|
|
@@ -173,23 +190,16 @@ export const buildPackFilter = (manifest, packDir) => {
|
|
|
173
190
|
if (hasEmptyFilesField) {
|
|
174
191
|
return false;
|
|
175
192
|
}
|
|
176
|
-
if (hasFilesField
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
dot: true,
|
|
187
|
-
});
|
|
188
|
-
const isParentDir = pattern.includes('/') &&
|
|
189
|
-
pattern.startsWith(normalizedPath + '/');
|
|
190
|
-
const isChildOfPattern = minimatch(normalizedPath, pattern + '/**', { dot: true });
|
|
191
|
-
return directMatch || isParentDir || isChildOfPattern;
|
|
192
|
-
});
|
|
193
|
+
if (hasFilesField) {
|
|
194
|
+
const isDir = 'isDirectory' in entry && entry.isDirectory();
|
|
195
|
+
return (files.some(pattern => minimatch(normalizedPath, pattern, { dot: true }) ||
|
|
196
|
+
minimatch(normalizedPath, `${pattern}/**`, { dot: true })) ||
|
|
197
|
+
(isDir &&
|
|
198
|
+
files.some(pattern => minimatch(normalizedPath, pattern, {
|
|
199
|
+
dot: true,
|
|
200
|
+
partial: true,
|
|
201
|
+
})) &&
|
|
202
|
+
isReadableDir(normalizedPath)));
|
|
193
203
|
}
|
|
194
204
|
// Ignore-file mode: apply .npmignore or .gitignore deny patterns
|
|
195
205
|
if (ig?.ignores(normalizedPath)) {
|
|
@@ -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 {
|
|
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 =
|
|
94
|
+
const spec = parseSpecArg(item, specOptions);
|
|
107
95
|
parsedItems.push({ spec, type });
|
|
108
96
|
if (spec.type === 'file')
|
|
109
97
|
hasFileSpecs = true;
|
package/dist/query-auth.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
* network-reliant query selectors,
|
|
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;
|
package/dist/query-auth.js
CHANGED
|
@@ -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
|
|
6
|
-
* network-reliant query selectors,
|
|
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
|
-
|
|
11
|
+
const def = defaultRegistryKey(conf.options);
|
|
12
|
+
return async (url) => getTokenByURL(url, conf.options.identity, keys, def);
|
|
11
13
|
};
|
package/dist/reload-config.js
CHANGED
|
@@ -7,5 +7,6 @@ export const reloadConfig = async (folder) => {
|
|
|
7
7
|
}
|
|
8
8
|
catch { }
|
|
9
9
|
const { Config } = await import("./config/index.js");
|
|
10
|
-
|
|
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
|
package/dist/require-registry.js
CHANGED
|
@@ -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;
|