@vltpkg/cli-sdk 1.0.9 → 1.1.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.
@@ -1,5 +1,5 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient } from '@vltpkg/registry-client';
2
+ import { RegistryClient, assertOk } from '@vltpkg/registry-client';
3
3
  import { commandUsage } from "../config/usage.js";
4
4
  import { resolveRegistry } from "../require-registry.js";
5
5
  export const needsRegistry = true;
@@ -89,6 +89,14 @@ export const command = async (conf) => {
89
89
  const encodePkgName = (name) => name.startsWith('@') ?
90
90
  `@${encodeURIComponent(name.slice(1))}`
91
91
  : encodeURIComponent(name);
92
+ // 401/403/404 from the access endpoints all mean something the user can
93
+ // act on, so each gets a concrete next step rather than a bare status.
94
+ const accessAdvice = (what) => (statusCode) => statusCode === 401 ? 'Run `vlt login` and try again.'
95
+ : statusCode === 403 ?
96
+ `This account cannot ${what}. Run \`vlt whoami\` to see who you are logged in as.`
97
+ : statusCode === 404 ?
98
+ 'Not found. Check the name, and that this account has access to it.'
99
+ : undefined;
92
100
  const listPackages = async (conf, args) => {
93
101
  const [keyword, entity] = args;
94
102
  if (keyword !== 'packages') {
@@ -103,6 +111,11 @@ const listPackages = async (conf, args) => {
103
111
  const scope = entity ?? getDefaultScope(conf);
104
112
  const url = new URL(`-/org/${encodeURIComponent(scope)}/package`, registryUrl);
105
113
  const response = await rc.request(url, { useCache: false });
114
+ assertOk(response, {
115
+ message: 'Failed to list packages',
116
+ url,
117
+ advice: accessAdvice('list packages for this scope'),
118
+ });
106
119
  const data = response.json();
107
120
  return { packages: data };
108
121
  };
@@ -123,6 +136,11 @@ const getStatus = async (conf, args) => {
123
136
  const registryUrl = new URL(await resolveRegistry(conf));
124
137
  const url = new URL(`-/package/${encodePkgName(pkg)}/access`, registryUrl);
125
138
  const response = await rc.request(url, { useCache: false });
139
+ assertOk(response, {
140
+ message: 'Failed to get package access status',
141
+ url,
142
+ advice: accessAdvice('read access for this package'),
143
+ });
126
144
  const data = response.json();
127
145
  return { package: pkg, access: data.access };
128
146
  };
@@ -148,13 +166,19 @@ const setStatus = async (conf, args) => {
148
166
  const rc = new RegistryClient(conf.options);
149
167
  const registryUrl = new URL(await resolveRegistry(conf));
150
168
  const url = new URL(`-/package/${encodePkgName(pkg)}/access`, registryUrl);
151
- await rc.request(url, {
169
+ const response = await rc.request(url, {
152
170
  method: 'PUT',
153
171
  headers: { 'content-type': 'application/json' },
154
172
  body: JSON.stringify({ access: accessLevel }),
155
173
  otp: conf.options.otp,
156
174
  useCache: false,
157
175
  });
176
+ assertOk(response, {
177
+ message: 'Failed to set package access status',
178
+ url,
179
+ method: 'PUT',
180
+ advice: accessAdvice('change access for this package'),
181
+ });
158
182
  return { package: pkg, access: accessLevel };
159
183
  };
160
184
  const grant = async (conf, args) => {
@@ -184,7 +208,7 @@ const grant = async (conf, args) => {
184
208
  const rc = new RegistryClient(conf.options);
185
209
  const registryUrl = new URL(await resolveRegistry(conf));
186
210
  const url = new URL(`-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, registryUrl);
187
- await rc.request(url, {
211
+ const response = await rc.request(url, {
188
212
  method: 'PUT',
189
213
  headers: { 'content-type': 'application/json' },
190
214
  body: JSON.stringify({
@@ -194,6 +218,12 @@ const grant = async (conf, args) => {
194
218
  otp: conf.options.otp,
195
219
  useCache: false,
196
220
  });
221
+ assertOk(response, {
222
+ message: 'Failed to grant team access',
223
+ url,
224
+ method: 'PUT',
225
+ advice: accessAdvice('grant access for this team'),
226
+ });
197
227
  return {
198
228
  granted: {
199
229
  team: `${scope}:${team}`,
@@ -221,13 +251,19 @@ const revoke = async (conf, args) => {
221
251
  const rc = new RegistryClient(conf.options);
222
252
  const registryUrl = new URL(await resolveRegistry(conf));
223
253
  const url = new URL(`-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, registryUrl);
224
- await rc.request(url, {
254
+ const response = await rc.request(url, {
225
255
  method: 'DELETE',
226
256
  headers: { 'content-type': 'application/json' },
227
257
  body: JSON.stringify({ package: pkgName }),
228
258
  otp: conf.options.otp,
229
259
  useCache: false,
230
260
  });
261
+ assertOk(response, {
262
+ message: 'Failed to revoke team access',
263
+ url,
264
+ method: 'DELETE',
265
+ advice: accessAdvice('revoke access for this team'),
266
+ });
231
267
  return {
232
268
  revoked: {
233
269
  team: `${scope}:${team}`,
@@ -1,5 +1,5 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { CacheEntry } from '@vltpkg/registry-client';
2
+ import { CacheEntry, assertOk } from '@vltpkg/registry-client';
3
3
  import { Spec } from '@vltpkg/spec';
4
4
  import { mkdir, rm } from 'node:fs/promises';
5
5
  import prettyBytes from 'pretty-bytes';
@@ -243,12 +243,17 @@ const add = async (conf, specs, view) => {
243
243
  })
244
244
  .then(async (r) => {
245
245
  const { resolved, integrity } = r;
246
- await (await packageInfo.getRegistryClient()).request(resolved, {
246
+ const response = await (await packageInfo.getRegistryClient()).request(resolved, {
247
247
  ...conf.options,
248
248
  integrity,
249
249
  staleWhileRevalidate: false,
250
250
  query: undefined,
251
251
  });
252
+ // otherwise a 404 tarball still prints '+ <spec>'
253
+ assertOk(response, {
254
+ message: `Failed to cache ${spec}`,
255
+ url: resolved,
256
+ });
252
257
  view?.stdout('+', spec, r.resolved);
253
258
  });
254
259
  promises.push(p);
@@ -1,9 +1,8 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { PackageInfoClient } from '@vltpkg/package-info';
3
- import { RegistryClient } from '@vltpkg/registry-client';
3
+ import { RegistryClient, assertOk, requestError, } from '@vltpkg/registry-client';
4
4
  import { Spec } from '@vltpkg/spec';
5
5
  import { satisfies } from '@vltpkg/semver';
6
- import { asError } from '@vltpkg/types';
7
6
  import { commandUsage } from "../config/usage.js";
8
7
  import { resolveRegistry } from "../require-registry.js";
9
8
  export const needsRegistry = true;
@@ -123,16 +122,17 @@ export const command = async (conf) => {
123
122
  });
124
123
  }
125
124
  catch (err) {
126
- throw error('failed to update deprecation status', {
127
- cause: asError(err),
128
- });
129
- }
130
- if (response.statusCode < 200 || response.statusCode >= 300) {
131
- throw error('failed to update deprecation status', {
125
+ throw requestError(err, {
126
+ message: 'failed to update deprecation status',
132
127
  url: packageUrl,
133
- response,
128
+ method: 'PUT',
134
129
  });
135
130
  }
131
+ assertOk(response, {
132
+ message: 'failed to update deprecation status',
133
+ url: packageUrl,
134
+ method: 'PUT',
135
+ });
136
136
  return {
137
137
  name,
138
138
  version: versionRange || '*',
@@ -1,5 +1,5 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient } from '@vltpkg/registry-client';
2
+ import { RegistryClient, assertOk } from '@vltpkg/registry-client';
3
3
  import { Spec } from '@vltpkg/spec';
4
4
  import { commandUsage } from "../config/usage.js";
5
5
  import { resolveRegistry } from "../require-registry.js";
@@ -121,12 +121,11 @@ export const command = async (conf) => {
121
121
  body: JSON.stringify(version),
122
122
  useCache: false,
123
123
  });
124
- if (response.statusCode < 200 || response.statusCode >= 300) {
125
- throw error('Failed to add dist-tag', {
126
- url,
127
- response,
128
- });
129
- }
124
+ assertOk(response, {
125
+ message: 'Failed to add dist-tag',
126
+ url,
127
+ method: 'PUT',
128
+ });
130
129
  return { id: name, tag, version };
131
130
  }
132
131
  case 'rm':
@@ -144,12 +143,11 @@ export const command = async (conf) => {
144
143
  method: 'DELETE',
145
144
  useCache: false,
146
145
  });
147
- if (response.statusCode < 200 || response.statusCode >= 300) {
148
- throw error('Failed to remove dist-tag', {
149
- url,
150
- response,
151
- });
152
- }
146
+ assertOk(response, {
147
+ message: 'Failed to remove dist-tag',
148
+ url,
149
+ method: 'DELETE',
150
+ });
153
151
  return { id: name, tag };
154
152
  }
155
153
  case 'ls':
@@ -159,12 +157,10 @@ export const command = async (conf) => {
159
157
  const response = await rc.request(url, {
160
158
  useCache: false,
161
159
  });
162
- if (response.statusCode < 200 || response.statusCode >= 300) {
163
- throw error('Failed to list dist-tags', {
164
- url,
165
- response,
166
- });
167
- }
160
+ assertOk(response, {
161
+ message: 'Failed to list dist-tags',
162
+ url,
163
+ });
168
164
  const tags = response.json();
169
165
  return { id: name, tags };
170
166
  }
@@ -4,6 +4,7 @@ import Spinner from 'ink-spinner';
4
4
  import { createElement as $, Fragment, useEffect, useState, } from 'react';
5
5
  import { ViewClass } from "../../view.js";
6
6
  import { asError } from '@vltpkg/types';
7
+ import { persistedEntries } from "../../persist-spec-config.js";
7
8
  const labels = {
8
9
  build: 'resolving dependencies',
9
10
  actual: '',
@@ -84,6 +85,12 @@ export class InstallReporter extends ViewClass {
84
85
  out +=
85
86
  '🔨 Run `vlt build` to run all required scripts to build installed packages.\n';
86
87
  }
88
+ const saved = _result.persistedConfig;
89
+ if (saved) {
90
+ for (const e of persistedEntries(saved.values)) {
91
+ out += `\nSaved ${e} to ${saved.which} vlt.json`;
92
+ }
93
+ }
87
94
  this.#instance?.rerender($(App, { trailer: out }));
88
95
  return undefined;
89
96
  }
@@ -1,3 +1,4 @@
1
+ import type { SpecConfigPersistPlan } from '../persist-spec-config.ts';
1
2
  import type { DepID } from '@vltpkg/dep-id';
2
3
  import type { Diff, Graph } from '@vltpkg/graph';
3
4
  import type { CommandFn, CommandUsage } from '../index.ts';
@@ -17,12 +18,17 @@ export type InstallResult = {
17
18
  * The diff between the actual and ideal graphs, if available.
18
19
  */
19
20
  diff?: Diff;
21
+ /**
22
+ * Spec config from the cli / env that was saved to a config file.
23
+ */
24
+ persistedConfig?: SpecConfigPersistPlan;
20
25
  };
21
26
  export declare const needsRegistry = true;
22
27
  export declare const needsNpmRegistry = true;
23
28
  export declare const usage: CommandUsage;
24
29
  export declare const views: {
25
30
  readonly json: (i: InstallResult) => {
31
+ persistedConfig?: SpecConfigPersistPlan | undefined;
26
32
  buildQueue?: DepID[] | undefined;
27
33
  message?: string | undefined;
28
34
  add: {
@@ -1,6 +1,8 @@
1
1
  import { commandUsage } from "../config/usage.js";
2
2
  import { install } from '@vltpkg/graph';
3
3
  import { parseAddArgs } from "../parse-add-remove-args.js";
4
+ import { planSpecConfigPersist } from "../persist-spec-config.js";
5
+ import { asUnknownSpecPrefix } from "../require-registry.js";
4
6
  import { trackInstall } from "../telemetry.js";
5
7
  import { lazyView } from "../view.js";
6
8
  export const needsRegistry = true;
@@ -30,6 +32,9 @@ export const usage = () => commandUsage({
30
32
  'save-prod': {
31
33
  description: 'Save installed packages to package.json as dependencies.',
32
34
  },
35
+ 'save-config': {
36
+ 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.',
37
+ },
33
38
  workspace: {
34
39
  value: '<path|glob>',
35
40
  description: 'Limit installation targets to matching workspaces.',
@@ -111,6 +116,9 @@ export const views = {
111
116
  message: `${i.buildQueue.length} packages that will need to be built, run "vlt build" to complete the install.`,
112
117
  }
113
118
  : null),
119
+ ...(i.persistedConfig ?
120
+ { persistedConfig: i.persistedConfig }
121
+ : null),
114
122
  };
115
123
  },
116
124
  human: lazyView(async () => (await import('./install/reporter.ts')).InstallReporter),
@@ -121,6 +129,7 @@ export const command = async (conf) => {
121
129
  const monorepo = conf.options.monorepo;
122
130
  const scurry = conf.options.scurry;
123
131
  const { add } = parseAddArgs(conf, scurry, monorepo);
132
+ const persist = planSpecConfigPersist(conf);
124
133
  const frozenLockfile = conf.options['frozen-lockfile'];
125
134
  const expectLockfile = conf.options['expect-lockfile'];
126
135
  const lockfileOnly = conf.options['lockfile-only'];
@@ -140,7 +149,11 @@ export const command = async (conf) => {
140
149
  lockfileOnly,
141
150
  saveExact,
142
151
  savePrefix,
143
- }, add);
152
+ }, add).catch((er) => {
153
+ throw asUnknownSpecPrefix(er);
154
+ });
155
+ if (persist)
156
+ await conf.addConfigToFile(persist.which, persist.values);
144
157
  /* c8 ignore next 9 - telemetry is best-effort */
145
158
  try {
146
159
  trackInstall({
@@ -149,5 +162,10 @@ export const command = async (conf) => {
149
162
  }, conf.values.telemetry);
150
163
  }
151
164
  catch { }
152
- return { buildQueue, graph, diff };
165
+ return {
166
+ buildQueue,
167
+ graph,
168
+ diff,
169
+ ...(persist ? { persistedConfig: persist } : null),
170
+ };
153
171
  };
@@ -5,6 +5,7 @@ import { error } from '@vltpkg/error-cause';
5
5
  import { commandUsage } from "../config/usage.js";
6
6
  import { createHostContextsMap } from "../query-host-contexts.js";
7
7
  import { createGetAuthHeader } from "../query-auth.js";
8
+ import { assertVltInstalled } from "../is-vlt-installed.js";
8
9
  import { MermaidImageView } from "../mermaid-image-view.js";
9
10
  export const needsRegistry = true;
10
11
  export const usage = () => commandUsage({
@@ -25,6 +26,11 @@ export const usage = () => commandUsage({
25
26
  filter what dependencies to include in the output. Using both options
26
27
  allows you to render subgraphs of the dependency graph.
27
28
 
29
+ The listed graph is the one built by \`vlt install\`. In a project
30
+ installed by another client there is no such graph and the command
31
+ errors instead of reporting every dependency as missing. Queries
32
+ using \`:host()\` load their graph from elsewhere and are exempt.
33
+
28
34
  Defaults to listing direct dependencies of a project and any configured
29
35
  workspace.`,
30
36
  examples: {
@@ -73,19 +79,6 @@ export const command = async (conf) => {
73
79
  const mainManifest = conf.options.packageJson.maybeRead(conf.options.projectRoot);
74
80
  let graph;
75
81
  let securityArchive;
76
- // optionally load the cwd graph if we found a package.json file
77
- if (mainManifest) {
78
- graph = actual.load({
79
- ...conf.options,
80
- mainManifest,
81
- modifiers,
82
- monorepo,
83
- loadManifests: true,
84
- });
85
- securityArchive = await SecurityArchive.start({
86
- nodes: [...graph.nodes.values()],
87
- });
88
- }
89
82
  // Validate positional arguments - only allow package names, not direct queries
90
83
  for (const arg of conf.positionals) {
91
84
  if (!/^[@\w-]/.test(arg)) {
@@ -103,6 +96,25 @@ export const command = async (conf) => {
103
96
  const scopeQueryString = conf.get('scope');
104
97
  const queryString = targetQueryString || positionalQueryString;
105
98
  const projectQueryString = ':workspace, :project > *';
99
+ // `:host()` swaps the graph for one loaded from somewhere else, so
100
+ // those queries do not need this project to be installed.
101
+ const usesHostContext = [queryString, scopeQueryString].some(q => q?.includes(':host('));
102
+ // optionally load the cwd graph if we found a package.json file
103
+ if (mainManifest) {
104
+ if (!usesHostContext) {
105
+ assertVltInstalled(conf.options.projectRoot, 'ls');
106
+ }
107
+ graph = actual.load({
108
+ ...conf.options,
109
+ mainManifest,
110
+ modifiers,
111
+ monorepo,
112
+ loadManifests: true,
113
+ });
114
+ securityArchive = await SecurityArchive.start({
115
+ nodes: [...graph.nodes.values()],
116
+ });
117
+ }
106
118
  const selectImporters = [];
107
119
  const hostContexts = await createHostContextsMap(conf);
108
120
  const getAuthHeader = createGetAuthHeader(conf);
@@ -1,5 +1,5 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient } from '@vltpkg/registry-client';
2
+ import { RegistryClient, assertOk } from '@vltpkg/registry-client';
3
3
  import { commandUsage } from "../config/usage.js";
4
4
  import { resolveRegistry } from "../require-registry.js";
5
5
  export const needsRegistry = true;
@@ -69,6 +69,16 @@ const getProfile = async (conf, args) => {
69
69
  const registryUrl = new URL(await resolveRegistry(conf));
70
70
  const url = new URL('-/npm/v1/user', registryUrl);
71
71
  const response = await rc.request(url, { useCache: false });
72
+ // before json(): otherwise an error body makes the 'Property not
73
+ // found in profile' check below blame the user's argument for what is
74
+ // really an auth failure.
75
+ assertOk(response, {
76
+ message: 'Failed to fetch profile',
77
+ url,
78
+ advice: statusCode => statusCode === 401 || statusCode === 403 ?
79
+ `Not logged in to ${registryUrl.origin}. Run \`vlt login\` and try again.`
80
+ : undefined,
81
+ });
72
82
  const data = response.json();
73
83
  const [property] = args;
74
84
  if (property) {
@@ -101,6 +111,14 @@ const setProfile = async (conf, args) => {
101
111
  otp: conf.options.otp,
102
112
  useCache: false,
103
113
  });
114
+ assertOk(response, {
115
+ message: 'Failed to update profile',
116
+ url,
117
+ method: 'POST',
118
+ advice: statusCode => statusCode === 401 || statusCode === 403 ?
119
+ `Not logged in to ${registryUrl.origin}. Run \`vlt login\` and try again.`
120
+ : undefined,
121
+ });
104
122
  const data = response.json();
105
123
  return { property, value: data[property] };
106
124
  };
@@ -1,10 +1,9 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient, oidc, registryBase, } from '@vltpkg/registry-client';
2
+ import { RegistryClient, assertOk, oidc, registryBase, requestError, } from '@vltpkg/registry-client';
3
3
  import { run } from '@vltpkg/run';
4
4
  import { commandUsage } from "../config/usage.js";
5
5
  import { packTarball } from "../pack-tarball.js";
6
6
  import assert from 'node:assert';
7
- import { asError } from '@vltpkg/types';
8
7
  import { dirname, resolve } from 'node:path';
9
8
  import prettyBytes from 'pretty-bytes';
10
9
  import { actual } from '@vltpkg/graph';
@@ -12,7 +11,6 @@ import { Query } from '@vltpkg/query';
12
11
  import { createHostContextsMap } from "../query-host-contexts.js";
13
12
  import { minimatch } from 'minimatch';
14
13
  import { resolveRegistry } from "../require-registry.js";
15
- import { registryErrorMessage } from "../registry-error-message.js";
16
14
  import { stderr } from "../output.js";
17
15
  export const needsRegistry = true;
18
16
  export const usage = () => commandUsage({
@@ -246,6 +244,17 @@ const commandSingle = async (location, conf) => {
246
244
  });
247
245
  const publishUrl = new URL(name.startsWith('@') ? name.replace('/', '%2F') : name, registryUrl);
248
246
  if (!dry) {
247
+ // the same guidance whether the registry refused the request or the
248
+ // client threw before a response came back (an expired token, say)
249
+ const publishAdvice = (statusCode) => statusCode === 409 ?
250
+ `${name}@${version} already exists in the registry. Bump the version and try again.`
251
+ : statusCode === 404 ?
252
+ "Make sure you're logged in and have access to publish the package."
253
+ : statusCode === 401 ?
254
+ `Not logged in to ${registryUrl.origin}. Run \`vlt login\` and try again.`
255
+ : statusCode === 403 ?
256
+ `${name} cannot be published by this account. Run \`vlt whoami\` to see who you are logged in as, and \`vlt login\` to switch accounts. A name already taken by someone else cannot be reused.`
257
+ : undefined;
249
258
  let response;
250
259
  try {
251
260
  response = await rc.request(publishUrl, {
@@ -261,27 +270,26 @@ const commandSingle = async (location, conf) => {
261
270
  });
262
271
  }
263
272
  catch (err) {
264
- throw error('Failed to publish package', {
265
- cause: asError(err),
273
+ throw requestError(err, {
274
+ message: 'Failed to publish package',
275
+ url: publishUrl,
276
+ method: 'PUT',
277
+ advice: publishAdvice,
266
278
  });
267
279
  }
268
280
  // Any 2xx response is a successful publish. The npm registry may
269
281
  // respond with a 202 Accepted when processing is deferred (e.g. for
270
282
  // publish-time malware scanning).
271
- if (response.statusCode < 200 || response.statusCode >= 300) {
272
- let advice = '';
273
- if (response.statusCode === 409) {
274
- advice = `\n⚠️ ${name}@${version} already exists in the registry. Bump the version and try again.`;
275
- }
276
- else if (response.statusCode === 404) {
277
- advice =
278
- "\n⚠️ Make sure you're logged in and have access to publish the package.";
279
- }
280
- throw error(`Failed to publish package: ${registryErrorMessage(response)}${advice}`, {
281
- url: publishUrl,
282
- response,
283
- });
284
- }
283
+ //
284
+ // assertOk tells a rejected credential (ENEEDAUTH) apart from a
285
+ // registry that is simply broken (EREQUEST): the former is the
286
+ // user's to fix, not a bug to report.
287
+ assertOk(response, {
288
+ message: 'Failed to publish package',
289
+ url: publishUrl,
290
+ method: 'PUT',
291
+ advice: publishAdvice,
292
+ });
285
293
  // On deferred publishes the registry explains the delay in an
286
294
  // npm-notice header, e.g. "Your package is being processed and may
287
295
  // take a few minutes to become available."
@@ -7,6 +7,7 @@ import { SecurityArchive } from '@vltpkg/security-archive';
7
7
  import { commandUsage } from "../config/usage.js";
8
8
  import { createHostContextsMap } from "../query-host-contexts.js";
9
9
  import { createGetAuthHeader } from "../query-auth.js";
10
+ import { assertVltInstalled } from "../is-vlt-installed.js";
10
11
  import { MermaidImageView } from "../mermaid-image-view.js";
11
12
  export const needsRegistry = true;
12
13
  export const usage = () => commandUsage({
@@ -30,6 +31,11 @@ export const usage = () => commandUsage({
30
31
  dependencies to include in the output. Using both options allows you to
31
32
  render subgraphs of the dependency graph.
32
33
 
34
+ The queried graph is the one built by \`vlt install\`. In a project
35
+ installed by another client there is no such graph and the command
36
+ errors instead of reporting every dependency as missing. Queries
37
+ using \`:host()\` load their graph from elsewhere and are exempt.
38
+
33
39
  Defaults to listing all dependencies of the project root and workspaces.`,
34
40
  examples: {
35
41
  [`'#foo'`]: {
@@ -109,8 +115,22 @@ export const command = async (conf) => {
109
115
  const mainManifest = conf.options.packageJson.maybeRead(conf.options.projectRoot);
110
116
  let graph;
111
117
  let securityArchive;
118
+ // retrieve default values and set up host contexts
119
+ const defaultProjectQueryString = '*';
120
+ const defaultLocalScopeQueryString = ':host(local) *';
121
+ const positionalQueryString = conf.positionals[0];
122
+ const targetQueryString = conf.get('target');
123
+ const scopeQueryString = conf.get('scope');
124
+ // --target takes precedence over the positional query
125
+ const queryString = targetQueryString || positionalQueryString;
126
+ // `:host()` swaps the graph for one loaded from somewhere else, so
127
+ // those queries do not need this project to be installed.
128
+ const usesHostContext = [queryString, scopeQueryString].some(q => q?.includes(':host('));
112
129
  // optionally load the cwd graph if we found a package.json file
113
130
  if (mainManifest) {
131
+ if (!usesHostContext) {
132
+ assertVltInstalled(conf.options.projectRoot, 'query');
133
+ }
114
134
  graph = actual.load({
115
135
  ...conf.options,
116
136
  mainManifest,
@@ -122,13 +142,6 @@ export const command = async (conf) => {
122
142
  nodes: [...graph.nodes.values()],
123
143
  });
124
144
  }
125
- // retrieve default values and set up host contexts
126
- const defaultProjectQueryString = '*';
127
- const defaultLocalScopeQueryString = ':host(local) *';
128
- const positionalQueryString = conf.positionals[0];
129
- const targetQueryString = conf.get('target');
130
- const scopeQueryString = conf.get('scope');
131
- const queryString = targetQueryString || positionalQueryString;
132
145
  const hostContexts = await createHostContextsMap(conf);
133
146
  const diffFiles = createDiffFilesProvider(conf.options.projectRoot);
134
147
  const getAuthHeader = createGetAuthHeader(conf);
@@ -6,6 +6,7 @@ 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';
@@ -36,8 +37,6 @@ const accountAuthError = (account, er) => (isErrorWithCause(er) &&
36
37
  `organization at ${VLT_SIGNUP_URL}, then run \`vlt setup\` again.`,
37
38
  ].join('\n'), { code: 'ECONFIG', cause: er })
38
39
  : asError(er);
39
- /** Ensure a registry URL ends with a single trailing slash. */
40
- const normalizeRegistryURL = (url) => url.endsWith('/') ? url : `${url}/`;
41
40
  export const views = {
42
41
  human: (result) => {
43
42
  const lines = [
@@ -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)');