@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.
@@ -1,11 +1,12 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient } from '@vltpkg/registry-client';
2
+ import { getKC, normalizeRegistryKey, RegistryClient, } from '@vltpkg/registry-client';
3
3
  import { defaultRegistries } from '@vltpkg/spec';
4
4
  import { asError, isErrorWithCause, isObject } from '@vltpkg/types';
5
5
  import { createInterface } from 'node:readline/promises';
6
6
  import { configWriteTarget } from "../config/index.js";
7
7
  import { registrySelectionFields } from "../config/merge-layers.js";
8
8
  import { commandUsage } from "../config/usage.js";
9
+ import { normalizeRegistryURL } from "../require-registry.js";
9
10
  import { stdout } from "../output.js";
10
11
  /** Where users sign up for / log into a vlt.io account. */
11
12
  export const VLT_SIGNUP_URL = 'https://www.vlt.io';
@@ -18,6 +19,27 @@ export const VLT_REGISTRY_BASE = 'https://registry.vlt.io';
18
19
  export const accountRegistries = ['npm', 'main'];
19
20
  /** Build the per-account registry URL for a given registry name. */
20
21
  export const accountRegistryURL = (account, name) => `${VLT_REGISTRY_BASE}/${encodeURIComponent(account)}/${name}/`;
22
+ /**
23
+ * Path-scoped keychain keys do not match sibling registries
24
+ * (`…/<account>/main` vs `…/<account>/npm`). One account token covers
25
+ * both, so when the keychain holds it for only one alias, copy it to
26
+ * the other.
27
+ */
28
+ export const persistAccountTokens = async (account, identity) => {
29
+ // keychain only: getToken() also returns env/runtime tokens
30
+ const kc = getKC(identity);
31
+ const keys = accountRegistries.map(name => normalizeRegistryKey(accountRegistryURL(account, name)));
32
+ const tokens = await Promise.all(keys.map(k => kc.get(k)));
33
+ const token = tokens.find(Boolean);
34
+ if (!token)
35
+ return;
36
+ // fill gaps only: never overwrite an existing (maybe newer) token
37
+ for (const [i, key] of keys.entries()) {
38
+ if (!tokens[i])
39
+ kc.set(key, token);
40
+ }
41
+ await kc.save();
42
+ };
21
43
  /**
22
44
  * The account registries live at `${VLT_REGISTRY_BASE}/<account>/<name>/`, so
23
45
  * an account slug that doesn't exist is a 404 from the login endpoint. Say
@@ -36,8 +58,6 @@ const accountAuthError = (account, er) => (isErrorWithCause(er) &&
36
58
  `organization at ${VLT_SIGNUP_URL}, then run \`vlt setup\` again.`,
37
59
  ].join('\n'), { code: 'ECONFIG', cause: er })
38
60
  : asError(er);
39
- /** Ensure a registry URL ends with a single trailing slash. */
40
- const normalizeRegistryURL = (url) => url.endsWith('/') ? url : `${url}/`;
41
61
  export const views = {
42
62
  human: (result) => {
43
63
  const lines = [
@@ -62,9 +82,10 @@ export const usage = () => commandUsage({
62
82
  that \`vlt install\` and \`vlx\` work out of the box.
63
83
 
64
84
  Sign up or log in at ${VLT_SIGNUP_URL} first. The wizard
65
- authenticates against your account and writes the
66
- \`npm\` and \`main\` registry aliases (and any additional
67
- aliases you add) to your user \`vlt.json\`.`,
85
+ authenticates against your account, stores the token
86
+ for both the \`npm\` and \`main\` registries, and writes
87
+ those aliases (and any additional aliases you add) to
88
+ your user \`vlt.json\`.`,
68
89
  options: {
69
90
  config: {
70
91
  value: '<user | project>',
@@ -129,8 +150,8 @@ export const command = async (conf) => {
129
150
  if (!yes) {
130
151
  const doAuth = await ask('Authenticate with your vlt.io account now? (Y/n) ');
131
152
  if (!/^n/i.test(doAuth)) {
132
- // one token covers every registry on the account, so the browser
133
- // opens once and the token is stored for all of them.
153
+ // one login covers both account registries; the token is stored
154
+ // for each of them so path-scoped keychain lookups match.
134
155
  stdout(`Authenticating your account registries (${accountRegistries.join(', ')})...`);
135
156
  try {
136
157
  await rc.login(accountRegistries.map(name => accountRegistryURL(account, name)));
@@ -168,6 +189,10 @@ export const command = async (conf) => {
168
189
  }
169
190
  // 6. persist the staged registries (merged, not clobbered)
170
191
  await conf.addConfigToFile(which, { registries });
192
+ // 7. fill a missing `npm`/`main` keychain entry from the other.
193
+ // no-op after login(), which already wrote both. runs after the
194
+ // config write so a keychain failure can't block it.
195
+ await persistAccountTokens(account, conf.options.identity);
171
196
  // writing the user config from inside a project that configures its own
172
197
  // registries has no effect here, so say so rather than looking like it
173
198
  // worked.
@@ -77,7 +77,9 @@ const formatRegistryTokens = (r) => {
77
77
  lines.push(' (skipped remote query — no auth token)');
78
78
  }
79
79
  else if (r.error) {
80
- lines.push(` error: ${r.error}`);
80
+ // messages now carry advice on a second line; keep it indented
81
+ // under the registry heading rather than dedented to column 0.
82
+ lines.push(` error: ${r.error.replaceAll('\n', '\n ')}`);
81
83
  }
82
84
  else if (r.tokens.length === 0) {
83
85
  lines.push(' (no remote tokens found)');
@@ -1,7 +1,6 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient } from '@vltpkg/registry-client';
2
+ import { RegistryClient, assertOk, requestError, } from '@vltpkg/registry-client';
3
3
  import { Spec } from '@vltpkg/spec';
4
- import { asError } from '@vltpkg/types';
5
4
  import { commandUsage } from "../config/usage.js";
6
5
  import { resolveRegistry } from "../require-registry.js";
7
6
  export const needsRegistry = true;
@@ -87,16 +86,18 @@ export const command = async (conf) => {
87
86
  });
88
87
  }
89
88
  catch (err) {
90
- throw error('Failed to fetch package metadata', {
91
- cause: asError(err),
92
- });
93
- }
94
- if (packumentResponse.statusCode !== 200) {
95
- throw error('Package not found on the registry', {
89
+ throw requestError(err, {
90
+ message: 'Failed to fetch package metadata',
96
91
  url: packumentUrl,
97
- response: packumentResponse,
98
92
  });
99
93
  }
94
+ assertOk(packumentResponse, {
95
+ message: 'Failed to fetch package metadata',
96
+ url: packumentUrl,
97
+ advice: statusCode => statusCode === 404 ?
98
+ `${name} was not found on this registry.`
99
+ : undefined,
100
+ });
100
101
  const packument = packumentResponse.json();
101
102
  const versions = packument.versions;
102
103
  const distTags = packument['dist-tags'];
@@ -137,16 +138,17 @@ export const command = async (conf) => {
137
138
  });
138
139
  }
139
140
  catch (err) {
140
- throw error('Failed to unpublish package version', {
141
- cause: asError(err),
142
- });
143
- }
144
- if (response.statusCode < 200 || response.statusCode >= 300) {
145
- throw error('Failed to unpublish package version', {
141
+ throw requestError(err, {
142
+ message: 'Failed to unpublish package version',
146
143
  url: putUrl,
147
- response,
144
+ method: 'PUT',
148
145
  });
149
146
  }
147
+ assertOk(response, {
148
+ message: 'Failed to unpublish package version',
149
+ url: putUrl,
150
+ method: 'PUT',
151
+ });
150
152
  }
151
153
  else {
152
154
  // Unpublish entire package — DELETE the packument
@@ -159,16 +161,18 @@ export const command = async (conf) => {
159
161
  });
160
162
  }
161
163
  catch (err) {
162
- throw error('Failed to fetch package metadata', {
163
- cause: asError(err),
164
- });
165
- }
166
- if (packumentResponse.statusCode !== 200) {
167
- throw error('Package not found on the registry', {
164
+ throw requestError(err, {
165
+ message: 'Failed to fetch package metadata',
168
166
  url: packumentUrl,
169
- response: packumentResponse,
170
167
  });
171
168
  }
169
+ assertOk(packumentResponse, {
170
+ message: 'Failed to fetch package metadata',
171
+ url: packumentUrl,
172
+ advice: statusCode => statusCode === 404 ?
173
+ `${name} was not found on this registry.`
174
+ : undefined,
175
+ });
172
176
  const packument = packumentResponse.json();
173
177
  const deleteUrl = new URL(`${encodedName}/-rev/${packument._rev}`, registryUrl);
174
178
  let response;
@@ -184,16 +188,17 @@ export const command = async (conf) => {
184
188
  });
185
189
  }
186
190
  catch (err) {
187
- throw error('Failed to unpublish package', {
188
- cause: asError(err),
189
- });
190
- }
191
- if (response.statusCode < 200 || response.statusCode >= 300) {
192
- throw error('Failed to unpublish package', {
191
+ throw requestError(err, {
192
+ message: 'Failed to unpublish package',
193
193
  url: deleteUrl,
194
- response,
194
+ method: 'DELETE',
195
195
  });
196
196
  }
197
+ assertOk(response, {
198
+ message: 'Failed to unpublish package',
199
+ url: deleteUrl,
200
+ method: 'DELETE',
201
+ });
197
202
  }
198
203
  return {
199
204
  name,
@@ -5,6 +5,7 @@ export declare const needsNpmRegistry = true;
5
5
  export declare const usage: CommandUsage;
6
6
  export declare const views: {
7
7
  readonly json: (i: InstallResult) => {
8
+ persistedConfig?: import("../persist-spec-config.ts").SpecConfigPersistPlan | undefined;
8
9
  graph: import("@vltpkg/graph").LockfileData;
9
10
  buildQueue?: import("@vltpkg/dep-id").DepID[] | undefined;
10
11
  message?: string | undefined;
@@ -1,6 +1,8 @@
1
1
  import { update } from '@vltpkg/graph';
2
2
  import { error } from '@vltpkg/error-cause';
3
3
  import { commandUsage } from "../config/usage.js";
4
+ import { planSpecConfigPersist } from "../persist-spec-config.js";
5
+ import { asUnknownSpecPrefix } from "../require-registry.js";
4
6
  import { lazyView } from "../view.js";
5
7
  export const needsRegistry = true;
6
8
  export const needsNpmRegistry = true;
@@ -10,6 +12,9 @@ export const usage = () => commandUsage({
10
12
  description: `Update dependencies to their latest in-range versions.
11
13
  Discards the lockfile and resolves dependencies from scratch.`,
12
14
  options: {
15
+ 'save-config': {
16
+ description: 'Save registry and git host options given on the command line or via env (e.g. `--registries name=url`) to the project vlt.json, or the user vlt.json with --config=user.',
17
+ },
13
18
  'allow-scripts': {
14
19
  value: '<query>',
15
20
  description: 'Filter which packages are allowed to run lifecycle scripts using DSS query syntax.',
@@ -25,6 +30,9 @@ export const views = {
25
30
  }
26
31
  : null),
27
32
  graph: i.graph.toJSON(),
33
+ ...(i.persistedConfig ?
34
+ { persistedConfig: i.persistedConfig }
35
+ : null),
28
36
  }),
29
37
  human: lazyView(async () => (await import('./install/reporter.ts')).InstallReporter),
30
38
  };
@@ -35,6 +43,7 @@ export const command = async (conf) => {
35
43
  code: 'EUSAGE',
36
44
  });
37
45
  }
46
+ const persist = planSpecConfigPersist(conf);
38
47
  /* c8 ignore start */
39
48
  const allowScripts = conf.get('allow-scripts') ?
40
49
  String(conf.get('allow-scripts'))
@@ -43,6 +52,14 @@ export const command = async (conf) => {
43
52
  const { buildQueue, graph } = await update({
44
53
  ...conf.options,
45
54
  allowScripts,
55
+ }).catch((er) => {
56
+ throw asUnknownSpecPrefix(er);
46
57
  });
47
- return { buildQueue, graph };
58
+ if (persist)
59
+ await conf.addConfigToFile(persist.which, persist.values);
60
+ return {
61
+ buildQueue,
62
+ graph,
63
+ ...(persist ? { persistedConfig: persist } : null),
64
+ };
48
65
  };
@@ -1,4 +1,5 @@
1
- import { RegistryClient } from '@vltpkg/registry-client';
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { RegistryClient, assertOk } from '@vltpkg/registry-client';
2
3
  import { commandUsage } from "../config/usage.js";
3
4
  import { resolveRegistry } from "../require-registry.js";
4
5
  export const needsRegistry = true;
@@ -27,7 +28,24 @@ export const views = {
27
28
  };
28
29
  export const command = async (conf) => {
29
30
  const rc = new RegistryClient(conf.options);
30
- const response = await rc.request(new URL('-/whoami', await resolveRegistry(conf)), { useCache: false });
31
+ const url = new URL('-/whoami', await resolveRegistry(conf));
32
+ const response = await rc.request(url, { useCache: false });
33
+ assertOk(response, {
34
+ message: 'Failed to look up the current user',
35
+ url,
36
+ advice: statusCode => statusCode === 401 || statusCode === 403 ?
37
+ `Not logged in to ${url.origin}. Run \`vlt login\` and try again.`
38
+ : undefined,
39
+ });
31
40
  const { username } = response.json();
41
+ if (typeof username !== 'string' || !username) {
42
+ throw error('Registry did not report a username', {
43
+ code: 'EREQUEST',
44
+ url,
45
+ wanted: 'a `username` string',
46
+ found: username,
47
+ response,
48
+ });
49
+ }
32
50
  return { username };
33
51
  };
@@ -355,12 +355,16 @@ export declare const definition: import("jackspeak").Jack<{
355
355
  short: string;
356
356
  description: string;
357
357
  };
358
+ 'save-config': {
359
+ description: string;
360
+ };
358
361
  } & {
359
362
  'save-dev': import("jackspeak").ConfigOption<"boolean", false, undefined>;
360
363
  'save-exact': import("jackspeak").ConfigOption<"boolean", false, undefined>;
361
364
  'save-optional': import("jackspeak").ConfigOption<"boolean", false, undefined>;
362
365
  'save-peer': import("jackspeak").ConfigOption<"boolean", false, undefined>;
363
366
  'save-prod': import("jackspeak").ConfigOption<"boolean", false, undefined>;
367
+ 'save-config': import("jackspeak").ConfigOption<"boolean", false, undefined>;
364
368
  } & {
365
369
  'save-prefix': {
366
370
  description: string;
@@ -151,6 +151,10 @@ export const definition = j
151
151
 
152
152
  Command-specific fields may be set in a nested \`command\` object that
153
153
  overrides any options defined at the top level.
154
+
155
+ Object type values given in the environment or on the command line
156
+ are merged key by key over the config files (command line wins over
157
+ environment); other values replace them.
154
158
  `)
155
159
  .flag({
156
160
  color: {
@@ -216,6 +220,12 @@ export const definition = j
216
220
  alias must be configured (e.g. via \`vlt setup\`) before it
217
221
  can be used. The \`gh\` and \`jsr\` aliases have built-in
218
222
  defaults that can be overridden here.
223
+
224
+ Aliases given on the command line or in
225
+ \`VLT_REGISTRIES\` (newline-delimited) merge with the
226
+ configured ones, later sources winning per key. Pass
227
+ \`--save-config\` to \`install\`, \`add\` or
228
+ \`update\` to save them to \`vlt.json\`.
219
229
  `,
220
230
  },
221
231
  'scoped-registries': {
@@ -656,6 +666,17 @@ export const definition = j
656
666
  devDependencies or optionalDependencies, but you want to
657
667
  move it to be a non-optional production dependency.`,
658
668
  },
669
+ 'save-config': {
670
+ description: `Save the registry and git host options given on the
671
+ command line or via \`VLT_*\` env (\`--registry\`,
672
+ \`--registries\`, \`--default-registry-alias\`,
673
+ \`--scoped-registries\`, \`--jsr-registries\`,
674
+ \`--git-hosts\`, \`--git-host-archives\`) to the
675
+ project \`vlt.json\` (or the user one with
676
+ \`--config=user\`) after a successful \`install\`,
677
+ \`add\` or \`update\`. Values a config file already
678
+ has are left alone.`,
679
+ },
659
680
  })
660
681
  .opt({
661
682
  'save-prefix': {
@@ -43,6 +43,11 @@ export type PairsAsRecords = ConfigOptionsNoExtras & {
43
43
  * optional, since a config file only sets what it sets.
44
44
  */
45
45
  export type ConfigFileLayer = Partial<PairsAsRecords>;
46
+ /**
47
+ * `registries` alias names are used as spec prefixes, where `~` is
48
+ * reserved, and an empty name has nothing to prefix with.
49
+ */
50
+ export declare const assertRegistryKeys: (registries: unknown, file: string) => void;
46
51
  export declare const pairsToRecords: (obj: NonNullable<ConfigFileData> | OptionsResults<ConfigDefinitions>) => PairsAsRecords;
47
52
  export declare const recordsToPairs: (obj: RecordPairs) => RecordPairs;
48
53
  export type ConfigDataNoCommand = {
@@ -145,6 +150,15 @@ export declare class Config {
145
150
  * Parse the arguments and set configuration and positionals accordingly.
146
151
  */
147
152
  parse(args?: string[]): this & ParsedConfig;
153
+ /**
154
+ * Values set on the command line or via `VLT_*` env for this run.
155
+ * Record fields hold only those `key=value` pairs, not the ones from
156
+ * config files.
157
+ *
158
+ * `VLT_*` env left as a parent vlt process wrote it only counts for
159
+ * what was explicit in that parent.
160
+ */
161
+ explicit: ConfigData;
148
162
  /**
149
163
  * Get a `key=value` list option value as an object.
150
164
  *
@@ -25,7 +25,7 @@ import { PackageInfoClient } from '@vltpkg/package-info';
25
25
  import { PackageJson } from '@vltpkg/package-json';
26
26
  import { resetCaches } from '@vltpkg/dep-id';
27
27
  import { getOptions } from '@vltpkg/spec';
28
- import { assertRecordStringString, assertRecordStringT, isRecordStringString, } from '@vltpkg/types';
28
+ import { assertRecordStringString, assertRecordStringT, isObject, isRecordStringString, } from '@vltpkg/types';
29
29
  import { find, load, reload, save } from '@vltpkg/vlt-json';
30
30
  import { Monorepo } from '@vltpkg/workspaces';
31
31
  import { readFile, rm, writeFile } from 'node:fs/promises';
@@ -58,7 +58,7 @@ const isRecordFieldValue = (k, v) => Array.isArray(v) &&
58
58
  * `registries` alias names are used as spec prefixes, where `~` is
59
59
  * reserved, and an empty name has nothing to prefix with.
60
60
  */
61
- const assertRegistryKeys = (registries, file) => {
61
+ export const assertRegistryKeys = (registries, file) => {
62
62
  if (!registries || typeof registries !== 'object')
63
63
  return;
64
64
  const obj = Array.isArray(registries) ?
@@ -67,6 +67,7 @@ const assertRegistryKeys = (registries, file) => {
67
67
  for (const key of Object.keys(obj)) {
68
68
  if (key === '' || key.includes('~')) {
69
69
  throw error('Reserved character found in registries name', {
70
+ code: 'ECONFIG',
70
71
  path: file,
71
72
  found: key,
72
73
  });
@@ -110,6 +111,35 @@ export const recordsToPairs = (obj) => {
110
111
  ]));
111
112
  };
112
113
  const kRecord = Symbol('parsed key=value record');
114
+ // jackspeak's env var name for a field
115
+ const envKey = (k) => `VLT_${k.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase()}`;
116
+ // `parse()` writes every resolved value to `VLT_*`, so a nested vlt
117
+ // can't tell inherited env from env set for it. this holds the
118
+ // parent's explicit values and the `VLT_*` env it wrote.
119
+ const kParentEnv = '__VLT_INTERNAL_EXPLICIT';
120
+ const parentEnv = () => {
121
+ try {
122
+ const p = JSON.parse(process.env[kParentEnv] ?? '');
123
+ return (isObject(p) &&
124
+ isObject(p.explicit) &&
125
+ Object.entries(p.explicit).every(([k, v]) => isRecordField(k) ?
126
+ Array.isArray(v) && v.every(x => typeof x === 'string')
127
+ : !isObject(v)) &&
128
+ isRecordStringString(p.env)) ?
129
+ p
130
+ : undefined;
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
135
+ };
136
+ // last pair per key wins, keeping the first one's position
137
+ const dedupePairs = (pairs) => [
138
+ ...new Map(pairs.map(p => {
139
+ const eq = p.indexOf('=');
140
+ return [eq === -1 ? p : p.substring(0, eq), p];
141
+ })).values(),
142
+ ];
113
143
  /**
114
144
  * Class that handles configuration for vlt.
115
145
  *
@@ -220,18 +250,63 @@ export class Config {
220
250
  return this;
221
251
  // Store the original args for potential reload
222
252
  this.#originalArgs = [...args];
253
+ const envKeys = Object.keys(defaultValues).filter(k => process.env[envKey(k)] !== undefined);
254
+ const parent = parentEnv();
255
+ // file layer values, before env replaces them
256
+ const fileValues = this.#defaults();
223
257
  this.jack.loadEnvDefaults();
258
+ const envValues = this.#defaults();
224
259
  const p = this.jack.parseRaw(args);
260
+ const cliKeys = Object.keys(p.values);
225
261
  const fallback = getCommand(p.values['fallback-command']);
226
262
  this.command = getCommand(p.positionals[0]);
227
263
  const cmdOrFallback = this.command ?? fallback;
228
264
  const cmdSpecific = cmdOrFallback && this.commandValues[cmdOrFallback];
229
- if (cmdSpecific) {
230
- this.jack.setConfigValues(recordsToPairs(cmdSpecific));
231
- }
265
+ const cmdPairs = cmdSpecific ? recordsToPairs(cmdSpecific) : {};
266
+ if (cmdSpecific)
267
+ this.jack.setConfigValues(cmdPairs);
232
268
  // ok, applied cmd-specific defaults, do rest of the parse
233
269
  this.jack.applyDefaults(p);
270
+ // what was set on the cli or env for this run. record fields hold
271
+ // only those pairs, env then cli, so later ones win per key.
272
+ const values = p.values;
273
+ const explicit = {};
274
+ for (const k of new Set([...envKeys, ...cliKeys])) {
275
+ const cli = cliKeys.includes(k) ? values[k] : undefined;
276
+ const env = this.#explicitEnv(k, envValues[k], parent);
277
+ if (isRecordField(k)) {
278
+ const pairs = [
279
+ ...(env ?? []),
280
+ ...(cli ?? []),
281
+ ];
282
+ if (pairs.length)
283
+ explicit[k] = dedupePairs(pairs);
284
+ }
285
+ else if (cli !== undefined)
286
+ explicit[k] = cli;
287
+ // a command block beats env for scalars
288
+ else if (env !== undefined && !(k in cmdPairs))
289
+ explicit[k] = env;
290
+ }
291
+ this.explicit = explicit;
292
+ // record fields merge per key: file (or command block) -> env -> cli
293
+ for (const k of recordFields) {
294
+ const env = envKeys.includes(k) ? envValues[k] : [];
295
+ const cli = cliKeys.includes(k) ? values[k] : [];
296
+ const pairs = [...env, ...cli];
297
+ if (!pairs.length)
298
+ continue;
299
+ const base = (k in cmdPairs ? cmdPairs[k] : fileValues[k]) ?? [];
300
+ values[k] = dedupePairs([...base, ...pairs]);
301
+ }
234
302
  this.jack.writeEnv(p);
303
+ process.env[kParentEnv] = JSON.stringify({
304
+ explicit,
305
+ env: Object.fromEntries(Object.keys(defaultValues).flatMap(k => {
306
+ const v = process.env[envKey(k)];
307
+ return v === undefined ? [] : [[envKey(k), v]];
308
+ })),
309
+ });
235
310
  if (this.command)
236
311
  p.positionals.shift();
237
312
  else
@@ -249,6 +324,36 @@ export class Config {
249
324
  /* c8 ignore stop */
250
325
  return this;
251
326
  }
327
+ /**
328
+ * Values set on the command line or via `VLT_*` env for this run.
329
+ * Record fields hold only those `key=value` pairs, not the ones from
330
+ * config files.
331
+ *
332
+ * `VLT_*` env left as a parent vlt process wrote it only counts for
333
+ * what was explicit in that parent.
334
+ */
335
+ explicit = {};
336
+ // a field's explicit env value: all of it at the top level, the new
337
+ // pairs (or changed scalar) when inherited from a parent vlt. once a
338
+ // script changes a record var, the parent's explicit pairs are dropped.
339
+ #explicitEnv(k, value, parent) {
340
+ const cur = process.env[envKey(k)];
341
+ if (cur === undefined)
342
+ return undefined;
343
+ const written = parent?.env[envKey(k)];
344
+ if (cur === written)
345
+ return parent?.explicit[k];
346
+ if (!isRecordField(k) || written === undefined)
347
+ return value;
348
+ const old = new Set(written.split('\n'));
349
+ return value.filter(p => !old.has(p));
350
+ }
351
+ // the current jack defaults, ie config file (+ env, once loaded) values
352
+ #defaults() {
353
+ const d = { values: {}, positionals: [] };
354
+ this.jack.applyDefaults(d);
355
+ return d.values;
356
+ }
252
357
  /**
253
358
  * Get a `key=value` list option value as an object.
254
359
  *
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Whether `node_modules` in `projectRoot` was built by `vlt install`.
3
+ *
4
+ * The store dir `node_modules/.vlt` is only ever created by reify, so it
5
+ * is the primary marker. A dependency-less install creates no store, so
6
+ * the hidden lockfile it writes is accepted as the fallback. Outside
7
+ * of reify, the `actual.load` fs walk and `vlt build` only write that
8
+ * file when the store exists, so a project installed by another client
9
+ * never gains either.
10
+ */
11
+ export declare const isVltInstalled: (projectRoot: string) => boolean;
12
+ /**
13
+ * Throws when `node_modules` was not installed by vlt. Without a vlt
14
+ * install `actual.load` walks a foreign `node_modules`, recognizes
15
+ * nothing and reports every dependency as missing - an empty answer
16
+ * that looks like a real one.
17
+ */
18
+ export declare const assertVltInstalled: (projectRoot: string, command: string) => void;
@@ -0,0 +1,46 @@
1
+ import { lstatSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { error } from '@vltpkg/error-cause';
4
+ // `throwIfNoEntry: false` only covers ENOENT: a `node_modules` that is a
5
+ // file (ENOTDIR) or unreadable (EACCES) still throws, and neither of
6
+ // those is a vlt install.
7
+ const lstat = (path) => {
8
+ try {
9
+ return lstatSync(path, { throwIfNoEntry: false });
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ };
15
+ const isDir = (path) => !!lstat(path)?.isDirectory();
16
+ const isFile = (path) => !!lstat(path)?.isFile();
17
+ /**
18
+ * Whether `node_modules` in `projectRoot` was built by `vlt install`.
19
+ *
20
+ * The store dir `node_modules/.vlt` is only ever created by reify, so it
21
+ * is the primary marker. A dependency-less install creates no store, so
22
+ * the hidden lockfile it writes is accepted as the fallback. Outside
23
+ * of reify, the `actual.load` fs walk and `vlt build` only write that
24
+ * file when the store exists, so a project installed by another client
25
+ * never gains either.
26
+ */
27
+ export const isVltInstalled = (projectRoot) => isDir(resolve(projectRoot, 'node_modules/.vlt')) ||
28
+ isFile(resolve(projectRoot, 'node_modules/.vlt-lock.json'));
29
+ /**
30
+ * Throws when `node_modules` was not installed by vlt. Without a vlt
31
+ * install `actual.load` walks a foreign `node_modules`, recognizes
32
+ * nothing and reports every dependency as missing - an empty answer
33
+ * that looks like a real one.
34
+ */
35
+ export const assertVltInstalled = (projectRoot, command) => {
36
+ if (isVltInstalled(projectRoot))
37
+ return;
38
+ const nodeModules = resolve(projectRoot, 'node_modules');
39
+ const hint = isDir(nodeModules) ?
40
+ `run \`vlt install\` to rebuild it before running \`vlt ${command}\``
41
+ : `run \`vlt install\` before running \`vlt ${command}\``;
42
+ throw error(`No vlt install found in ${nodeModules}\n\n ${hint}`, {
43
+ code: 'EQUERY',
44
+ path: nodeModules,
45
+ });
46
+ };
@@ -2,6 +2,7 @@ import { addKey, asDependency } from '@vltpkg/graph';
2
2
  import { joinDepIDTuple, splitDepID } from '@vltpkg/dep-id';
3
3
  import { Spec } from '@vltpkg/spec';
4
4
  import { isAbsolute, relative, resolve } from 'node:path';
5
+ import { asUnknownSpecPrefix } from "./require-registry.js";
5
6
  const rootDepID = joinDepIDTuple(['file', '.']);
6
7
  /**
7
8
  * Compute a DepID for the current working directory relative to the project
@@ -76,6 +77,18 @@ const rebaseFileSpec = (spec, importerPath, scurryCwd, specOptions, monorepo) =>
76
77
  return spec;
77
78
  return Spec.parseArgs(rebasedPath, specOptions);
78
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
+ };
79
92
  /**
80
93
  * Parses the positional arguments into {@link AddImportersDependenciesMap}.
81
94
  */
@@ -90,7 +103,7 @@ export const parseAddArgs = (config, scurry, monorepo) => {
90
103
  const parsedItems = [];
91
104
  let hasFileSpecs = false;
92
105
  for (const item of items) {
93
- const spec = Spec.parseArgs(item, specOptions);
106
+ const spec = parseArg(item, specOptions);
94
107
  parsedItems.push({ spec, type });
95
108
  if (spec.type === 'file')
96
109
  hasFileSpecs = true;