@vltpkg/cli-sdk 1.0.10 → 1.1.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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * With `--save-config`, save registry and git host config given on the
3
+ * command line or via `VLT_*` env to `install` / `update`, so that the
4
+ * config files plus the lockfile can reproduce the install.
5
+ * @module
6
+ */
7
+ import type { WhichConfig } from '@vltpkg/vlt-json';
8
+ import type { ConfigFileData, LoadedConfig } from './config/index.ts';
9
+ /** The config fields that change how specs are parsed. */
10
+ export declare const specConfigFields: readonly ["registry", "registries", "default-registry-alias", "scoped-registries", "jsr-registries", "git-hosts", "git-host-archives"];
11
+ export type SpecConfigPersistPlan = {
12
+ which: WhichConfig;
13
+ values: ConfigFileData;
14
+ };
15
+ /**
16
+ * The spec config set on the cli / env that the target config file (the
17
+ * project `vlt.json`, unless `--config=user`) should get. `undefined`
18
+ * unless `--save-config` is set. Values already in effect from a config
19
+ * file or builtin are left alone. Throws `ECONFIG` when the target file
20
+ * (or its `command.<cmd>` block) sets a value to something else, or a
21
+ * value is invalid.
22
+ *
23
+ * Call before installing, write the result after it succeeds.
24
+ */
25
+ export declare const planSpecConfigPersist: (conf: LoadedConfig) => SpecConfigPersistPlan | undefined;
26
+ /** `field=value` / `field.key=value` lines for a plan's values. */
27
+ export declare const persistedEntries: (values: ConfigFileData) => string[];
@@ -0,0 +1,212 @@
1
+ /**
2
+ * With `--save-config`, save registry and git host config given on the
3
+ * command line or via `VLT_*` env to `install` / `update`, so that the
4
+ * config files plus the lockfile can reproduce the install.
5
+ * @module
6
+ */
7
+ import { error } from '@vltpkg/error-cause';
8
+ import { defaultGitHostArchives, defaultGitHosts, defaultJsrRegistries, defaultRegistries, defaultRegistryName, } from '@vltpkg/spec';
9
+ import { isObject } from '@vltpkg/types';
10
+ import { find } from '@vltpkg/vlt-json';
11
+ import { assertRegistryKeys, configWriteTarget, isRecordField, pairsToRecords, } from "./config/index.js";
12
+ import { getCommand } from "./config/definition.js";
13
+ import { registrySelectionFields, registrySelectors, } from "./config/merge-layers.js";
14
+ import { normalizeRegistryURL } from "./require-registry.js";
15
+ /** The config fields that change how specs are parsed. */
16
+ export const specConfigFields = [
17
+ 'registry',
18
+ 'registries',
19
+ 'default-registry-alias',
20
+ 'scoped-registries',
21
+ 'jsr-registries',
22
+ 'git-hosts',
23
+ 'git-host-archives',
24
+ ];
25
+ const builtins = {
26
+ registries: defaultRegistries,
27
+ 'jsr-registries': defaultJsrRegistries,
28
+ 'git-hosts': defaultGitHosts,
29
+ 'git-host-archives': defaultGitHostArchives,
30
+ };
31
+ // git host templates are not urls
32
+ const urlFields = new Set([
33
+ 'registry',
34
+ 'registries',
35
+ 'scoped-registries',
36
+ 'jsr-registries',
37
+ ]);
38
+ const norm = (field, v) => urlFields.has(field) ? normalizeRegistryURL(v) : v;
39
+ // printed values end up in CI logs, and a url can carry a token
40
+ const redact = (value) => {
41
+ if (!URL.canParse(value))
42
+ return value;
43
+ const url = new URL(value);
44
+ if (!url.username && !url.password)
45
+ return value;
46
+ url.username = '';
47
+ url.password = '';
48
+ return url.href.replace('://', '://***@');
49
+ };
50
+ // a layer's (or command block's) value, `null` meaning absent
51
+ const fromLayer = (layer, field, key) => {
52
+ const v = layer?.[field];
53
+ const r = key === undefined ? v
54
+ : v && typeof v === 'object' ? v[key]
55
+ : undefined;
56
+ return typeof r === 'string' ? r : undefined;
57
+ };
58
+ // set to `null`, removing the value from outer layers
59
+ const isNulled = (layer, field, key) => {
60
+ const v = layer?.[field];
61
+ return key === undefined ?
62
+ v === null
63
+ : isObject(v) && v[key] === null;
64
+ };
65
+ const conflictError = (field, key, current, wanted, file, block) => {
66
+ const name = (block ? `command.${block}.` : '') +
67
+ (key === undefined ? field : `${field}.${key}`);
68
+ const cur = redact(current);
69
+ const flag = key === undefined ?
70
+ `--${field}=${cur}`
71
+ : `--${field} ${key}=${cur}`;
72
+ return error([
73
+ `${name} is already set to ${cur} in ${file}.`,
74
+ '',
75
+ `Pass \`${flag}\`${key === undefined ? '' : ', use another name,'} or run`,
76
+ block ?
77
+ '`vlt config edit` to change it first.'
78
+ : `\`vlt config set ${name}=${redact(wanted)}\` first.`,
79
+ ].join('\n'), { code: 'ECONFIG', found: wanted, wanted: current });
80
+ };
81
+ const ownsSelection = (layer) => registrySelectionFields.some(f => f in (layer ?? {}));
82
+ /**
83
+ * The spec config set on the cli / env that the target config file (the
84
+ * project `vlt.json`, unless `--config=user`) should get. `undefined`
85
+ * unless `--save-config` is set. Values already in effect from a config
86
+ * file or builtin are left alone. Throws `ECONFIG` when the target file
87
+ * (or its `command.<cmd>` block) sets a value to something else, or a
88
+ * value is invalid.
89
+ *
90
+ * Call before installing, write the result after it succeeds.
91
+ */
92
+ export const planSpecConfigPersist = (conf) => {
93
+ if (!conf.get('save-config'))
94
+ return undefined;
95
+ const explicit = pairsToRecords(conf.explicit);
96
+ if (!specConfigFields.some(f => f in explicit))
97
+ return undefined;
98
+ const which = configWriteTarget(conf, 'project');
99
+ const target = conf.layers[which];
100
+ const other = conf.layers[which === 'project' ? 'user' : 'project'];
101
+ // a project that owns selection drops the user's selectors
102
+ const otherSelectors = which === 'user' || !ownsSelection(target);
103
+ // a command block beats the top level for its command (names like
104
+ // `add` canonicalized, last wins). a record field in the block replaces
105
+ // the top level one, so keys it lacks go in the block.
106
+ const blocks = (target?.command ?? {});
107
+ const blockName = Object.keys(blocks)
108
+ .filter(n => getCommand(n) === conf.command)
109
+ .at(-1);
110
+ const block = blockName ? blocks[blockName] : undefined;
111
+ const staged = {};
112
+ for (const field of specConfigFields) {
113
+ const v = explicit[field];
114
+ if (v === undefined)
115
+ continue;
116
+ const entries = isRecordField(field) ?
117
+ Object.entries(v)
118
+ : [[undefined, v]];
119
+ for (const [key, raw] of entries) {
120
+ if (key === '') {
121
+ throw error(`${field} has an entry with no name.`, {
122
+ code: 'ECONFIG',
123
+ found: `=${raw}`,
124
+ wanted: `<name>=${raw}`,
125
+ });
126
+ }
127
+ if (key !== undefined && field === 'registries') {
128
+ assertRegistryKeys({ [key]: raw }, `--${field}`);
129
+ }
130
+ if (key !== undefined && !raw) {
131
+ throw error(`${field}.${key} has no value.`, {
132
+ code: 'ECONFIG',
133
+ found: key,
134
+ wanted: `${key}=<value>`,
135
+ });
136
+ }
137
+ // eg an empty VLT_REGISTRY
138
+ if (!raw)
139
+ continue;
140
+ const value = norm(field, raw);
141
+ const inBlock = fromLayer(block, field, key);
142
+ if (inBlock !== undefined) {
143
+ if (norm(field, inBlock) === value)
144
+ continue;
145
+ throw conflictError(field, key, inBlock, value, find(which), blockName);
146
+ }
147
+ const toBlock = key !== undefined && isObject(block?.[field]) ?
148
+ blockName
149
+ : undefined;
150
+ const current = toBlock === undefined ?
151
+ fromLayer(target, field, key)
152
+ : undefined;
153
+ if (current !== undefined) {
154
+ if (norm(field, current) === value)
155
+ continue;
156
+ throw conflictError(field, key, current, value, find(which));
157
+ }
158
+ // in effect without saving: the other file's value, unless the
159
+ // target drops it, else the builtin
160
+ const inOther = (toBlock !== undefined ||
161
+ isNulled(target, field, key) ||
162
+ (key === undefined && !otherSelectors)) ?
163
+ undefined
164
+ : fromLayer(other, field, key);
165
+ const builtin = key !== undefined ? builtins[field]?.[key]
166
+ : field === 'default-registry-alias' ? defaultRegistryName
167
+ : undefined;
168
+ const inEffect = inOther ?? builtin;
169
+ if (inEffect !== undefined && norm(field, inEffect) === value) {
170
+ continue;
171
+ }
172
+ let dest = staged;
173
+ if (toBlock !== undefined) {
174
+ const cmds = (staged.command ??= {});
175
+ dest = cmds[toBlock] ??= {};
176
+ }
177
+ if (key === undefined)
178
+ dest[field] = value;
179
+ else {
180
+ const rec = (dest[field] ??= {});
181
+ rec[key] = value;
182
+ }
183
+ }
184
+ }
185
+ if (!Object.keys(staged).length)
186
+ return undefined;
187
+ // a project layer that sets any selection field owns selection, which
188
+ // would drop the user's registry / default-registry-alias. keep them,
189
+ // with the alias url, so the project file is self contained.
190
+ const { user } = conf.layers;
191
+ if (which === 'project' &&
192
+ ownsSelection(staged) &&
193
+ !ownsSelection(target)) {
194
+ for (const f of registrySelectors) {
195
+ const v = fromLayer(user, f);
196
+ if (v !== undefined && !(f in staged))
197
+ staged[f] = v;
198
+ }
199
+ const alias = staged['default-registry-alias'];
200
+ const url = alias && fromLayer(user, 'registries', alias);
201
+ const regs = (staged.registries ?? {});
202
+ if (url && !(alias in regs)) {
203
+ staged.registries = { ...regs, [alias]: url };
204
+ }
205
+ }
206
+ return { which, values: staged };
207
+ };
208
+ const entries = (path, v) => isObject(v) ?
209
+ Object.entries(v).flatMap(([k, x]) => entries(`${path}.${k}`, x))
210
+ : [`${path}=${redact(String(v))}`];
211
+ /** `field=value` / `field.key=value` lines for a plan's values. */
212
+ export const persistedEntries = (values) => Object.entries(values).flatMap(([f, v]) => entries(f, v));
package/dist/print-err.js CHANGED
@@ -167,15 +167,22 @@ const printCode = (err, usage, stderr, format) => {
167
167
  return { file: true };
168
168
  }
169
169
  case 'ENEEDAUTH': {
170
- const { url } = err.cause;
170
+ const { url, method, status } = err.cause;
171
171
  stderr(`Authentication Error: ${err.message}`);
172
172
  if (url) {
173
173
  stderr(indent(`URL: ${formatURL(url, format)}`));
174
174
  }
175
+ if (method) {
176
+ stderr(indent(`Method: ${format(method)}`));
177
+ }
178
+ // a 401 and a 403 mean very different things to a user
179
+ if (status) {
180
+ stderr(indent(`Status: ${format(status)}`));
181
+ }
175
182
  return {};
176
183
  }
177
184
  case 'EREQUEST': {
178
- const { url, method } = err.cause;
185
+ const { url, method, status } = err.cause;
179
186
  const { code, syscall } = err.cause.cause ?? {};
180
187
  stderr(`Request Error: ${err.message}`);
181
188
  if (code) {
@@ -190,6 +197,11 @@ const printCode = (err, usage, stderr, format) => {
190
197
  if (method) {
191
198
  stderr(indent(`Method: ${format(method)}`));
192
199
  }
200
+ // set whenever the registry actually answered; its absence is
201
+ // what distinguishes a transport failure from a response.
202
+ if (status) {
203
+ stderr(indent(`Status: ${format(status)}`));
204
+ }
193
205
  return { file: true };
194
206
  }
195
207
  case 'EQUERY': {
@@ -5,6 +5,7 @@ import { reloadConfig } from "./reload-config.js";
5
5
  import { actual, createVirtualRoot } from '@vltpkg/graph';
6
6
  import { SecurityArchive } from '@vltpkg/security-archive';
7
7
  import { error } from '@vltpkg/error-cause';
8
+ import { isVltInstalled } from "./is-vlt-installed.js";
8
9
  // In restricted environments (like locked-down Codespaces),
9
10
  // homedir() might fail. Fall back to parent directory.
10
11
  let foundHome;
@@ -14,11 +15,6 @@ try {
14
15
  }
15
16
  catch { }
16
17
  const home = foundHome ?? posix.dirname(posix.format(parse(process.cwd())));
17
- const isVltInstalled = (folder) => !!folder.resolve('node_modules/.vlt').lstatSync()?.isDirectory() ||
18
- !!folder
19
- .resolve('node_modules/.vlt-lock.json')
20
- .lstatSync()
21
- ?.isFile();
22
18
  /**
23
19
  * Generates possible project keys for a given folder.
24
20
  */
@@ -96,7 +92,7 @@ export const createHostContextsMap = async (conf) => {
96
92
  try {
97
93
  const config = await reloadConfig(folder.fullpath());
98
94
  // only include projects that are vlt-installed
99
- if (!isVltInstalled(folder)) {
95
+ if (!isVltInstalled(folder.fullpath())) {
100
96
  continue;
101
97
  }
102
98
  // load each individual graph
@@ -5,6 +5,18 @@ import type { LoadedConfig } from './config/index.ts';
5
5
  * everywhere a registry is required but missing.
6
6
  */
7
7
  export declare const missingRegistryError: () => Error;
8
+ /** Ensure a registry URL ends with a single trailing slash. */
9
+ export declare const normalizeRegistryURL: (url: string) => string;
10
+ /**
11
+ * A spec used a `name:` prefix that no registry alias or git host
12
+ * defines.
13
+ */
14
+ export declare const unknownSpecPrefixError: (prefix: string, spec: string, validOptions?: unknown[]) => Error;
15
+ /**
16
+ * Turn a spec parse error about an unknown `name:` prefix into
17
+ * {@link unknownSpecPrefixError}. Other values are returned as is.
18
+ */
19
+ export declare const asUnknownSpecPrefix: (er: unknown) => unknown;
8
20
  /**
9
21
  * Install-related commands need the alias that bare specs resolve
10
22
  * through (not merely some other configured registry) so packages and
@@ -1,4 +1,5 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
+ import { isErrorWithCause, isObject } from '@vltpkg/types';
2
3
  import { defaultRegistries, defaultRegistryName } from '@vltpkg/spec';
3
4
  import { selectRegistry } from "./select-registry.js";
4
5
  /**
@@ -12,6 +13,34 @@ export const missingRegistryError = () => error([
12
13
  '',
13
14
  'See https://docs.vlt.sh/cli for other ways to set a registry.',
14
15
  ].join('\n'), { code: 'ECONFIG' });
16
+ /** Ensure a registry URL ends with a single trailing slash. */
17
+ export const normalizeRegistryURL = (url) => url.replace(/\/*$/, '/');
18
+ /**
19
+ * A spec used a `name:` prefix that no registry alias or git host
20
+ * defines.
21
+ */
22
+ export const unknownSpecPrefixError = (prefix, spec, validOptions) => error([
23
+ `Unknown spec prefix "${prefix}:" in "${spec}".`,
24
+ '',
25
+ `Define it with \`--registries ${prefix}=<url>\` or`,
26
+ `\`--git-hosts ${prefix}=<template>\` (add \`--save-config\` to keep`,
27
+ `it in vlt.json), or run \`vlt config set registries.${prefix}=<url>\`.`,
28
+ ].join('\n'), { code: 'ECONFIG', found: `${prefix}:`, validOptions });
29
+ /**
30
+ * Turn a spec parse error about an unknown `name:` prefix into
31
+ * {@link unknownSpecPrefixError}. Other values are returned as is.
32
+ */
33
+ export const asUnknownSpecPrefix = (er) => {
34
+ const { found, spec, validOptions } = (isErrorWithCause(er) && isObject(er.cause) ?
35
+ er.cause
36
+ : {});
37
+ return (typeof found === 'string' &&
38
+ /^[^:]+:$/.test(found) &&
39
+ typeof spec === 'string' &&
40
+ er.message.startsWith('Protocol ')) ?
41
+ unknownSpecPrefixError(found.slice(0, -1), spec, validOptions)
42
+ : er;
43
+ };
15
44
  /**
16
45
  * Install-related commands need the alias that bare specs resolve
17
46
  * through (not merely some other configured registry) so packages and
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/cli-sdk",
3
3
  "description": "The source for the vlt CLI",
4
- "version": "1.0.10",
4
+ "version": "1.1.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -14,30 +14,30 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@resvg/resvg-wasm": "^2.6.2",
17
- "@vltpkg/config": "1.0.10",
18
- "@vltpkg/dep-id": "1.0.10",
19
- "@vltpkg/dot-prop": "1.0.10",
20
- "@vltpkg/error-cause": "1.0.10",
21
- "@vltpkg/git": "1.0.10",
22
- "@vltpkg/graph": "1.0.10",
23
- "@vltpkg/graph-run": "1.0.10",
24
- "@vltpkg/init": "1.0.10",
25
- "@vltpkg/output": "1.0.10",
26
- "@vltpkg/package-info": "1.0.10",
27
- "@vltpkg/package-json": "1.0.10",
28
- "@vltpkg/promise-spawn": "1.0.10",
29
- "@vltpkg/query": "1.0.10",
30
- "@vltpkg/registry-client": "1.0.10",
31
- "@vltpkg/rollback-remove": "1.0.10",
32
- "@vltpkg/run": "1.0.10",
33
- "@vltpkg/security-archive": "1.0.10",
34
- "@vltpkg/spec": "1.0.10",
35
- "@vltpkg/types": "1.0.10",
36
- "@vltpkg/url-open": "1.0.10",
37
- "@vltpkg/vlt-json": "1.0.10",
38
- "@vltpkg/vlx": "1.0.10",
39
- "@vltpkg/workspaces": "1.0.10",
40
- "@vltpkg/xdg": "1.0.10",
17
+ "@vltpkg/config": "1.1.1",
18
+ "@vltpkg/dep-id": "1.1.1",
19
+ "@vltpkg/dot-prop": "1.1.1",
20
+ "@vltpkg/error-cause": "1.1.1",
21
+ "@vltpkg/git": "1.1.1",
22
+ "@vltpkg/graph": "1.1.1",
23
+ "@vltpkg/graph-run": "1.1.1",
24
+ "@vltpkg/init": "1.1.1",
25
+ "@vltpkg/output": "1.1.1",
26
+ "@vltpkg/package-info": "1.1.1",
27
+ "@vltpkg/package-json": "1.1.1",
28
+ "@vltpkg/promise-spawn": "1.1.1",
29
+ "@vltpkg/query": "1.1.1",
30
+ "@vltpkg/registry-client": "1.1.1",
31
+ "@vltpkg/rollback-remove": "1.1.1",
32
+ "@vltpkg/run": "1.1.1",
33
+ "@vltpkg/security-archive": "1.1.1",
34
+ "@vltpkg/spec": "1.1.1",
35
+ "@vltpkg/types": "1.1.1",
36
+ "@vltpkg/url-open": "1.1.1",
37
+ "@vltpkg/vlt-json": "1.1.1",
38
+ "@vltpkg/vlx": "1.1.1",
39
+ "@vltpkg/workspaces": "1.1.1",
40
+ "@vltpkg/xdg": "1.1.1",
41
41
  "ansi-to-pre": "^1.0.6",
42
42
  "beautiful-mermaid": "^1.1.3",
43
43
  "chalk": "^5.6.2",
@@ -1,2 +0,0 @@
1
- import type { CacheEntry } from '@vltpkg/registry-client';
2
- export declare const registryErrorMessage: (response: CacheEntry) => string;
@@ -1,29 +0,0 @@
1
- import { STATUS_CODES } from 'node:http';
2
- export const registryErrorMessage = (response) => {
3
- const statusMessage = STATUS_CODES[response.statusCode];
4
- const status = `${response.statusCode}${statusMessage ? ` ${statusMessage}` : ''}`;
5
- let text;
6
- try {
7
- text = response.text().trim();
8
- }
9
- catch {
10
- return status;
11
- }
12
- if (!text)
13
- return status;
14
- let detail = text;
15
- try {
16
- const body = JSON.parse(text);
17
- if (body &&
18
- typeof body === 'object' &&
19
- 'error' in body &&
20
- typeof body.error === 'string' &&
21
- body.error) {
22
- detail = body.error;
23
- }
24
- }
25
- catch {
26
- // A non-JSON response body is itself the most useful error detail.
27
- }
28
- return `${status} — ${detail}`;
29
- };