@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
@@ -0,0 +1,213 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { asError } from '@vltpkg/types';
3
+ import { existsSync } from 'node:fs';
4
+ import { chown, mkdir, realpath, writeFile } from 'node:fs/promises';
5
+ import { basename, dirname, resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { readGlobalState, updateGlobalState, writeFileAtomic, } from "../global-state.js";
8
+ import { detectShell, fishPath, posixPath, prettyPath, shellPathCommand, } from "../shell.js";
9
+ import { envFile, envFishFile, renderEnv, renderEnvFish, } from "./env.js";
10
+ import { planEnvFile, planRcFile, rcFiles, renderBlock, } from "./posix.js";
11
+ import { applyWindowsPath, planWindowsPath, userEnvLabel, } from "./windows.js";
12
+ const posixShells = new Set([
13
+ 'bash',
14
+ 'zsh',
15
+ 'fish',
16
+ 'sh',
17
+ ]);
18
+ /** whether `vlt setup --path` can edit config for `shell` */
19
+ export const canSetupPath = (shell, platform = process.platform) =>
20
+ // win32: the user Path reaches every shell
21
+ platform === 'win32' || posixShells.has(shell);
22
+ /** what adding `dir` to the `PATH` of new shells would change */
23
+ export const planPathSetup = async (o) => {
24
+ const { globalDir, dir, shell } = o;
25
+ if ((o.platform ?? process.platform) === 'win32') {
26
+ const { env } = o;
27
+ const registry = await planWindowsPath(dir, { env });
28
+ return { globalDir, dir, shell, changes: [], registry, env };
29
+ }
30
+ const command = shellPathCommand(shell, dir, o);
31
+ if (!canSetupPath(shell, o.platform)) {
32
+ const what = shell === 'unknown' ? 'your shell' : shell;
33
+ throw error(`vlt setup --path doesn't support ${what} yet. Add it to your PATH yourself:\n ${command}`, { code: 'ECONFIG', found: shell });
34
+ }
35
+ // env first: rc files point at it
36
+ const changes = [
37
+ await planEnvFile(envFile(globalDir), renderEnv(dir, o)),
38
+ await planEnvFile(envFishFile(globalDir), renderEnvFish(dir, o)),
39
+ ];
40
+ const posix = shell;
41
+ const block = renderBlock(posix, globalDir, o);
42
+ // links to one target: edit it once
43
+ const seen = new Set();
44
+ for (const f of rcFiles(posix, o)) {
45
+ const c = await planRcFile(f, block);
46
+ const real = await realpath(c.file).catch(() => c.file);
47
+ if (seen.has(real))
48
+ continue;
49
+ seen.add(real);
50
+ changes.push(c);
51
+ }
52
+ return { globalDir, dir, shell, changes };
53
+ };
54
+ export const pathSetupChanged = (plan) => plan.changes.some(c => c.action !== 'unchanged') ||
55
+ (!!plan.registry && plan.registry.action !== 'unchanged');
56
+ export const pathSetupFiles = (plan) => {
57
+ const files = plan.changes.map(({ file, kind, action }) => ({ file, kind, action }));
58
+ const reg = plan.registry;
59
+ if (reg) {
60
+ files.push({
61
+ file: reg.key,
62
+ kind: 'registry',
63
+ action: reg.action,
64
+ });
65
+ }
66
+ return files;
67
+ };
68
+ /** rc files or the user Path, for display */
69
+ export const rcList = (files, home) => files
70
+ .filter(f => f.kind !== 'env')
71
+ .map(f => f.kind === 'registry' ? userEnvLabel : prettyPath(f.file, home))
72
+ .join(', ');
73
+ /** write the plan: env files, rc files, user Path, backed up */
74
+ export const applyPathSetup = async (plan) => {
75
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
76
+ const backupDir = resolve(plan.globalDir, 'backups');
77
+ const backups = [];
78
+ const backup = async (name, data, mode) => {
79
+ await mkdir(backupDir, { recursive: true });
80
+ const b = resolve(backupDir, `${name}.${stamp}.bak`);
81
+ await writeFile(b, data, { mode });
82
+ backups.push(b);
83
+ };
84
+ for (const c of plan.changes) {
85
+ if (c.action === 'unchanged')
86
+ continue;
87
+ if (c.previous !== undefined) {
88
+ await backup(basename(c.file).replace(/^\./, ''), c.previous, c.mode);
89
+ }
90
+ // sudo must not leave root-owned files in the home dir
91
+ const owner = process.getuid?.() === 0 ? c.owner : undefined;
92
+ // and the dirs mkdir makes
93
+ const own = [c.file];
94
+ let d = dirname(c.file);
95
+ while (owner && !existsSync(d)) {
96
+ own.push(d);
97
+ d = dirname(d);
98
+ }
99
+ await mkdir(dirname(c.file), { recursive: true });
100
+ await writeFileAtomic(c.file, c.content, c.mode);
101
+ if (owner) {
102
+ for (const f of own)
103
+ await chown(f, owner.uid, owner.gid);
104
+ }
105
+ }
106
+ const reg = plan.registry;
107
+ if (reg && reg.action !== 'unchanged') {
108
+ if (reg.previous !== undefined)
109
+ await backup('Path', reg.previous);
110
+ await applyWindowsPath(reg, { env: plan.env });
111
+ }
112
+ return {
113
+ dir: plan.dir,
114
+ shell: plan.shell,
115
+ files: pathSetupFiles(plan),
116
+ backups,
117
+ };
118
+ };
119
+ /** loads the env file into the current shell (win32: adds `dir`) */
120
+ export const sourceCommand = (shell, globalDir, dir, o = {}) =>
121
+ // no env file on win32
122
+ (o.platform ?? process.platform) === 'win32' ?
123
+ shellPathCommand(shell, dir, o)
124
+ : shell === 'fish' ? `source ${fishPath(envFishFile(globalDir), o)}`
125
+ : shell === 'sh' ? `. ${posixPath(envFile(globalDir), o)}`
126
+ : `source ${posixPath(envFile(globalDir), o)}`;
127
+ export const pathSetupLines = (r, source, home) => {
128
+ const lines = [
129
+ `✅ Added ${prettyPath(r.dir, home)} to your PATH in ${rcList(r.files, home)}`,
130
+ ` Open a new terminal${r.files.some(f => f.kind === 'registry') ? ' (restart editors like VS Code)' : ''}, or run: ${source}`,
131
+ ];
132
+ const [b] = r.backups;
133
+ if (b)
134
+ lines.push(` Backups in ${prettyPath(dirname(b), home)}`);
135
+ return lines;
136
+ };
137
+ /** one trimmed answer */
138
+ export const ask = async (question, { input = process.stdin, output = process.stdout, } = {}) => {
139
+ const rl = createInterface({ input, output });
140
+ try {
141
+ return (await rl.question(question)).trim();
142
+ }
143
+ finally {
144
+ rl.close();
145
+ input.pause();
146
+ }
147
+ };
148
+ /**
149
+ * After `install -g` linked into a dir not in the `PATH`: offer, once,
150
+ * in a terminal, to add it to the PATH. Never throws.
151
+ */
152
+ export const offerPathSetup = async (g, o) => {
153
+ const { globalDir, input = process.stdin, output = process.stdout, env = process.env, platform, home, } = o;
154
+ if (o.noModifyPath || g.inPath)
155
+ return;
156
+ if (!g.bins.length && !g.conflicts.length)
157
+ return;
158
+ if (!input.isTTY || !output.isTTY || env.CI)
159
+ return;
160
+ if ((await readGlobalState(globalDir)).pathSetup === 'declined')
161
+ return;
162
+ const sh = { env, platform, home };
163
+ const shell = detectShell(env, platform);
164
+ const po = { ...sh, globalDir, dir: g.binDir, shell };
165
+ let plan;
166
+ try {
167
+ plan = await planPathSetup(po);
168
+ }
169
+ catch {
170
+ // the PATH warning has the manual line
171
+ return;
172
+ }
173
+ const rcs = rcList(pathSetupFiles(plan), home);
174
+ const source = sourceCommand(shell, globalDir, g.binDir, sh);
175
+ const write = (line) => output.write(line + '\n');
176
+ o.beforeOutput?.();
177
+ // blank line after the PATH warning
178
+ write('');
179
+ if (!pathSetupChanged(plan)) {
180
+ write(`ℹ️ It's already added in ${rcs}: open a new terminal, or run: ${source}`);
181
+ return;
182
+ }
183
+ if (!o.yes) {
184
+ let answer;
185
+ try {
186
+ answer = await ask(`Add it to ${rcs} now? (Y/n) `, {
187
+ input,
188
+ output,
189
+ });
190
+ }
191
+ catch {
192
+ return;
193
+ }
194
+ if (/^n/i.test(answer)) {
195
+ await updateGlobalState(globalDir, s => ({
196
+ ...s,
197
+ pathSetup: 'declined',
198
+ })).catch(() => { });
199
+ return;
200
+ }
201
+ }
202
+ try {
203
+ // rc files may have changed while asking
204
+ if (!o.yes)
205
+ plan = await planPathSetup(po);
206
+ const r = await applyPathSetup(plan);
207
+ for (const line of pathSetupLines(r, source, home))
208
+ write(line);
209
+ }
210
+ catch (er) {
211
+ write(`⚠️ Could not add it to your PATH: ${asError(er).message}`);
212
+ }
213
+ };
@@ -0,0 +1,36 @@
1
+ import type { ShellOptions } from '../shell.ts';
2
+ export type PosixShell = 'bash' | 'zsh' | 'fish' | 'sh';
3
+ export declare const blockStart = "# vlt";
4
+ export declare const blockEnd = "# vlt end";
5
+ export type FileAction = 'create' | 'append' | 'replace' | 'unchanged';
6
+ export type Owner = {
7
+ uid: number;
8
+ gid: number;
9
+ };
10
+ export type FileChange = {
11
+ /** file to write: the rc file, or its symlink target */
12
+ file: string;
13
+ kind: 'env' | 'rc';
14
+ action: FileAction;
15
+ /** bytes: rc files may not be UTF-8 */
16
+ content: Buffer;
17
+ /** rc content before an append/replace, backed up */
18
+ previous?: Buffer;
19
+ mode?: number;
20
+ /** of the rc file, or of the dir a new one goes in */
21
+ owner?: Owner;
22
+ };
23
+ /** rc files that load the env file in new `shell` sessions */
24
+ export declare const rcFiles: (shell: PosixShell, { env, platform, home, }?: ShellOptions) => string[];
25
+ /** the marked section loading the env file */
26
+ export declare const renderBlock: (shell: PosixShell, globalDir: string, o?: ShellOptions) => string;
27
+ /** the last complete vlt section in `content` */
28
+ export declare const findBlock: (content: string) => {
29
+ start: number;
30
+ end: number;
31
+ text: string;
32
+ } | undefined;
33
+ /** env files are vlt's own: rewritten when different */
34
+ export declare const planEnvFile: (file: string, text: string) => Promise<FileChange>;
35
+ /** add or update the vlt section of an rc file */
36
+ export declare const planRcFile: (file: string, block: string) => Promise<FileChange>;
@@ -0,0 +1,158 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { lstat, readFile, realpath, stat } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, isAbsolute, resolve } from 'node:path';
5
+ import { fishPath, posixPath } from "../shell.js";
6
+ import { envFile, envFishFile } from "./env.js";
7
+ export const blockStart = '# vlt';
8
+ export const blockEnd = '# vlt end';
9
+ const abs = (v) => v && isAbsolute(v) ? v : undefined;
10
+ const isENOENT = (er) => er.code === 'ENOENT';
11
+ const ownerOf = (st) => ({ uid: st.uid, gid: st.gid });
12
+ // owner of the nearest existing dir
13
+ const dirOwner = async (dir) => {
14
+ let st = await stat(dir).catch(() => undefined);
15
+ while (!st && dirname(dir) !== dir) {
16
+ dir = dirname(dir);
17
+ st = await stat(dir).catch(() => undefined);
18
+ }
19
+ return st && ownerOf(st);
20
+ };
21
+ // strings with one char per byte, so offsets are byte offsets
22
+ const latin1 = (s) => Buffer.from(s).toString('latin1');
23
+ const bytes = (s) => Buffer.from(s, 'latin1');
24
+ /** rc files that load the env file in new `shell` sessions */
25
+ export const rcFiles = (shell, { env = process.env, platform = process.platform, home = homedir(), } = {}) => {
26
+ const h = (f) => resolve(home, f);
27
+ switch (shell) {
28
+ case 'zsh':
29
+ // after path_helper, in every interactive zsh
30
+ return [resolve(abs(env.ZDOTDIR) ?? home, '.zshrc')];
31
+ case 'fish':
32
+ return [
33
+ resolve(abs(env.XDG_CONFIG_HOME) ?? h('.config'), 'fish/conf.d/vlt.fish'),
34
+ ];
35
+ case 'sh':
36
+ return [resolve(abs(env.ENV) ?? h('.profile'))];
37
+ case 'bash': {
38
+ const login = ['.bash_profile', '.bash_login'].map(h);
39
+ const list = [...login, h('.bashrc')].filter(f => existsSync(f));
40
+ // never create .bash_profile: it would shadow .profile.
41
+ // macOS terminals run login bash, which skips .bashrc
42
+ if (!list.length ||
43
+ (platform === 'darwin' && !login.some(f => list.includes(f)))) {
44
+ list.push(h('.profile'));
45
+ }
46
+ return list;
47
+ }
48
+ }
49
+ };
50
+ /** the marked section loading the env file */
51
+ export const renderBlock = (shell, globalDir, o = {}) => {
52
+ let line;
53
+ if (shell === 'fish') {
54
+ const f = fishPath(envFishFile(globalDir), o);
55
+ line = `test -f ${f}; and source ${f}`;
56
+ }
57
+ else {
58
+ // status 0 when missing: `set -e` callers source rc files
59
+ const f = posixPath(envFile(globalDir), o);
60
+ line = `if [ -f ${f} ]; then . ${f}; fi`;
61
+ }
62
+ return `${blockStart}\n${line}\n${blockEnd}`;
63
+ };
64
+ const trimEnd = (line) => line.replace(/[\r \t]+$/, '');
65
+ /** the last complete vlt section in `content` */
66
+ export const findBlock = (content) => {
67
+ let start;
68
+ let found;
69
+ let offset = 0;
70
+ for (const raw of content.split('\n')) {
71
+ const line = trimEnd(raw);
72
+ if (line === blockStart)
73
+ start = offset;
74
+ else if (line === blockEnd && start !== undefined) {
75
+ found = { start, end: offset + line.length };
76
+ start = undefined;
77
+ }
78
+ offset += raw.length + 1;
79
+ }
80
+ if (!found)
81
+ return;
82
+ const text = content
83
+ .slice(found.start, found.end)
84
+ .split('\n')
85
+ .map(trimEnd)
86
+ .join('\n');
87
+ return { ...found, text };
88
+ };
89
+ /** env files are vlt's own: rewritten when different */
90
+ export const planEnvFile = async (file, text) => {
91
+ const content = Buffer.from(text);
92
+ const previous = await readFile(file).catch((er) => {
93
+ if (isENOENT(er))
94
+ return undefined;
95
+ throw er;
96
+ });
97
+ return {
98
+ file,
99
+ kind: 'env',
100
+ content,
101
+ action: previous === undefined ? 'create'
102
+ : previous.equals(content) ? 'unchanged'
103
+ : 'replace',
104
+ };
105
+ };
106
+ /** add or update the vlt section of an rc file */
107
+ export const planRcFile = async (file, block) => {
108
+ const lst = await lstat(file).catch((er) => {
109
+ if (isENOENT(er))
110
+ return undefined;
111
+ throw er;
112
+ });
113
+ if (!lst) {
114
+ return {
115
+ file,
116
+ kind: 'rc',
117
+ action: 'create',
118
+ content: Buffer.from(block + '\n'),
119
+ owner: await dirOwner(dirname(file)),
120
+ };
121
+ }
122
+ if (lst.isSymbolicLink()) {
123
+ // dotfile managers: edit the target, keep the link
124
+ file = await realpath(file);
125
+ }
126
+ const st = await stat(file);
127
+ const previous = await readFile(file);
128
+ const text = previous.toString('latin1');
129
+ const blk = latin1(block);
130
+ const base = {
131
+ file,
132
+ kind: 'rc',
133
+ mode: st.mode & 0o7777,
134
+ owner: ownerOf(st),
135
+ };
136
+ const b = findBlock(text);
137
+ if (!b) {
138
+ const sep = text === '' ? ''
139
+ : text.endsWith('\n') ? '\n'
140
+ : '\n\n';
141
+ return {
142
+ ...base,
143
+ action: 'append',
144
+ content: bytes(text + sep + blk + '\n'),
145
+ previous,
146
+ };
147
+ }
148
+ if (b.text === blk) {
149
+ return { ...base, action: 'unchanged', content: previous };
150
+ }
151
+ // the section is vlt's own: an old or edited one is replaced
152
+ return {
153
+ ...base,
154
+ action: 'replace',
155
+ content: bytes(text.slice(0, b.start) + blk + text.slice(b.end)),
156
+ previous,
157
+ };
158
+ };
@@ -0,0 +1,39 @@
1
+ export declare const userEnvKey = "HKCU\\Environment";
2
+ /** how the user Path is named in messages */
3
+ export declare const userEnvLabel = "your Windows user environment";
4
+ /** where users edit their Path by hand */
5
+ export declare const editPathHint = "Start > \"Edit environment variables for your account\" > Path";
6
+ export type WindowsOptions = {
7
+ env?: NodeJS.ProcessEnv;
8
+ /** key holding Path; tests use a scratch key */
9
+ key?: string;
10
+ };
11
+ /** REG_SZ, REG_EXPAND_SZ, else the raw .reg type (dword, hex(7), …) */
12
+ export type RegValue = {
13
+ type: string;
14
+ data: string;
15
+ };
16
+ export type WindowsPathChange = {
17
+ key: string;
18
+ /** canonical dir, written as is */
19
+ dir: string;
20
+ action: 'create' | 'prepend' | 'unchanged';
21
+ /** Path before; undefined when not set */
22
+ previous?: string;
23
+ /** Path after */
24
+ data: string;
25
+ };
26
+ /** `name` in the first key of a `reg export` file */
27
+ export declare const parseRegExport: (text: string, name: string) => RegValue | undefined;
28
+ /** `%VAR%` expanded, names case-insensitive */
29
+ export declare const expandEnv: (s: string, env?: NodeJS.ProcessEnv) => string;
30
+ /** Path with `dir` first, unless already in it */
31
+ export declare const addPathEntry: (value: RegValue | undefined, dir: string, env?: NodeJS.ProcessEnv) => Pick<WindowsPathChange, "action" | "previous" | "data">;
32
+ /** the user Path value, undefined when not set */
33
+ export declare const readUserPath: ({ env, key, }?: WindowsOptions) => Promise<RegValue | undefined>;
34
+ /** what adding `dir` to the user Path would change */
35
+ export declare const planWindowsPath: (dir: string, { env, key }?: WindowsOptions) => Promise<WindowsPathChange>;
36
+ /** write the planned Path, then tell running programs */
37
+ export declare const applyWindowsPath: (c: WindowsPathChange, { env }?: {
38
+ env?: NodeJS.ProcessEnv;
39
+ }) => Promise<void>;
@@ -0,0 +1,151 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { promiseSpawn } from '@vltpkg/promise-spawn';
3
+ import { asError } from '@vltpkg/types';
4
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { join, win32 } from 'node:path';
7
+ export const userEnvKey = 'HKCU\\Environment';
8
+ /** how the user Path is named in messages */
9
+ export const userEnvLabel = 'your Windows user environment';
10
+ /** where users edit their Path by hand */
11
+ export const editPathHint = 'Start > "Edit environment variables for your account" > Path';
12
+ const refreshVar = 'VLT_PATH_REFRESH';
13
+ const system32 = (env) => win32.join([env.SystemRoot, env.windir].find(d => d && win32.isAbsolute(d)) ?? 'C:\\Windows', 'System32');
14
+ // absolute: never a reg.exe from the cwd
15
+ const run = (env, exe, args) => promiseSpawn(win32.join(system32(env), exe), args, {
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ windowsHide: true,
18
+ });
19
+ // reg's stderr, else the spawn error
20
+ const detail = (er) => {
21
+ const e = asError(er);
22
+ const c = e.cause;
23
+ return (typeof c?.stderr === 'string' && c.stderr ? c.stderr
24
+ : c?.cause ? asError(c.cause).message
25
+ : e.message);
26
+ };
27
+ const fail = (what, key, why, hint, cause) => error(`Could not ${what} your user Path (${key}): ${why}\n ${hint}`, {
28
+ code: 'ECONFIG',
29
+ cause,
30
+ });
31
+ const addIt = (dir) => `Add ${dir} yourself: ${editPathHint}`;
32
+ const norm = (p, env) => win32
33
+ .normalize(expandEnv(p.trim().replace(/^"(.*)"$/, '$1'), env))
34
+ .replace(/[\\/]+$/, '')
35
+ .toLowerCase();
36
+ // .reg strings escape \ and "
37
+ const unesc = (s) => s.replace(/\\([\s\S])/g, '$1');
38
+ /** `name` in the first key of a `reg export` file */
39
+ export const parseRegExport = (text, name) => {
40
+ // hex values wrap as `,\` + newline + indent
41
+ const lines = text.replace(/,\\\r?\n */g, ',').split(/\r?\n/);
42
+ let inKey = false;
43
+ for (const line of lines) {
44
+ if (line.startsWith('[')) {
45
+ // subkeys follow
46
+ if (inKey)
47
+ return undefined;
48
+ inKey = true;
49
+ continue;
50
+ }
51
+ // [\s\S]: data may hold \r, U+2028, U+2029
52
+ const [, n = '', v = ''] = /^"((?:[^"\\]|\\[\s\S])*)"=([\s\S]*)$/.exec(line) ?? [];
53
+ if (!inKey || unesc(n).toLowerCase() !== name.toLowerCase()) {
54
+ continue;
55
+ }
56
+ const [, sz] = /^"((?:[^"\\]|\\[\s\S])*)"$/.exec(v) ?? [];
57
+ if (sz !== undefined)
58
+ return { type: 'REG_SZ', data: unesc(sz) };
59
+ const [, hex] = /^hex\(2\):([\da-f,]*)$/i.exec(v) ?? [];
60
+ if (hex !== undefined) {
61
+ const bytes = hex
62
+ .split(',')
63
+ .filter(Boolean)
64
+ .map(h => parseInt(h, 16));
65
+ return {
66
+ type: 'REG_EXPAND_SZ',
67
+ data: Buffer.from(bytes)
68
+ .toString('utf16le')
69
+ .replace(/\0+$/, ''),
70
+ };
71
+ }
72
+ return { type: v.replace(/:[\s\S]*$/, ''), data: '' };
73
+ }
74
+ return undefined;
75
+ };
76
+ /** `%VAR%` expanded, names case-insensitive */
77
+ export const expandEnv = (s, env = process.env) => s.replace(/%([^%]+)%/g, (m, n) => Object.entries(env).find(([k]) => k.toLowerCase() === n.toLowerCase())?.[1] ?? m);
78
+ /** Path with `dir` first, unless already in it */
79
+ export const addPathEntry = (value, dir, env = process.env) => {
80
+ if (!value)
81
+ return { action: 'create', data: dir };
82
+ const previous = value.data;
83
+ const d = norm(dir, env);
84
+ if (previous.split(';').some(e => e.trim() && norm(e, env) === d)) {
85
+ return { action: 'unchanged', previous, data: previous };
86
+ }
87
+ return {
88
+ action: 'prepend',
89
+ previous,
90
+ data: previous ? `${dir};${previous}` : dir,
91
+ };
92
+ };
93
+ /** the user Path value, undefined when not set */
94
+ export const readUserPath = async ({ env = process.env, key = userEnvKey, } = {}) => {
95
+ const d = await mkdtemp(join(tmpdir(), 'vlt-'));
96
+ const file = join(d, 'env.reg');
97
+ try {
98
+ // utf-16 file: reg query prints in the console code page
99
+ await run(env, 'reg.exe', ['export', key, file, '/y']);
100
+ const v = parseRegExport((await readFile(file)).toString('utf16le'), 'Path');
101
+ // lone surrogate or NUL: would not survive the write
102
+ if (v && /[\p{Cs}\0]/u.test(v.data)) {
103
+ throw new Error('it has characters vlt could not read');
104
+ }
105
+ return v;
106
+ }
107
+ finally {
108
+ // AV may hold the new file
109
+ await rm(d, {
110
+ recursive: true,
111
+ force: true,
112
+ maxRetries: 3,
113
+ }).catch(() => { });
114
+ }
115
+ };
116
+ /** what adding `dir` to the user Path would change */
117
+ export const planWindowsPath = async (dir, { env = process.env, key = userEnvKey } = {}) => {
118
+ // would split or expand in Path
119
+ if (/[;%]/.test(dir)) {
120
+ throw fail('update', key, `the folder name has ";" or "%"`, 'Set --global-bin-dir to a folder without them');
121
+ }
122
+ const value = await readUserPath({ env, key }).catch((er) => {
123
+ throw fail('read', key, detail(er), addIt(dir), er);
124
+ });
125
+ if (value && !/^REG_(EXPAND_)?SZ$/.test(value.type)) {
126
+ throw fail('update', key, `unexpected value type ${value.type}`, addIt(dir));
127
+ }
128
+ return { key, dir, ...addPathEntry(value, dir, env) };
129
+ };
130
+ /** write the planned Path, then tell running programs */
131
+ export const applyWindowsPath = async (c, { env = process.env } = {}) => {
132
+ await run(env, 'reg.exe', [
133
+ 'add',
134
+ c.key,
135
+ '/v',
136
+ 'Path',
137
+ '/t',
138
+ 'REG_EXPAND_SZ',
139
+ '/d',
140
+ c.data,
141
+ '/f',
142
+ ]).catch((er) => {
143
+ throw fail('update', c.key, detail(er), addIt(c.dir), er);
144
+ });
145
+ if (c.key !== userEnvKey)
146
+ return;
147
+ // setx broadcasts WM_SETTINGCHANGE: new terminals see the new Path
148
+ await run(env, 'setx.exe', [refreshVar, '1'])
149
+ .then(() => run(env, 'reg.exe', ['delete', c.key, '/v', refreshVar, '/f']))
150
+ .catch(() => { });
151
+ };
@@ -0,0 +1,18 @@
1
+ export type Shell = 'bash' | 'zsh' | 'fish' | 'sh' | 'nu' | 'pwsh' | 'cmd' | 'unknown';
2
+ export type ShellOptions = {
3
+ env?: NodeJS.ProcessEnv;
4
+ platform?: NodeJS.Platform;
5
+ home?: string;
6
+ };
7
+ /** `~/x` for a path under the home dir */
8
+ export declare const prettyPath: (path: string, home?: string, win32?: boolean) => string;
9
+ /** the user's shell, from `SHELL`, or a guess on win32 */
10
+ export declare const detectShell: (env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => Shell;
11
+ /** `dir` double-quoted for a POSIX shell, `$HOME` late-bound */
12
+ export declare const posixPath: (dir: string, o?: ShellOptions) => string;
13
+ /** `dir` double-quoted for fish, `$HOME` late-bound */
14
+ export declare const fishPath: (dir: string, o?: ShellOptions) => string;
15
+ /** the line that adds `dir` to the `PATH` in `shell` */
16
+ export declare const shellPathCommand: (shell: Shell, dir: string, o?: ShellOptions) => string;
17
+ /** rc file to add the `PATH` line to, for display */
18
+ export declare const shellRcFile: (shell: Shell, o?: ShellOptions) => string | undefined;