@vltpkg/cli-sdk 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/allow-skills.d.ts +36 -0
  2. package/dist/allow-skills.js +49 -0
  3. package/dist/commands/build.d.ts +15 -4
  4. package/dist/commands/build.js +50 -12
  5. package/dist/commands/ci.d.ts +2 -2
  6. package/dist/commands/ci.js +10 -2
  7. package/dist/commands/create.d.ts +2 -1
  8. package/dist/commands/create.js +2 -3
  9. package/dist/commands/exec.d.ts +2 -1
  10. package/dist/commands/exec.js +2 -3
  11. package/dist/commands/init.js +4 -3
  12. package/dist/commands/install/reporter.d.ts +1 -1
  13. package/dist/commands/install/reporter.js +91 -5
  14. package/dist/commands/install.d.ts +50 -4
  15. package/dist/commands/install.js +107 -25
  16. package/dist/commands/list.js +8 -2
  17. package/dist/commands/publish.d.ts +0 -1
  18. package/dist/commands/publish.js +27 -16
  19. package/dist/commands/query.js +8 -2
  20. package/dist/commands/setup.d.ts +28 -2
  21. package/dist/commands/setup.js +185 -3
  22. package/dist/commands/skills.d.ts +56 -0
  23. package/dist/commands/skills.js +267 -0
  24. package/dist/commands/token.js +1 -1
  25. package/dist/commands/uninstall.d.ts +15 -4
  26. package/dist/commands/uninstall.js +32 -4
  27. package/dist/commands/unpublish.d.ts +0 -1
  28. package/dist/commands/unpublish.js +17 -4
  29. package/dist/commands/update.d.ts +13 -1
  30. package/dist/commands/update.js +18 -1
  31. package/dist/config/definition.d.ts +52 -0
  32. package/dist/config/definition.js +109 -1
  33. package/dist/config/index.d.ts +26 -3
  34. package/dist/config/index.js +149 -7
  35. package/dist/custom-help.js +4 -0
  36. package/dist/global-bin-dir.d.ts +37 -0
  37. package/dist/global-bin-dir.js +138 -0
  38. package/dist/global-project.d.ts +18 -0
  39. package/dist/global-project.js +67 -0
  40. package/dist/global-state.d.ts +24 -0
  41. package/dist/global-state.js +94 -0
  42. package/dist/global.d.ts +78 -0
  43. package/dist/global.js +409 -0
  44. package/dist/index.js +14 -3
  45. package/dist/install-receipt.d.ts +40 -0
  46. package/dist/install-receipt.js +69 -0
  47. package/dist/load-command.d.ts +2 -0
  48. package/dist/migrate-workspaces.d.ts +16 -0
  49. package/dist/migrate-workspaces.js +44 -0
  50. package/dist/output.js +13 -0
  51. package/dist/parse-add-remove-args.js +2 -14
  52. package/dist/print-err.js +4 -1
  53. package/dist/publish-config.d.ts +9 -0
  54. package/dist/publish-config.js +8 -0
  55. package/dist/query-auth.d.ts +3 -2
  56. package/dist/query-auth.js +6 -4
  57. package/dist/reload-config.js +2 -1
  58. package/dist/require-registry.d.ts +7 -0
  59. package/dist/require-registry.js +13 -1
  60. package/dist/self-update.d.ts +74 -0
  61. package/dist/self-update.js +401 -0
  62. package/dist/shell-setup/env.d.ts +9 -0
  63. package/dist/shell-setup/env.js +25 -0
  64. package/dist/shell-setup/index.d.ts +69 -0
  65. package/dist/shell-setup/index.js +213 -0
  66. package/dist/shell-setup/posix.d.ts +36 -0
  67. package/dist/shell-setup/posix.js +158 -0
  68. package/dist/shell-setup/windows.d.ts +39 -0
  69. package/dist/shell-setup/windows.js +151 -0
  70. package/dist/shell.d.ts +18 -0
  71. package/dist/shell.js +110 -0
  72. package/dist/skills-prompt.d.ts +28 -0
  73. package/dist/skills-prompt.js +78 -0
  74. package/package.json +30 -28
@@ -1,9 +1,14 @@
1
1
  import { commandUsage } from "../config/usage.js";
2
2
  import { install } from '@vltpkg/graph';
3
+ import { saveAllowSkills } from "../allow-skills.js";
3
4
  import { parseAddArgs } from "../parse-add-remove-args.js";
5
+ import { assertGlobalOptions, linkGlobalBins, parseGlobalAddArgs, removeGlobalWorkspaces, } from "../global.js";
6
+ import { resolveGlobalBinDir } from "../global-bin-dir.js";
7
+ import { deferSelfRemove, runningStoreDep, selfUpdate, } from "../self-update.js";
4
8
  import { planSpecConfigPersist } from "../persist-spec-config.js";
5
9
  import { asUnknownSpecPrefix } from "../require-registry.js";
6
10
  import { addTypesDeps } from "../save-types.js";
11
+ import { migrateWorkspaces } from "../migrate-workspaces.js";
7
12
  import { trackInstall } from "../telemetry.js";
8
13
  import { lazyView } from "../view.js";
9
14
  export const needsRegistry = true;
@@ -39,6 +44,9 @@ export const usage = () => commandUsage({
39
44
  'save-config': {
40
45
  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.',
41
46
  },
47
+ 'no-migrate-workspaces': {
48
+ description: "Don't save workspaces read from package.json or pnpm-workspace.yaml to vlt.json.",
49
+ },
42
50
  workspace: {
43
51
  value: '<path|glob>',
44
52
  description: 'Limit installation targets to matching workspaces.',
@@ -60,10 +68,24 @@ export const usage = () => commandUsage({
60
68
  value: '<query>',
61
69
  description: 'Filter which packages are allowed to run lifecycle scripts using DSS query syntax.',
62
70
  },
71
+ 'allow-skills': {
72
+ value: '<query>',
73
+ description: "Link agent skills of packages matching this DSS query into the project's agent skill dirs. Saved to vlt.json.",
74
+ },
75
+ global: {
76
+ description: 'Install packages globally, each in its own workspace of the global project, linking their bins. `vlt install -g vlt` updates vlt itself.',
77
+ },
63
78
  },
64
79
  });
80
+ // link mounts; `unchanged` left out
81
+ const skillsJson = ({ linked, unchanged: _, ...rest }) => ({
82
+ linked: linked.map(s => s.mount),
83
+ ...rest,
84
+ });
65
85
  export const views = {
66
86
  json: i => {
87
+ if (!('graph' in i))
88
+ return { self: i.self };
67
89
  const added = i.diff?.nodes.add ?? new Set();
68
90
  const deleted = i.diff?.nodes.delete ?? new Set();
69
91
  // The actual (from) graph represents what was on disk before the install.
@@ -117,12 +139,19 @@ export const views = {
117
139
  ...(i.buildQueue?.length ?
118
140
  {
119
141
  buildQueue: i.buildQueue,
120
- message: `${i.buildQueue.length} packages that will need to be built, run "vlt build" to complete the install.`,
142
+ message: `${i.buildQueue.length} packages that will need to be built, run "vlt build${i.global ? ' -g' : ''}" to complete the install.`,
121
143
  }
122
144
  : null),
123
145
  ...(i.persistedConfig ?
124
146
  { persistedConfig: i.persistedConfig }
125
147
  : null),
148
+ ...(i.migratedWorkspaces ?
149
+ { migratedWorkspaces: i.migratedWorkspaces }
150
+ : null),
151
+ ...(i.skills ? { skills: skillsJson(i.skills) } : null),
152
+ ...(i.allowSkills ? { allowSkills: i.allowSkills } : null),
153
+ ...(i.global ? { global: i.global } : null),
154
+ ...(i.self ? { self: i.self } : null),
126
155
  };
127
156
  },
128
157
  human: lazyView(async () => (await import('./install/reporter.ts')).InstallReporter),
@@ -130,36 +159,52 @@ export const views = {
130
159
  export const command = async (conf) => {
131
160
  // TODO: we should probably throw an error if the user
132
161
  // tries to install using view=mermaid
133
- const monorepo = conf.options.monorepo;
134
- const scurry = conf.options.scurry;
135
- const { add } = parseAddArgs(conf, scurry, monorepo);
162
+ if (conf.globalRoot)
163
+ assertGlobalOptions(conf);
164
+ // throws before any global ws dir is created
136
165
  const persist = planSpecConfigPersist(conf);
137
- if (conf.values['save-types'])
138
- await addTypesDeps(conf, add);
139
- const frozenLockfile = conf.options['frozen-lockfile'];
140
- const expectLockfile = conf.options['expect-lockfile'];
141
- const lockfileOnly = conf.options['lockfile-only'];
142
- const saveExact = conf.values['save-exact'];
143
- const savePrefix = conf.values['save-prefix'];
144
- /* c8 ignore start */
145
- const allowScripts = conf.get('allow-scripts') ?
146
- String(conf.get('allow-scripts'))
147
- : ':not(*)';
148
- /* c8 ignore stop */
166
+ // vlt i -g vlt: before any global ws or bin dir is created
167
+ const selfPlan = await selfUpdate(conf);
168
+ if (selfPlan && 'done' in selfPlan)
169
+ return { self: selfPlan.done };
170
+ const binDir = conf.globalRoot ?
171
+ await resolveGlobalBinDir(conf, {
172
+ selfUnwritable: !!selfPlan?.selfUnwritable,
173
+ })
174
+ : undefined;
175
+ // before reading options, it may reset them
176
+ const globalAdd = conf.globalRoot ? await parseGlobalAddArgs(conf) : undefined;
177
+ // never removed from under itself
178
+ const keep = conf.globalRoot ?
179
+ await runningStoreDep(conf.globalRoot)
180
+ : undefined;
149
181
  const installStart = Date.now();
150
- const { buildQueue, graph, diff } = await install({
151
- ...conf.options,
152
- frozenLockfile,
153
- expectLockfile,
154
- allowScripts,
155
- lockfileOnly,
156
- saveExact,
157
- savePrefix,
158
- }, add).catch((er) => {
182
+ const { buildQueue, graph, diff, skills } = await runInstall(conf, globalAdd?.add, keep).catch(async (er) => {
183
+ if (globalAdd)
184
+ await removeGlobalWorkspaces(globalAdd.created);
159
185
  throw asUnknownSpecPrefix(er);
160
186
  });
187
+ const migrated = (conf.options['frozen-lockfile'] ||
188
+ conf.options['expect-lockfile']) ?
189
+ undefined
190
+ : migrateWorkspaces(conf);
191
+ if (conf.globalRoot)
192
+ await deferSelfRemove(conf.globalRoot, graph, keep);
193
+ const global = globalAdd &&
194
+ binDir &&
195
+ (await linkGlobalBins(graph, {
196
+ projectRoot: conf.options.projectRoot,
197
+ scurry: conf.options.scurry,
198
+ force: conf.options.force,
199
+ binDir,
200
+ fallback: () => resolveGlobalBinDir(conf, { selfUnwritable: true }),
201
+ }, globalAdd.importers));
161
202
  if (persist)
162
203
  await conf.addConfigToFile(persist.which, persist.values);
204
+ // nothing linked on --lockfile-only
205
+ const allowSkillsSave = conf.options['lockfile-only'] ?
206
+ undefined
207
+ : await saveAllowSkills(conf);
163
208
  /* c8 ignore next 9 - telemetry is best-effort */
164
209
  try {
165
210
  trackInstall({
@@ -173,5 +218,42 @@ export const command = async (conf) => {
173
218
  graph,
174
219
  diff,
175
220
  ...(persist ? { persistedConfig: persist } : null),
221
+ ...(migrated ? { migratedWorkspaces: migrated } : null),
222
+ ...(skills ? { skills } : null),
223
+ ...(allowSkillsSave ? { allowSkills: allowSkillsSave } : null),
224
+ ...(global ? { global } : null),
225
+ ...(selfPlan ? { self: await selfPlan.finish(global) } : null),
176
226
  };
177
227
  };
228
+ // async, so any throw rolls back global ws dirs
229
+ const runInstall = async (conf, globalAdd, keep) => {
230
+ const monorepo = conf.options.monorepo;
231
+ const scurry = conf.options.scurry;
232
+ const add = globalAdd ?? parseAddArgs(conf, scurry, monorepo).add;
233
+ // global ws has only the package as dep
234
+ if (conf.values['save-types'] && !globalAdd)
235
+ await addTypesDeps(conf, add);
236
+ const frozenLockfile = conf.options['frozen-lockfile'];
237
+ const expectLockfile = conf.options['expect-lockfile'];
238
+ const lockfileOnly = conf.options['lockfile-only'];
239
+ const saveExact = conf.values['save-exact'];
240
+ const savePrefix = conf.values['save-prefix'];
241
+ /* c8 ignore start */
242
+ const allowScripts = conf.get('allow-scripts') ?
243
+ String(conf.get('allow-scripts'))
244
+ : ':not(*)';
245
+ /* c8 ignore stop */
246
+ const allowSkills = conf.get('allow-skills');
247
+ return install({
248
+ ...conf.options,
249
+ frozenLockfile,
250
+ expectLockfile,
251
+ allowScripts,
252
+ allowSkills,
253
+ skillsDirs: conf.get('skills-dir'),
254
+ lockfileOnly,
255
+ saveExact,
256
+ savePrefix,
257
+ ...(keep ? { keepNodes: new Set([keep]) } : null),
258
+ }, add);
259
+ };
@@ -1,4 +1,4 @@
1
- import { actual, asNode, humanReadableOutput, jsonOutput, mermaidOutput, GraphModifier, } from '@vltpkg/graph';
1
+ import { actual, asNode, hasSkills, humanReadableOutput, jsonOutput, mermaidOutput, GraphModifier, } from '@vltpkg/graph';
2
2
  import { Query } from '@vltpkg/query';
3
3
  import { SecurityArchive } from '@vltpkg/security-archive';
4
4
  import { error } from '@vltpkg/error-cause';
@@ -63,6 +63,9 @@ export const usage = () => commandUsage({
63
63
  value: '[human | json | mermaid | svg | png | count]',
64
64
  description: 'Output format. Defaults to human-readable or json if no tty. Use svg or png to render the dependency graph as an image and open it. Count outputs the number of dependency relationships in the result.',
65
65
  },
66
+ global: {
67
+ description: 'List globally installed packages.',
68
+ },
66
69
  },
67
70
  });
68
71
  export const views = {
@@ -101,7 +104,8 @@ export const command = async (conf) => {
101
104
  const usesHostContext = [queryString, scopeQueryString].some(q => q?.includes(':host('));
102
105
  // optionally load the cwd graph if we found a package.json file
103
106
  if (mainManifest) {
104
- if (!usesHostContext) {
107
+ // the global project is always vlt managed
108
+ if (!usesHostContext && !conf.globalRoot) {
105
109
  assertVltInstalled(conf.options.projectRoot, 'ls');
106
110
  }
107
111
  graph = actual.load({
@@ -138,6 +142,7 @@ export const command = async (conf) => {
138
142
  securityArchive,
139
143
  hostContexts,
140
144
  getAuthHeader,
145
+ hasSkills,
141
146
  });
142
147
  const { nodes: resultNodes } = await scopeQuery.search(scopeQueryString, {
143
148
  signal: new AbortController().signal,
@@ -190,6 +195,7 @@ export const command = async (conf) => {
190
195
  securityArchive,
191
196
  hostContexts,
192
197
  getAuthHeader,
198
+ hasSkills,
193
199
  });
194
200
  const query = queryString ||
195
201
  /* c8 ignore next */
@@ -1,5 +1,4 @@
1
1
  import type { CommandFn, CommandUsage } from '../index.ts';
2
- export declare const needsRegistry = true;
3
2
  export declare const usage: CommandUsage;
4
3
  export type CommandResultSingle = {
5
4
  id: string;
@@ -14,14 +14,17 @@ import { sortScopeLocations } from "../sort-scope-locations.js";
14
14
  import { minimatch } from 'minimatch';
15
15
  import { resolveRegistry } from "../require-registry.js";
16
16
  import { stderr } from "../output.js";
17
- export const needsRegistry = true;
17
+ import { getPublishConfig } from "../publish-config.js";
18
18
  export const usage = () => commandUsage({
19
19
  command: 'publish',
20
20
  usage: '',
21
21
  description: `Create a tarball from a package and publish it to the configured registry.
22
22
 
23
23
  This command will pack the package in the current directory or specified folder,
24
- and then upload it to the configured registry.`,
24
+ and then upload it to the configured registry.
25
+
26
+ The registry is the package's \`publishConfig.registry\` if set (no other
27
+ registry config needed), else \`--registry\`, else a configured alias.`,
25
28
  options: {
26
29
  tag: {
27
30
  description: 'Publish the package with the given tag',
@@ -143,22 +146,34 @@ export const command = async (conf) => {
143
146
  single = options.packageJson.find(process.cwd()) ?? projectRoot;
144
147
  }
145
148
  if (single) {
146
- return commandSingle(single, conf);
149
+ const [target] = await resolveTargets([single], conf);
150
+ assert(target);
151
+ return commandSingle(target, conf);
147
152
  }
148
153
  assert(locations.length > 0, error('No workspaces or query results found'));
149
154
  const results = [];
150
- for (const location of locations) {
151
- results.push(await commandSingle(location, conf));
155
+ for (const target of await resolveTargets(locations, conf)) {
156
+ results.push(await commandSingle(target, conf));
152
157
  }
153
158
  return results;
154
159
  };
155
- const getPublishConfig = (manifest) => {
156
- const pc = manifest
157
- .publishConfig;
158
- if (pc && typeof pc === 'object') {
159
- return pc;
160
+ /**
161
+ * Registry of every target, before anything runs or publishes.
162
+ * The configured one is only resolved (or prompted for) once.
163
+ */
164
+ const resolveTargets = async (locations, conf) => {
165
+ let fallback;
166
+ const targets = [];
167
+ for (const location of locations) {
168
+ const manifestPath = conf.options.packageJson.find(location);
169
+ assert(manifestPath, 'No package.json found');
170
+ const manifestDir = dirname(manifestPath);
171
+ const manifest = conf.options.packageJson.read(manifestDir);
172
+ const registry = getPublishConfig(manifest)?.registry ??
173
+ (await (fallback ??= resolveRegistry(conf)));
174
+ targets.push({ manifestDir, registry });
160
175
  }
161
- return undefined;
176
+ return targets;
162
177
  };
163
178
  /** Of several readmes at the package root, npm prefers the markdown one. */
164
179
  const README_MARKDOWN_EXTENSION = /\.(?:md|markdown|mdown|mdwn|mkd|mkdn)$/i;
@@ -182,17 +197,13 @@ const readPublishReadme = (packDir, files) => {
182
197
  readmeFilename,
183
198
  };
184
199
  };
185
- const commandSingle = async (location, conf) => {
186
- const manifestPath = conf.options.packageJson.find(location);
187
- assert(manifestPath, 'No package.json found');
188
- const manifestDir = dirname(manifestPath);
200
+ const commandSingle = async ({ manifestDir, registry }, conf) => {
189
201
  const manifest = conf.options.packageJson.read(manifestDir);
190
202
  assert(!manifest.private, error('Package has been marked as private'));
191
203
  const { 'dry-run': dry = false, otp } = conf.options;
192
204
  const publishConfig = getPublishConfig(manifest);
193
205
  const tag = publishConfig?.tag ?? conf.options.tag;
194
206
  const access = publishConfig?.access ?? conf.options.access;
195
- const registry = publishConfig?.registry ?? (await resolveRegistry(conf));
196
207
  const registryUrl = new URL(registryBase(registry));
197
208
  const runOptions = {
198
209
  cwd: manifestDir,
@@ -1,4 +1,4 @@
1
- import { actual, asNode, humanReadableOutput, jsonOutput, mermaidOutput, GraphModifier, } from '@vltpkg/graph';
1
+ import { actual, asNode, hasSkills, humanReadableOutput, jsonOutput, mermaidOutput, GraphModifier, } from '@vltpkg/graph';
2
2
  import { Query } from '@vltpkg/query';
3
3
  import { stdout } from "../output.js";
4
4
  import { error } from '@vltpkg/error-cause';
@@ -80,6 +80,9 @@ export const usage = () => commandUsage({
80
80
  value: '[human | json | mermaid | svg | png | count]',
81
81
  description: 'Output format. Defaults to human-readable or json if no tty. Use svg or png to render the dependency graph as an image and open it. Count outputs the number of dependency relationships in the result.',
82
82
  },
83
+ global: {
84
+ description: 'Query the global project.',
85
+ },
83
86
  },
84
87
  });
85
88
  const validateExpectedResult = (conf, edges) => {
@@ -128,7 +131,8 @@ export const command = async (conf) => {
128
131
  const usesHostContext = [queryString, scopeQueryString].some(q => q?.includes(':host('));
129
132
  // optionally load the cwd graph if we found a package.json file
130
133
  if (mainManifest) {
131
- if (!usesHostContext) {
134
+ // the global project is always vlt managed
135
+ if (!usesHostContext && !conf.globalRoot) {
132
136
  assertVltInstalled(conf.options.projectRoot, 'query');
133
137
  }
134
138
  graph = actual.load({
@@ -166,6 +170,7 @@ export const command = async (conf) => {
166
170
  hostContexts,
167
171
  diffFiles,
168
172
  getAuthHeader,
173
+ hasSkills,
169
174
  });
170
175
  const { nodes: resultNodes } = await scopeQuery.search(scopeQueryString, {
171
176
  signal: new AbortController().signal,
@@ -231,6 +236,7 @@ export const command = async (conf) => {
231
236
  hostContexts,
232
237
  diffFiles,
233
238
  getAuthHeader,
239
+ hasSkills,
234
240
  });
235
241
  const query = queryString ||
236
242
  (graph ? defaultProjectQueryString : defaultLocalScopeQueryString);
@@ -1,3 +1,5 @@
1
+ import type { Shell } from '../shell.ts';
2
+ import type { PathSetupFile } from '../shell-setup/index.ts';
1
3
  import type { CommandFn, CommandUsage } from '../index.ts';
2
4
  /** Where users sign up for / log into a vlt.io account. */
3
5
  export declare const VLT_SIGNUP_URL = "https://www.vlt.io";
@@ -29,9 +31,33 @@ export type SetupResult = {
29
31
  * the project's `vlt.json`, so the result won't take effect here.
30
32
  */
31
33
  shadowedByProject?: boolean;
34
+ /**
35
+ * PATH step outcome; absent when skipped (--yes, no terminal,
36
+ * no-modify-path)
37
+ */
38
+ path?: SetupPathResult['path'] | SetupPathFailed;
39
+ };
40
+ export type SetupPathFailed = {
41
+ status: 'failed';
42
+ /** why; may hold the manual line */
43
+ message: string;
44
+ };
45
+ export type SetupPathResult = {
46
+ path: {
47
+ status: 'added' | 'unchanged' | 'in-path' | 'disabled' | 'cancelled';
48
+ /** canonical dir added to the PATH */
49
+ dir: string;
50
+ shell: Shell;
51
+ files: PathSetupFile[];
52
+ backups: string[];
53
+ /** manual line for the shell */
54
+ command: string;
55
+ /** loads it in the current shell */
56
+ source: string;
57
+ };
32
58
  };
33
59
  export declare const views: {
34
- readonly human: (result: SetupResult) => string;
60
+ readonly human: (result: SetupResult | SetupPathResult) => string;
35
61
  };
36
62
  export declare const usage: CommandUsage;
37
- export declare const command: CommandFn<SetupResult>;
63
+ export declare const command: CommandFn<SetupResult | SetupPathResult>;
@@ -8,6 +8,10 @@ import { registrySelectionFields } from "../config/merge-layers.js";
8
8
  import { commandUsage } from "../config/usage.js";
9
9
  import { normalizeRegistryURL } from "../require-registry.js";
10
10
  import { stdout } from "../output.js";
11
+ import { resolveGlobalBinDir } from "../global-bin-dir.js";
12
+ import { detectShell, prettyPath, shellPathCommand, } from "../shell.js";
13
+ import { applyPathSetup, ask as askLine, pathSetupChanged, pathSetupFiles, pathSetupLines, planPathSetup, rcList, sourceCommand, } from "../shell-setup/index.js";
14
+ import { editPathHint } from "../shell-setup/windows.js";
11
15
  /** Where users sign up for / log into a vlt.io account. */
12
16
  export const VLT_SIGNUP_URL = 'https://www.vlt.io';
13
17
  /** Base URL that per-account registries live under. */
@@ -58,8 +62,47 @@ const accountAuthError = (account, er) => (isErrorWithCause(er) &&
58
62
  `organization at ${VLT_SIGNUP_URL}, then run \`vlt setup\` again.`,
59
63
  ].join('\n'), { code: 'ECONFIG', cause: er })
60
64
  : asError(er);
65
+ const pathView = (r, wizard = false) => {
66
+ if (r.status === 'failed') {
67
+ return `⚠️ Could not add the global bin folder to your PATH: ${r.message}`;
68
+ }
69
+ const dir = prettyPath(r.dir);
70
+ const rcs = rcList(r.files);
71
+ // the command lasts one session
72
+ const keep = process.platform === 'win32' ?
73
+ [` To keep it: ${editPathHint}`]
74
+ : [];
75
+ switch (r.status) {
76
+ case 'added':
77
+ return pathSetupLines(r, r.source).join('\n');
78
+ case 'unchanged':
79
+ return [
80
+ `✅ ${dir} is already added to your PATH in ${rcs}`,
81
+ ` Open a new terminal, or run: ${r.source}`,
82
+ ].join('\n');
83
+ case 'in-path':
84
+ return wizard ? '' : `✅ ${dir} is already on your PATH`;
85
+ case 'disabled':
86
+ return [
87
+ `ℹ️ no-modify-path is set, so nothing was changed. To add ${dir} to your PATH, run:`,
88
+ ` ${r.command}`,
89
+ ...keep,
90
+ ].join('\n');
91
+ case 'cancelled':
92
+ if (wizard) {
93
+ return `ℹ️ Skipped. Run \`vlt setup --path\` later to add ${dir} to your PATH.`;
94
+ }
95
+ return [
96
+ `Nothing changed. To add ${dir} to your PATH, run:`,
97
+ ` ${r.command}`,
98
+ ...keep,
99
+ ].join('\n');
100
+ }
101
+ };
61
102
  export const views = {
62
103
  human: (result) => {
104
+ if (!('account' in result))
105
+ return pathView(result.path);
63
106
  const lines = [
64
107
  `Configured ${Object.keys(result.registries).length} registry ${Object.keys(result.registries).length === 1 ?
65
108
  'alias'
@@ -71,13 +114,19 @@ export const views = {
71
114
  if (result.shadowedByProject) {
72
115
  lines.push('', `Note: this project's vlt.json configures its own registries, and`, 'project config takes precedence. Re-run with `--config=project`', 'to configure this project instead.');
73
116
  }
117
+ const path = result.path && pathView(result.path, true);
118
+ if (path)
119
+ lines.push('', path);
74
120
  lines.push('', 'You can now install packages, e.g.:', ' vlt install <pkg>');
75
121
  return lines.join('\n');
76
122
  },
77
123
  };
78
124
  export const usage = () => commandUsage({
79
125
  command: 'setup',
80
- usage: '[<account>]',
126
+ usage: [
127
+ '[<account>]',
128
+ '--path [--yes] [--global-bin-dir=<path>]',
129
+ ],
81
130
  description: `Configure the registry aliases for your vlt.io account so
82
131
  that \`vlt install\` and \`vlx\` work out of the box.
83
132
 
@@ -85,7 +134,19 @@ export const usage = () => commandUsage({
85
134
  authenticates against your account, stores the token
86
135
  for both the \`npm\` and \`main\` registries, and writes
87
136
  those aliases (and any additional aliases you add) to
88
- your user \`vlt.json\`.`,
137
+ your user \`vlt.json\`.
138
+
139
+ In a terminal, if the global bin folder isn't on
140
+ your \`PATH\` or set up yet, it ends by asking
141
+ \`Setup PATH for global installs? (Y/n)\`: yes adds
142
+ it (same as \`--path\`), no skips it. Run
143
+ \`vlt setup --path\` later to do just that step.
144
+
145
+ With \`--path\`, add the global bin folder
146
+ (\`--global-bin-dir\`) to your \`PATH\` instead: writes
147
+ \`<global-dir>/env\` and a marked section that loads
148
+ it into your shell config. On Windows it adds it to
149
+ your user \`Path\` (\`HKCU\\Environment\`).`,
89
150
  options: {
90
151
  config: {
91
152
  value: '<user | project>',
@@ -99,11 +160,129 @@ export const usage = () => commandUsage({
99
160
  },
100
161
  yes: {
101
162
  description: 'Skip interactive prompts (requires <account> and does not ' +
102
- 'open a browser for authentication).',
163
+ 'open a browser for authentication). With --path: edit ' +
164
+ 'without asking. Without --path: skips the PATH prompt.',
165
+ },
166
+ path: {
167
+ description: 'Only add the global bin folder to your PATH via your ' +
168
+ 'shell config (user Path on Windows).',
169
+ },
170
+ 'global-bin-dir': {
171
+ value: '<path>',
172
+ description: 'The folder to add to your PATH.',
103
173
  },
104
174
  },
105
175
  });
176
+ const runPathSetup = async (o) => {
177
+ const root = o.globalDir;
178
+ const bin = await resolveGlobalBinDir({
179
+ projectRoot: root,
180
+ globalBinDir: o.globalBinDir,
181
+ });
182
+ const shell = detectShell();
183
+ const base = {
184
+ dir: bin.dir,
185
+ shell,
186
+ files: [],
187
+ backups: [],
188
+ command: shellPathCommand(shell, bin.dir),
189
+ source: sourceCommand(shell, root, bin.dir),
190
+ };
191
+ if (bin.inPath) {
192
+ return { ...base, status: 'in-path' };
193
+ }
194
+ // scripted installers must not fail
195
+ if (o.noModifyPath) {
196
+ return { ...base, status: 'disabled' };
197
+ }
198
+ const opts = { globalDir: root, dir: bin.dir, shell };
199
+ let plan = await planPathSetup(opts);
200
+ const files = pathSetupFiles(plan);
201
+ if (!pathSetupChanged(plan)) {
202
+ return { ...base, status: 'unchanged', files };
203
+ }
204
+ if (!o.yes) {
205
+ if (!process.stdin.isTTY) {
206
+ throw error('Not a terminal: pass --yes to add it to your PATH', {
207
+ code: 'EUSAGE',
208
+ });
209
+ }
210
+ const answer = await (o.ask ?? askLine)(o.question({ dir: prettyPath(bin.dir), rcs: rcList(files) })).catch((er) => {
211
+ // Ctrl-C
212
+ if (asError(er).name !== 'AbortError')
213
+ throw er;
214
+ process.stdout.write('\n');
215
+ return 'n';
216
+ });
217
+ if (/^n/i.test(answer)) {
218
+ return { ...base, status: 'cancelled', files };
219
+ }
220
+ // rc files may have changed while asking
221
+ plan = await planPathSetup(opts);
222
+ }
223
+ const r = await applyPathSetup(plan);
224
+ return {
225
+ ...base,
226
+ status: 'added',
227
+ files: r.files,
228
+ backups: r.backups,
229
+ };
230
+ };
231
+ const setupPath = async (conf, root) => {
232
+ if (conf.positionals.length) {
233
+ throw error('`vlt setup --path` takes no arguments', {
234
+ code: 'EUSAGE',
235
+ found: conf.positionals[0],
236
+ });
237
+ }
238
+ return {
239
+ path: await runPathSetup({
240
+ globalDir: root,
241
+ globalBinDir: conf.globalBinDir,
242
+ yes: !!conf.get('yes'),
243
+ noModifyPath: !!conf.get('no-modify-path'),
244
+ question: ({ dir, rcs }) => `Add ${dir} to your PATH in ${rcs}? (Y/n) `,
245
+ }),
246
+ };
247
+ };
248
+ // end of the wizard, in a terminal only
249
+ const offerPath = async (conf, yes, ask) => {
250
+ if (yes || !process.stdin.isTTY || conf.get('no-modify-path')) {
251
+ return;
252
+ }
253
+ const g = conf.globalProject();
254
+ if (!g)
255
+ return;
256
+ try {
257
+ return await runPathSetup({
258
+ globalDir: g.root,
259
+ globalBinDir: g.binDir,
260
+ question: () => 'Setup PATH for global installs? (Y/n) ',
261
+ ask,
262
+ });
263
+ }
264
+ catch (er) {
265
+ // registries are already saved
266
+ const e = asError(er);
267
+ // e.g. the global-bin-dir that could not be created
268
+ const found = (isErrorWithCause(e) &&
269
+ isObject(e.cause) &&
270
+ typeof e.cause.found === 'string') ?
271
+ prettyPath(e.cause.found)
272
+ : undefined;
273
+ return {
274
+ status: 'failed',
275
+ message: found && !e.message.includes(found) ?
276
+ `${e.message}: ${found}`
277
+ : e.message,
278
+ };
279
+ }
280
+ };
106
281
  export const command = async (conf) => {
282
+ // set only by --path, see Config#globalProject
283
+ const root = conf.globalRoot;
284
+ if (root)
285
+ return setupPath(conf, root);
107
286
  const yes = !!conf.get('yes');
108
287
  const which = configWriteTarget(conf, 'user');
109
288
  let rl;
@@ -198,11 +377,14 @@ export const command = async (conf) => {
198
377
  // worked.
199
378
  const shadowedByProject = which === 'user' &&
200
379
  registrySelectionFields.some(f => f in (conf.layers.project ?? {}));
380
+ // 8. offer PATH setup for global installs
381
+ const path = await offerPath(conf, yes, ask);
201
382
  return {
202
383
  account,
203
384
  which,
204
385
  registries,
205
386
  ...(shadowedByProject ? { shadowedByProject } : undefined),
387
+ ...(path ? { path } : undefined),
206
388
  };
207
389
  }
208
390
  finally {