@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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { GlobalBins } from '../global.ts';
|
|
2
|
+
import type { Shell, ShellOptions } from '../shell.ts';
|
|
3
|
+
import type { FileAction, FileChange } from './posix.ts';
|
|
4
|
+
import type { WindowsPathChange } from './windows.ts';
|
|
5
|
+
export type { FileAction } from './posix.ts';
|
|
6
|
+
/** whether `vlt setup --path` can edit config for `shell` */
|
|
7
|
+
export declare const canSetupPath: (shell: Shell, platform?: NodeJS.Platform) => boolean;
|
|
8
|
+
export type PathSetupOptions = ShellOptions & {
|
|
9
|
+
/** holds the env files and backups */
|
|
10
|
+
globalDir: string;
|
|
11
|
+
/** dir to add to the `PATH` */
|
|
12
|
+
dir: string;
|
|
13
|
+
shell: Shell;
|
|
14
|
+
};
|
|
15
|
+
export type PathSetupPlan = {
|
|
16
|
+
globalDir: string;
|
|
17
|
+
dir: string;
|
|
18
|
+
shell: Shell;
|
|
19
|
+
changes: FileChange[];
|
|
20
|
+
/** win32: user Path change, instead of files */
|
|
21
|
+
registry?: WindowsPathChange;
|
|
22
|
+
/** win32: env the user Path was read with */
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
};
|
|
25
|
+
export type PathSetupFile = {
|
|
26
|
+
file: string;
|
|
27
|
+
kind: 'env' | 'rc' | 'registry';
|
|
28
|
+
action: FileAction | 'prepend';
|
|
29
|
+
};
|
|
30
|
+
export type PathSetupResult = {
|
|
31
|
+
dir: string;
|
|
32
|
+
shell: Shell;
|
|
33
|
+
files: PathSetupFile[];
|
|
34
|
+
backups: string[];
|
|
35
|
+
};
|
|
36
|
+
/** what adding `dir` to the `PATH` of new shells would change */
|
|
37
|
+
export declare const planPathSetup: (o: PathSetupOptions) => Promise<PathSetupPlan>;
|
|
38
|
+
export declare const pathSetupChanged: (plan: PathSetupPlan) => boolean;
|
|
39
|
+
export declare const pathSetupFiles: (plan: PathSetupPlan) => PathSetupFile[];
|
|
40
|
+
/** rc files or the user Path, for display */
|
|
41
|
+
export declare const rcList: (files: PathSetupFile[], home?: string) => string;
|
|
42
|
+
/** write the plan: env files, rc files, user Path, backed up */
|
|
43
|
+
export declare const applyPathSetup: (plan: PathSetupPlan) => Promise<PathSetupResult>;
|
|
44
|
+
/** loads the env file into the current shell (win32: adds `dir`) */
|
|
45
|
+
export declare const sourceCommand: (shell: Shell, globalDir: string, dir: string, o?: ShellOptions) => string;
|
|
46
|
+
export declare const pathSetupLines: (r: PathSetupResult, source: string, home?: string) => string[];
|
|
47
|
+
/** one trimmed answer */
|
|
48
|
+
export declare const ask: (question: string, { input, output, }?: {
|
|
49
|
+
input?: NodeJS.ReadableStream;
|
|
50
|
+
output?: NodeJS.WritableStream;
|
|
51
|
+
}) => Promise<string>;
|
|
52
|
+
export type OfferOptions = ShellOptions & {
|
|
53
|
+
globalDir: string;
|
|
54
|
+
yes?: boolean;
|
|
55
|
+
noModifyPath?: boolean;
|
|
56
|
+
input?: NodeJS.ReadableStream & {
|
|
57
|
+
isTTY?: boolean;
|
|
58
|
+
};
|
|
59
|
+
output?: NodeJS.WritableStream & {
|
|
60
|
+
isTTY?: boolean;
|
|
61
|
+
};
|
|
62
|
+
/** once, before the first prompt or line */
|
|
63
|
+
beforeOutput?: () => void;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* After `install -g` linked into a dir not in the `PATH`: offer, once,
|
|
67
|
+
* in a terminal, to add it to the PATH. Never throws.
|
|
68
|
+
*/
|
|
69
|
+
export declare const offerPathSetup: (g: GlobalBins, o: OfferOptions) => Promise<void>;
|
|
@@ -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>;
|