@vltpkg/cli-sdk 1.4.0 → 1.5.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.
@@ -0,0 +1,268 @@
1
+ import { styleText } from 'node:util';
2
+ import { error } from '@vltpkg/error-cause';
3
+ import { assertQuery, linkSkills, listSkills, mermaidOutput, } from '@vltpkg/graph';
4
+ import { configLabel, savedLine, storedAllowSkills, unmatchedLines, unscannedLines, } from "../allow-skills.js";
5
+ import { configWriteTarget } from "../config/index.js";
6
+ import { commandUsage } from "../config/usage.js";
7
+ import { confirm, editQuery, interactive } from "../skills-prompt.js";
8
+ export const needsRegistry = true;
9
+ export const usage = () => commandUsage({
10
+ command: 'skills',
11
+ usage: ['', 'list [<query>]', 'link [<query>]'],
12
+ description: `Manage the agent skills shipped by installed packages.
13
+
14
+ A skill is a folder with a \`SKILL.md\` file
15
+ (https://agentskills.io/specification). Packages ship them as a
16
+ root \`SKILL.md\`, or at \`<name>/SKILL.md\` in \`skills/\`,
17
+ \`dist/skills/\` or \`.agents/skills/\`: the first skill of a name
18
+ wins.
19
+
20
+ Skills are linked into the project's agent skill dirs:
21
+ \`.agents/skills\`, plus \`<folder>/skills\` for each agent folder
22
+ at the project root (e.g. \`.claude/\`), or the \`skills-dir\`
23
+ config. When packages ship skills with the same name, one is
24
+ linked and the others are reported. Only links made by vlt
25
+ (pointing into \`node_modules/.vlt\`) are ever replaced or
26
+ removed; anything else in the way is skipped.
27
+
28
+ Use --target or a query after the subcommand to select packages
29
+ using DSS query syntax. End a selector with
30
+ \`:skill(<name>, …)\` to pick single skills, e.g.
31
+ \`#skills:skill(find-skills)\`. Use
32
+ \`vlt install --allow-skills=<query>\` to link skills on install.`,
33
+ subcommands: {
34
+ list: {
35
+ usage: '[<query>]',
36
+ description: `List the skills of installed packages. The default.
37
+ Selects all packages (\`*\`) by default. Applies
38
+ \`:skill()\`: preview a query before \`link\` saves
39
+ it.`,
40
+ },
41
+ link: {
42
+ usage: '[<query>]',
43
+ description: `Set, save and apply \`allow-skills\`: link what the
44
+ query selects and remove vlt links to other skills.
45
+ With no query: the stored one, or
46
+ \`:not(:malware)\`. Asks before replacing or editing
47
+ a stored query (-y skips). Saves to the project
48
+ vlt.json, or the user one with --config=user.`,
49
+ },
50
+ },
51
+ options: {
52
+ target: {
53
+ value: '<query>',
54
+ description: 'Query selector to filter packages using DSS syntax.',
55
+ },
56
+ yes: {
57
+ description: 'Skip the prompts: replace or keep the stored allow-skills without asking.',
58
+ },
59
+ config: {
60
+ value: 'user|project',
61
+ description: 'vlt.json to save allow-skills to (default: project).',
62
+ },
63
+ view: {
64
+ value: '[human | json | mermaid]',
65
+ description: 'Output format. mermaid: list only.',
66
+ },
67
+ },
68
+ });
69
+ const plural = (n, s) => `${n} ${s}${n === 1 ? '' : 's'}`;
70
+ const lines = (header, items) => [header, ...items.map(i => ` ${i}`)].join('\n');
71
+ // warnings shared by list and link
72
+ const notes = (r) => [
73
+ ...(r.skipped ? [`⚠️ Skipped ${r.skipped.join(', ')}`] : []),
74
+ ...(r.gitignore ?
75
+ [`⚠️ Could not update ${r.gitignore.join(', ')}`]
76
+ : []),
77
+ ...unscannedLines(r),
78
+ ];
79
+ // `vlt ls` style
80
+ const pkgLabel = (s) => {
81
+ const pkg = s.version ? `${s.name}@${s.version}` : s.name;
82
+ return s.alias ? `${s.alias} (${pkg})` : pkg;
83
+ };
84
+ const linkedSuffix = (s, dirs) => dirs.length && dirs.every(d => s.linked.includes(d)) ? ' (linked)'
85
+ : s.linked.length ? ` (linked in ${s.linked.join(', ')})`
86
+ : '';
87
+ const row = (s, dirs) => `${s.skill} (${pkgLabel(s)})${linkedSuffix(s, dirs)}`;
88
+ const listNotes = (r) => [
89
+ ...(!r.dirs.length && !r.skipped ?
90
+ ['No agent skill dirs: skills-dir is empty']
91
+ : []),
92
+ ...notes(r),
93
+ ];
94
+ // colors as `vlt ls` / `vlt query`: linked skills = selected nodes
95
+ const humanList = (r, { colors } = {}) => {
96
+ const style = (format) => (s) => colors ? styleText(format, s, { validateStream: false }) : s;
97
+ const yellow = style('yellow');
98
+ const rows = r.skills.map(s => {
99
+ const x = row(s, r.dirs);
100
+ return s.linked.length ? yellow(x) : x;
101
+ });
102
+ const last = rows.length - 1;
103
+ return [
104
+ ...(rows.length ?
105
+ [
106
+ style('reset')([
107
+ r.project,
108
+ ...rows.map((x, i) => `${i === last ? '└──' : '├──'} ${x}`),
109
+ ].join('\n')),
110
+ ]
111
+ : ['No agent skills found in installed packages.']),
112
+ ...listNotes(r),
113
+ ].join('\n');
114
+ };
115
+ // the dependency graph down to each package, its skills as leaves
116
+ const mermaidList = (r) => {
117
+ const children = new Map();
118
+ for (const s of r.skills) {
119
+ const list = children.get(s.id) ?? [];
120
+ list.push({
121
+ edge: pkgLabel(s),
122
+ label: `${s.skill}${linkedSuffix(s, r.dirs)}`,
123
+ });
124
+ children.set(s.id, list);
125
+ }
126
+ const { graph } = r;
127
+ return [
128
+ mermaidOutput({
129
+ edges: [],
130
+ importers: new Set([graph.mainImporter]),
131
+ nodes: [...graph.nodes.values()].filter(n => n === graph.mainImporter || children.has(n.id)),
132
+ children,
133
+ }),
134
+ ...listNotes(r).map(n => `%% ${n}`),
135
+ ].join('\n');
136
+ };
137
+ // `name`: the skill, `dependency-name`: the dep it's installed as,
138
+ // `package-id`: its package
139
+ const jsonSkill = ({ skill, name, alias, version, id, description, ...rest }) => ({
140
+ name: skill,
141
+ description,
142
+ 'dependency-name': alias ?? name,
143
+ 'package-id': id,
144
+ ...rest,
145
+ });
146
+ // distinct skills, in any number of dirs
147
+ const count = (rows) => new Set(rows.map(r => `${r.id}\0${r.skill}`)).size;
148
+ const humanLink = (r) => {
149
+ const out = [];
150
+ if (r.linked.length) {
151
+ out.push(lines(`🔗 Linked ${plural(count(r.linked), 'agent skill')}:`, r.linked.map(s => s.mount)));
152
+ }
153
+ else if (r.unchanged.length) {
154
+ out.push(`${plural(count(r.unchanged), 'agent skill')} already linked.`);
155
+ }
156
+ if (r.removed.length) {
157
+ out.push(lines(`🧹 Removed ${plural(r.removed.length, 'skill link')}:`, r.removed));
158
+ }
159
+ if (r.conflicts.length) {
160
+ out.push(lines(`⚠️ Skipped ${plural(r.conflicts.length, 'skill')}, path in use or taken by another package:`, r.conflicts));
161
+ }
162
+ out.push(...notes(r));
163
+ return out.join('\n') || 'No agent skills found to link.';
164
+ };
165
+ const human = (r, o) => r.action === 'list' ? humanList(r, o)
166
+ : r.action === 'unchanged' ? 'No changes made.'
167
+ : [
168
+ humanLink(r),
169
+ ...unmatchedLines(r.unmatched),
170
+ ...(r.saved ? [savedLine(r.query, r.saved)] : []),
171
+ ].join('\n');
172
+ export const views = {
173
+ human,
174
+ json: (r) => {
175
+ if (r.action !== 'list')
176
+ return r;
177
+ // key order kept
178
+ const { project, graph, ...rest } = r;
179
+ return { ...rest, skills: rest.skills.map(jsonSkill) };
180
+ },
181
+ mermaid: (r) => r.action === 'list' ? mermaidList(r) : human(r),
182
+ };
183
+ const defaultQuery = ':not(:malware)';
184
+ const where = (which) => which === 'user' ?
185
+ 'in the user vlt.json'
186
+ : 'for this project (vlt.json)';
187
+ // the query to link; undefined: no changes
188
+ const pickQuery = async (conf, given) => {
189
+ const check = (q) => assertQuery(q, 'allow-skills', ['skill']);
190
+ const stored = storedAllowSkills(conf);
191
+ const yes = !!conf.get('yes');
192
+ const tty = interactive();
193
+ if (given) {
194
+ await check(given);
195
+ if (!stored || stored.query === given || yes)
196
+ return given;
197
+ if (!tty) {
198
+ throw error(`allow-skills is already set (${configLabel(stored.which)}): "${stored.query}"; pass -y to replace it`, { code: 'EUSAGE', found: given, wanted: stored.query });
199
+ }
200
+ if (!(await confirm(`allow-skills is already set ${where(stored.which)}: "${stored.query}". Replace it? [y/N] `))) {
201
+ return undefined;
202
+ }
203
+ return editQuery({ current: stored.query, prefill: given, check });
204
+ }
205
+ if (stored) {
206
+ if (yes || !tty)
207
+ return stored.query;
208
+ const edit = await confirm(`allow-skills is set ${where(stored.which)}: "${stored.query}". Edit it? [y/N] `);
209
+ // ctrl+C/D: cancel
210
+ if (edit === undefined)
211
+ return undefined;
212
+ return edit ?
213
+ editQuery({
214
+ current: stored.query,
215
+ prefill: stored.query,
216
+ check,
217
+ })
218
+ : stored.query;
219
+ }
220
+ if (yes || !tty)
221
+ return defaultQuery;
222
+ return editQuery({ prefill: defaultQuery, check });
223
+ };
224
+ export const command = async (conf) => {
225
+ const [sub = 'list', query] = conf.positionals;
226
+ const target = conf.get('target') || query;
227
+ const opts = {
228
+ ...conf.options,
229
+ projectRoot: conf.projectRoot,
230
+ skillsDirs: conf.get('skills-dir'),
231
+ };
232
+ switch (sub) {
233
+ case 'list':
234
+ case 'ls':
235
+ return {
236
+ action: 'list',
237
+ ...(await listSkills({ ...opts, target })),
238
+ };
239
+ case 'link': {
240
+ const query = await pickQuery(conf, target);
241
+ if (query === undefined)
242
+ return { action: 'unchanged' };
243
+ const res = await linkSkills({
244
+ ...opts,
245
+ target: query,
246
+ apply: true,
247
+ });
248
+ // after linking: a link error saves nothing
249
+ let saved;
250
+ if (storedAllowSkills(conf)?.query !== query) {
251
+ saved = configWriteTarget(conf, 'project');
252
+ await conf.addConfigToFile(saved, { 'allow-skills': query });
253
+ }
254
+ return {
255
+ action: 'link',
256
+ query,
257
+ ...(saved && { saved }),
258
+ ...res,
259
+ };
260
+ }
261
+ default:
262
+ throw error('Unknown skills subcommand', {
263
+ code: 'EUSAGE',
264
+ found: sub,
265
+ validOptions: ['list', 'ls', 'link'],
266
+ });
267
+ }
268
+ };
@@ -1,10 +1,14 @@
1
- import type { Graph } from '@vltpkg/graph';
1
+ import type { Graph, LinkSkillsResult } from '@vltpkg/graph';
2
2
  import type { CommandFn, CommandUsage } from '../index.ts';
3
3
  export type UninstallResult = {
4
4
  /**
5
5
  * The resulting graph structure at the end of an uninstall.
6
6
  */
7
7
  graph: Graph;
8
+ /**
9
+ * Agent skill links added or removed.
10
+ */
11
+ skills?: LinkSkillsResult;
8
12
  };
9
13
  export declare const needsRegistry = true;
10
14
  export declare const needsNpmRegistry = true;
@@ -39,10 +39,12 @@ export const command = async (conf) => {
39
39
  String(conf.get('allow-scripts'))
40
40
  : ':not(*)';
41
41
  /* c8 ignore stop */
42
- const { graph } = await uninstall({
42
+ const { graph, skills } = await uninstall({
43
43
  ...conf.options,
44
44
  allowScripts,
45
+ allowSkills: conf.get('allow-skills'),
46
+ skillsDirs: conf.get('skills-dir'),
45
47
  lockfileOnly: conf.options['lockfile-only'],
46
48
  }, remove);
47
- return { graph };
49
+ return { graph, ...(skills ? { skills } : null) };
48
50
  };
@@ -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 CommandResult = {
5
4
  /** The package name */
@@ -1,9 +1,10 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
- import { RegistryClient, assertOk, requestError, } from '@vltpkg/registry-client';
2
+ import { RegistryClient, assertOk, registryBase, requestError, } from '@vltpkg/registry-client';
3
3
  import { Spec } from '@vltpkg/spec';
4
+ import { dirname } from 'node:path';
4
5
  import { commandUsage } from "../config/usage.js";
6
+ import { getPublishConfig } from "../publish-config.js";
5
7
  import { resolveRegistry } from "../require-registry.js";
6
- export const needsRegistry = true;
7
8
  export const usage = () => commandUsage({
8
9
  command: 'unpublish',
9
10
  usage: ['<package>@<version>', '<package> --force'],
@@ -11,6 +12,8 @@ export const usage = () => commandUsage({
11
12
 
12
13
  To unpublish a single version, specify the package name and version.
13
14
  To unpublish an entire package, specify the package name and use --force.
15
+ A spec naming the package in the current directory uses its
16
+ \`publishConfig.registry\`, if set.
14
17
 
15
18
  ⚠️ Unpublishing is a destructive action that cannot be undone.
16
19
  Consider using \`vlt deprecate\` instead if you want to discourage
@@ -45,14 +48,22 @@ export const views = {
45
48
  },
46
49
  json: (r) => r,
47
50
  };
51
+ /** `publishConfig.registry` of the local package, if it is `name` */
52
+ const localPublishRegistry = (conf, name) => {
53
+ const path = conf.options.packageJson.find(process.cwd());
54
+ if (!path)
55
+ return undefined;
56
+ const manifest = conf.options.packageJson.read(dirname(path));
57
+ return manifest.name === name ?
58
+ getPublishConfig(manifest)?.registry
59
+ : undefined;
60
+ };
48
61
  export const command = async (conf) => {
49
62
  const specArg = conf.positionals[0];
50
63
  if (!specArg) {
51
64
  throw error('unpublish requires a package spec argument (e.g. pkg@version)', { code: 'EUSAGE' });
52
65
  }
53
66
  const { otp, force } = conf.options;
54
- const registry = await resolveRegistry(conf);
55
- const registryUrl = new URL(registry);
56
67
  const spec = Spec.parseArgs(specArg, conf.options);
57
68
  const name = spec.name;
58
69
  /* c8 ignore start - Spec.parseArgs always produces a name for valid input */
@@ -71,6 +82,8 @@ export const command = async (conf) => {
71
82
  'To unpublish all versions, use: vlt unpublish <package> --force');
72
83
  }
73
84
  }
85
+ const registryUrl = new URL(registryBase(localPublishRegistry(conf, name) ??
86
+ (await resolveRegistry(conf))));
74
87
  const rc = new RegistryClient(conf.options);
75
88
  const encodedName = name.startsWith('@') ? name.replace('/', '%2F') : name;
76
89
  if (version) {
@@ -5,6 +5,18 @@ 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
+ allowSkills?: import("../allow-skills.ts").AllowSkillsSave | undefined;
9
+ skills?: {
10
+ unscanned?: string[];
11
+ unscannedReason?: "timeout" | "unscanned";
12
+ removed: string[];
13
+ conflicts: string[];
14
+ dirs: string[];
15
+ skipped?: string[];
16
+ gitignore?: string[];
17
+ unmatched?: import("@vltpkg/graph").UnmatchedPattern[];
18
+ linked: string[];
19
+ } | undefined;
8
20
  persistedConfig?: import("../persist-spec-config.ts").SpecConfigPersistPlan | undefined;
9
21
  graph: import("@vltpkg/graph").LockfileData;
10
22
  buildQueue?: import("@vltpkg/dep-id").DepID[] | undefined;
@@ -1,6 +1,7 @@
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 { saveAllowSkills } from "../allow-skills.js";
4
5
  import { planSpecConfigPersist } from "../persist-spec-config.js";
5
6
  import { asUnknownSpecPrefix } from "../require-registry.js";
6
7
  import { lazyView } from "../view.js";
@@ -28,8 +29,17 @@ export const usage = () => commandUsage({
28
29
  value: '<query>',
29
30
  description: 'Filter which packages are allowed to run lifecycle scripts using DSS query syntax.',
30
31
  },
32
+ 'allow-skills': {
33
+ value: '<query>',
34
+ description: "Link agent skills of packages matching this DSS query into the project's agent skill dirs. Saved to vlt.json.",
35
+ },
31
36
  },
32
37
  });
38
+ // link mounts; `unchanged` left out
39
+ const skillsJson = ({ linked, unchanged: _, ...rest }) => ({
40
+ linked: linked.map(s => s.mount),
41
+ ...rest,
42
+ });
33
43
  export const views = {
34
44
  json: i => ({
35
45
  ...(i.buildQueue?.length ?
@@ -42,6 +52,8 @@ export const views = {
42
52
  ...(i.persistedConfig ?
43
53
  { persistedConfig: i.persistedConfig }
44
54
  : null),
55
+ ...(i.skills ? { skills: skillsJson(i.skills) } : null),
56
+ ...(i.allowSkills ? { allowSkills: i.allowSkills } : null),
45
57
  }),
46
58
  human: lazyView(async () => (await import('./install/reporter.ts')).InstallReporter),
47
59
  };
@@ -58,17 +70,22 @@ export const command = async (conf) => {
58
70
  String(conf.get('allow-scripts'))
59
71
  : ':not(*)';
60
72
  /* c8 ignore stop */
61
- const { buildQueue, graph } = await update({
73
+ const { buildQueue, graph, skills } = await update({
62
74
  ...conf.options,
63
75
  allowScripts,
76
+ allowSkills: conf.get('allow-skills'),
77
+ skillsDirs: conf.get('skills-dir'),
64
78
  }).catch((er) => {
65
79
  throw asUnknownSpecPrefix(er);
66
80
  });
67
81
  if (persist)
68
82
  await conf.addConfigToFile(persist.which, persist.values);
83
+ const allowSkillsSave = await saveAllowSkills(conf);
69
84
  return {
70
85
  buildQueue,
71
86
  graph,
72
87
  ...(persist ? { persistedConfig: persist } : null),
88
+ ...(skills ? { skills } : null),
89
+ ...(allowSkillsSave ? { allowSkills: allowSkillsSave } : null),
73
90
  };
74
91
  };
@@ -53,6 +53,7 @@ export declare const commands: {
53
53
  readonly 'run-exec': "run-exec";
54
54
  readonly run: "run";
55
55
  readonly setup: "setup";
56
+ readonly skills: "skills";
56
57
  readonly token: "token";
57
58
  readonly uninstall: "uninstall";
58
59
  readonly unpublish: "unpublish";
@@ -371,6 +372,10 @@ export declare const definition: import("jackspeak").Jack<{
371
372
  'save-config': {
372
373
  description: string;
373
374
  };
375
+ 'migrate-workspaces': {
376
+ description: string;
377
+ default: true;
378
+ };
374
379
  } & {
375
380
  'save-dev': import("jackspeak").ConfigOption<"boolean", false, undefined>;
376
381
  'save-exact': import("jackspeak").ConfigOption<"boolean", false, undefined>;
@@ -379,6 +384,7 @@ export declare const definition: import("jackspeak").Jack<{
379
384
  'save-prod': import("jackspeak").ConfigOption<"boolean", false, undefined>;
380
385
  'save-types': import("jackspeak").ConfigOption<"boolean", false, undefined>;
381
386
  'save-config': import("jackspeak").ConfigOption<"boolean", false, undefined>;
387
+ 'migrate-workspaces': import("jackspeak").ConfigOption<"boolean", false, undefined>;
382
388
  } & {
383
389
  'save-prefix': {
384
390
  description: string;
@@ -429,6 +435,20 @@ export declare const definition: import("jackspeak").Jack<{
429
435
  };
430
436
  } & {
431
437
  'allow-scripts': import("jackspeak").ConfigOption<"string", false, readonly string[] | undefined>;
438
+ } & {
439
+ 'allow-skills': {
440
+ hint: string;
441
+ description: string;
442
+ };
443
+ } & {
444
+ 'allow-skills': import("jackspeak").ConfigOption<"string", false, readonly string[] | undefined>;
445
+ } & {
446
+ 'skills-dir': {
447
+ hint: string;
448
+ description: string;
449
+ };
450
+ } & {
451
+ 'skills-dir': import("jackspeak").ConfigOption<"string", true, readonly string[] | undefined>;
432
452
  } & {
433
453
  access: {
434
454
  description: string;
@@ -46,6 +46,7 @@ const canonicalCommands = {
46
46
  'run-exec': 'run-exec',
47
47
  run: 'run',
48
48
  setup: 'setup',
49
+ skills: 'skills',
49
50
  token: 'token',
50
51
  uninstall: 'uninstall',
51
52
  unpublish: 'unpublish',
@@ -525,7 +526,9 @@ export const definition = j
525
526
  config: {
526
527
  hint: 'all | user | project',
527
528
  description: `Specify which configuration to show or operate on when running
528
- \`vlt config\` commands. For read operations (get, pick, list):
529
+ \`vlt config\` commands, and which file
530
+ \`vlt skills link\` saves \`allow-skills\` to. For read
531
+ operations (get, pick, list):
529
532
  \`all\` shows merged configuration from both user and project
530
533
  files (default). For write operations (set, delete, edit):
531
534
  defaults to \`project\`. \`user\` shows/modifies only user-level
@@ -729,6 +732,17 @@ export const definition = j
729
732
  \`add\` or \`update\`. Values a config file already
730
733
  has are left alone.`,
731
734
  },
735
+ 'migrate-workspaces': {
736
+ description: `Save workspaces read from the \`workspaces\` field of
737
+ the root \`package.json\` or from
738
+ \`pnpm-workspace.yaml\` to the project \`vlt.json\` on
739
+ \`vlt install\`. \`vlt.json\` then takes precedence.
740
+
741
+ Set \`--no-migrate-workspaces\` to skip this; an
742
+ already saved \`workspaces\` field stays until
743
+ removed.`,
744
+ default: true,
745
+ },
732
746
  })
733
747
  .opt({
734
748
  'save-prefix': {
@@ -796,6 +810,40 @@ export const definition = j
796
810
  Runs scripts only for direct dependencies of the current project and any occurrences
797
811
  of a specific dependency with the name "my-package" anywhere in the dependency graph.`,
798
812
  },
813
+ })
814
+ .opt({
815
+ 'allow-skills': {
816
+ hint: 'query',
817
+ description: `Link the agent skills (folders with a \`SKILL.md\`) shipped by
818
+ installed packages matching this DSS query into
819
+ the project's agent skill dirs (see \`skills-dir\`)
820
+ after \`install\`, \`ci\`, \`update\` or
821
+ \`uninstall\`. End a selector with
822
+ \`:skill(<name>, …)\` to pick single skills (\`*\`
823
+ globs, double-quoted names). Unset (the default) or
824
+ \`:not(*)\` links nothing and skips all skills work.
825
+
826
+ \`vlt install --allow-skills=<query>\` saves the query
827
+ to the project \`vlt.json\`; \`vlt skills link
828
+ [<query>]\` sets, saves and applies it.
829
+
830
+ Example: --allow-skills="#agent-browser:skill(agent-browser)"
831
+ Links the agent-browser skill from a package with the same name.`,
832
+ },
833
+ })
834
+ .optList({
835
+ 'skills-dir': {
836
+ hint: 'path',
837
+ description: `Dirs to link agent skills into (see \`allow-skills\`),
838
+ relative to the project root. Replaces detection:
839
+ \`.agents/skills\`, plus \`<folder>/skills\` for each
840
+ agent folder at the project root (e.g. \`.claude/\`),
841
+ \`.claude/skills\` inside Claude Code, and existing
842
+ \`.<name>/skills\` dirs. An empty list (\`[]\` in
843
+ vlt.json, or \`--skills-dir=\`) links nothing. Each
844
+ dir must be inside the project, not in
845
+ node_modules.`,
846
+ },
799
847
  })
800
848
  .opt({
801
849
  access: {
@@ -183,6 +183,10 @@ const commandHelp = {
183
183
  desc: 'Configure your vlt.io account registries',
184
184
  defaultOrder: 0,
185
185
  },
186
+ skills: {
187
+ args: '[list|link]',
188
+ desc: 'Manage agent skills from installed packages',
189
+ },
186
190
  token: {
187
191
  args: '[add|rm]',
188
192
  desc: 'Manage authentication tokens',
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { format } from 'node:util';
2
2
  import { asRootError, findRootError } from '@vltpkg/output/error';
3
+ import { setVltJsonOnly } from '@vltpkg/workspaces';
3
4
  import { loadPackageJson } from 'package-json-from-dist';
4
5
  import { getSortedCliOptions, getSortedKeys, } from "./config/definition.js";
5
6
  import { Config } from "./config/index.js";
@@ -49,6 +50,8 @@ const run = async () => {
49
50
  if (vlt.get('version')) {
50
51
  return stdout(version);
51
52
  }
53
+ // ci reads workspaces from vlt.json only; set before options load
54
+ setVltJsonOnly(vlt.command === 'ci');
52
55
  const { monorepo } = vlt.options;
53
56
  // Infer the workspace by being in that directory.
54
57
  if (vlt.get('workspace') === undefined) {
@@ -61,7 +64,10 @@ const run = async () => {
61
64
  if (vlt.command !== 'init' &&
62
65
  (vlt.get('workspace') || vlt.get('workspace-group')) &&
63
66
  ![...(monorepo?.values() ?? [])].length) {
64
- stderr(`Error: No matching workspaces found. Make sure the vlt.json config, the "workspaces" field of the root package.json, or the "packages" list of pnpm-workspace.yaml contains the correct workspaces.`);
67
+ const where = vlt.command === 'ci' ?
68
+ 'the vlt.json config (vlt ci ignores package.json and pnpm-workspace.yaml)'
69
+ : 'the vlt.json config, the "workspaces" field of the root package.json, or the "packages" list of pnpm-workspace.yaml';
70
+ stderr(`Error: No matching workspaces found. Make sure ${where} contains the correct workspaces.`);
65
71
  if (vlt.get('workspace')) {
66
72
  stderr(indent(`Workspace: ${format(vlt.get('workspace'))}`));
67
73
  }
@@ -15,6 +15,8 @@ export type Command<T> = {
15
15
  * Set to `true` by commands that hit a registry or resolve registry
16
16
  * specs. There is no default registry, so `outputCommand()` fails
17
17
  * early with an `ECONFIG` error when one of these runs unconfigured.
18
+ * `publish`/`unpublish` check in-command instead, since
19
+ * `publishConfig.registry` may suffice.
18
20
  */
19
21
  needsRegistry?: boolean;
20
22
  /**
@@ -0,0 +1,16 @@
1
+ import type { WorkspaceConfigSource } from '@vltpkg/workspaces';
2
+ import type { LoadedConfig } from './config/index.ts';
3
+ export type MigratedWorkspaces = {
4
+ source: WorkspaceConfigSource;
5
+ workspaces: string[];
6
+ };
7
+ /**
8
+ * Save workspaces read from `package.json` or `pnpm-workspace.yaml` to
9
+ * the project `vlt.json`, unless `--no-migrate-workspaces`.
10
+ */
11
+ export declare const migrateWorkspaces: (conf: LoadedConfig) => MigratedWorkspaces | undefined;
12
+ /**
13
+ * Warn when workspaces are declared only outside `vlt.json`, which
14
+ * `vlt ci` ignores.
15
+ */
16
+ export declare const warnIgnoredWorkspaces: (conf: LoadedConfig) => void;
@@ -0,0 +1,44 @@
1
+ import { save } from '@vltpkg/vlt-json';
2
+ import { migratableWSConfig, resolveWSConfig, } from '@vltpkg/workspaces';
3
+ import { stderr } from "./output.js";
4
+ // workspaces declared outside vlt.json, if any
5
+ const found = (conf) => {
6
+ const resolved = resolveWSConfig(conf.options.projectRoot, conf.options.packageJson, false);
7
+ const workspaces = migratableWSConfig(resolved);
8
+ const { source } = resolved;
9
+ if (!workspaces?.length || !source)
10
+ return;
11
+ return { source, workspaces };
12
+ };
13
+ /**
14
+ * Save workspaces read from `package.json` or `pnpm-workspace.yaml` to
15
+ * the project `vlt.json`, unless `--no-migrate-workspaces`.
16
+ */
17
+ export const migrateWorkspaces = (conf) => {
18
+ if (!conf.options['migrate-workspaces'])
19
+ return;
20
+ const migrated = found(conf);
21
+ if (migrated)
22
+ save('workspaces', migrated.workspaces);
23
+ return migrated;
24
+ };
25
+ /**
26
+ * Warn when workspaces are declared only outside `vlt.json`, which
27
+ * `vlt ci` ignores.
28
+ */
29
+ export const warnIgnoredWorkspaces = (conf) => {
30
+ const { loglevel } = conf.values;
31
+ if (loglevel === 'silent' || loglevel === 'error')
32
+ return;
33
+ let migrated;
34
+ try {
35
+ migrated = found(conf);
36
+ }
37
+ catch {
38
+ // invalid workspaces config: nothing to suggest
39
+ return;
40
+ }
41
+ if (!migrated)
42
+ return;
43
+ stderr(`Warning: vlt ci reads workspaces from vlt.json only; ignoring ${migrated.source}, its workspaces are not installed. Run \`vlt install\` to save them to vlt.json, or add them there.`);
44
+ };