@vltpkg/query 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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { EdgeLike, NodeLike } from '@vltpkg/types';
2
2
  import type { SecurityArchiveLike } from '@vltpkg/security-archive';
3
- import type { DiffFilesProvider, GetAuthHeader, HostContextsMap, ParsedSelectorToken, ParserState, QueryResponse } from './types.ts';
3
+ import type { PartPseudoName } from '@vltpkg/dss-parser';
4
+ import type { DiffFilesProvider, GetAuthHeader, HasSkills, HostContextsMap, ParsedSelectorToken, ParserState, QueryResponse } from './types.ts';
4
5
  import type { DepID } from '@vltpkg/dep-id';
5
6
  export * from './types.ts';
6
7
  export type SearchOptions = {
@@ -17,6 +18,10 @@ export type QueryOptions = {
17
18
  hostContexts?: HostContextsMap;
18
19
  diffFiles?: DiffFilesProvider;
19
20
  getAuthHeader?: GetAuthHeader;
21
+ /** for `:skills`; unset: `:skills` throws */
22
+ hasSkills?: HasSkills;
23
+ /** part selectors to allow, e.g. `['skill']`; others throw */
24
+ partSelectors?: readonly PartPseudoName[];
20
25
  };
21
26
  /**
22
27
  * The Query class is used to search the graph for nodes and edges
@@ -37,7 +42,7 @@ export declare class Query {
37
42
  * the original order is preserved.
38
43
  */
39
44
  static specificitySort(responses: QueryResponse[]): QueryResponse[];
40
- constructor({ edges, nodes, importers, retries, securityArchive, hostContexts, diffFiles, getAuthHeader, }: QueryOptions);
45
+ constructor({ edges, nodes, importers, retries, securityArchive, hostContexts, diffFiles, getAuthHeader, hasSkills, partSelectors, }: QueryOptions);
41
46
  /**
42
47
  * Search the graph for nodes and edges that match the given query.
43
48
  */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { joinDepIDTuple } from '@vltpkg/dep-id/browser';
2
- import { parse, isPostcssNodeWithChildren, asPostcssNodeWithChildren, isSelectorNode, isPseudoNode, isIdentifierNode, isAttributeNode, } from '@vltpkg/dss-parser';
2
+ import { parse, partPseudoNames, isPostcssNodeWithChildren, asPostcssNodeWithChildren, isSelectorNode, isPseudoNode, isIdentifierNode, isAttributeNode, } from '@vltpkg/dss-parser';
3
3
  import { attribute } from "./attribute.js";
4
4
  import { combinator } from "./combinator.js";
5
5
  import { id } from "./id.js";
@@ -7,6 +7,7 @@ import { pseudo, pseudoSelectorNames } from "./pseudo.js";
7
7
  import { queryError, selectorText } from "./error.js";
8
8
  export * from "./types.js";
9
9
  const noopFn = async (state) => state;
10
+ const partPseudos = new Set(partPseudoNames);
10
11
  /**
11
12
  * `.dev` and a bare `dev` are the two ways a css habit shows up in a
12
13
  * query. Neither is valid DSS, but the intent is usually a pseudo-class
@@ -16,7 +17,8 @@ const unsupportedName = (state) => {
16
17
  const text = selectorText(state.current);
17
18
  const name = text.replace(/^[.#]/, '');
18
19
  return queryError('Unsupported selector', state.current, {
19
- wanted: pseudoSelectorNames.has(name) ? `:${name}`
20
+ wanted: pseudoSelectorNames.has(name) && !partPseudos.has(name) ?
21
+ `:${name}`
20
22
  : name ? `#${name}`
21
23
  : undefined,
22
24
  });
@@ -97,6 +99,13 @@ export const walk = async (state) => {
97
99
  for (const node of state.partial.nodes) {
98
100
  state.collect.nodes.add(node);
99
101
  }
102
+ if (state.parts && node.parent?.type === 'root') {
103
+ state.parts.push({
104
+ ...state.part,
105
+ nodes: [...state.partial.nodes],
106
+ });
107
+ delete state.part;
108
+ }
100
109
  }
101
110
  }
102
111
  return state;
@@ -155,12 +164,14 @@ export class Query {
155
164
  #cache;
156
165
  #diffFiles;
157
166
  #getAuthHeader;
167
+ #hasSkills;
158
168
  #edges;
159
169
  #nodes;
160
170
  #importers;
161
171
  #retries;
162
172
  #securityArchive;
163
173
  #hostContexts;
174
+ #partSelectors;
164
175
  /**
165
176
  * Helper method to determine if a given query string is using any of
166
177
  * the known security selectors. This is useful so that operations can
@@ -194,10 +205,12 @@ export class Query {
194
205
  return 0;
195
206
  });
196
207
  }
197
- constructor({ edges, nodes, importers, retries, securityArchive, hostContexts, diffFiles, getAuthHeader, }) {
208
+ constructor({ edges, nodes, importers, retries, securityArchive, hostContexts, diffFiles, getAuthHeader, hasSkills, partSelectors, }) {
209
+ this.#partSelectors = new Set(partSelectors);
198
210
  this.#cache = new Map();
199
211
  this.#diffFiles = diffFiles;
200
212
  this.#getAuthHeader = getAuthHeader;
213
+ this.#hasSkills = hasSkills;
201
214
  this.#edges = edges;
202
215
  this.#nodes = nodes;
203
216
  this.#importers = importers;
@@ -325,7 +338,7 @@ export class Query {
325
338
  const loose = asPostcssNodeWithChildren(current).nodes.length > 1;
326
339
  // builds initial state and walks over it,
327
340
  // retrieving the collected result
328
- const { collect, comment, importers: stateResultImporters, specificity, } = await walk({
341
+ const { collect, comment, importers: stateResultImporters, parts, specificity, } = await walk({
329
342
  cancellable: async () => {
330
343
  await new Promise(resolve => {
331
344
  setTimeout(resolve, 0);
@@ -344,9 +357,13 @@ export class Query {
344
357
  comment: '',
345
358
  diffFiles: this.#diffFiles,
346
359
  getAuthHeader: this.#getAuthHeader,
360
+ hasSkills: this.#hasSkills,
347
361
  loose,
348
362
  importers,
349
363
  partial: { nodes, edges },
364
+ ...(this.#partSelectors.size ?
365
+ { parts: [], partSelectors: this.#partSelectors }
366
+ : undefined),
350
367
  retries: this.#retries,
351
368
  signal,
352
369
  securityArchive: this.#securityArchive,
@@ -361,6 +378,8 @@ export class Query {
361
378
  importers: this.#getQueryResponseNodes(stateResultImporters),
362
379
  comment,
363
380
  specificity,
381
+ // part nodes are a subset of `nodes`, already hydrated above
382
+ ...(parts ? { parts: parts } : undefined),
364
383
  };
365
384
  this.#cache.set(query, res);
366
385
  return res;
@@ -0,0 +1,7 @@
1
+ import type { ParserState } from '../types.ts';
2
+ /**
3
+ * `:skill(<pattern>, ...)`: picks agent skills of the packages its
4
+ * selector matched. Never filters nodes: the walker stores the
5
+ * patterns in the selector's `parts` entry. Opt-in only.
6
+ */
7
+ export declare const skill: (state: ParserState) => Promise<ParserState>;
@@ -0,0 +1,52 @@
1
+ import { asPseudoNode } from '@vltpkg/dss-parser';
2
+ import { queryError } from "../error.js";
3
+ import { removeQuotes } from "./helpers.js";
4
+ /**
5
+ * `:skill(<pattern>, ...)`: picks agent skills of the packages its
6
+ * selector matched. Never filters nodes: the walker stores the
7
+ * patterns in the selector's `parts` entry. Opt-in only.
8
+ */
9
+ export const skill = async (state) => {
10
+ const curr = asPseudoNode(state.current);
11
+ const sel = curr.parent;
12
+ const fail = (message) => queryError(message, state.current);
13
+ if (sel?.parent?.type !== 'root' ||
14
+ sel.nodes
15
+ .slice(sel.index(curr) + 1)
16
+ .some(n => n.type !== 'comment')) {
17
+ throw fail(':skill() must end a top-level selector');
18
+ }
19
+ if (!state.partSelectors?.has('skill')) {
20
+ throw fail(':skill() only applies to --allow-skills and vlt skills');
21
+ }
22
+ if (!curr.nodes.length)
23
+ throw fail('Missing skill name in :skill()');
24
+ const patterns = curr.nodes.map(arg => {
25
+ let pattern = '';
26
+ for (const n of arg.nodes) {
27
+ if (n.type === 'comment')
28
+ continue;
29
+ // `a|b` is tag `b` in namespace `a`: never drop the `a`
30
+ if ((n.type === 'tag' || n.type === 'universal') &&
31
+ !String(n).includes('|')) {
32
+ pattern += n.value;
33
+ }
34
+ else if (n.type === 'string') {
35
+ if (n.value.startsWith("'")) {
36
+ throw fail('Invalid skill name in :skill(), use double quotes');
37
+ }
38
+ pattern += removeQuotes(n.value);
39
+ }
40
+ else {
41
+ throw fail(n.type === 'combinator' ?
42
+ 'Invalid skill name in :skill(), quote names with spaces'
43
+ : 'Invalid skill name in :skill()');
44
+ }
45
+ }
46
+ if (!pattern)
47
+ throw fail('Missing skill name in :skill()');
48
+ return pattern;
49
+ });
50
+ state.part = { name: 'skill', patterns };
51
+ return state;
52
+ };
@@ -0,0 +1,6 @@
1
+ import type { ParserState } from '../types.ts';
2
+ /**
3
+ * `:skills`: matches installed packages that ship agent skills, as
4
+ * told by the `hasSkills` provider.
5
+ */
6
+ export declare const skills: (state: ParserState) => Promise<ParserState>;
@@ -0,0 +1,26 @@
1
+ import { asPseudoNode } from '@vltpkg/dss-parser';
2
+ import { queryError } from "../error.js";
3
+ import { clear, removeDanglingEdges, removeNode } from "./helpers.js";
4
+ /**
5
+ * `:skills`: matches installed packages that ship agent skills, as
6
+ * told by the `hasSkills` provider.
7
+ */
8
+ export const skills = async (state) => {
9
+ // `:skills(x)`: likely a `:skill(x)` typo
10
+ if (asPseudoNode(state.current).nodes.length) {
11
+ if (state.loose)
12
+ return clear(state);
13
+ throw queryError(':skills takes no arguments', state.current, {
14
+ wanted: ':skill',
15
+ });
16
+ }
17
+ if (!state.hasSkills) {
18
+ throw queryError('The :skills selector is not supported here', state.current);
19
+ }
20
+ for (const node of state.partial.nodes) {
21
+ if (!(await state.hasSkills(node)))
22
+ removeNode(state, node);
23
+ }
24
+ removeDanglingEdges(state);
25
+ return state;
26
+ };
package/dist/pseudo.js CHANGED
@@ -49,6 +49,8 @@ import { semverParser as semver } from "./pseudo/semver.js";
49
49
  import { severity } from "./pseudo/severity.js";
50
50
  import { spec } from "./pseudo/spec.js";
51
51
  import { shrinkwrap } from "./pseudo/shrinkwrap.js";
52
+ import { skill } from "./pseudo/skill.js";
53
+ import { skills } from "./pseudo/skills.js";
52
54
  import { squat } from "./pseudo/squat.js";
53
55
  import { suspicious } from "./pseudo/suspicious.js";
54
56
  import { tracker } from "./pseudo/tracker.js";
@@ -88,6 +90,7 @@ const has = async (state) => {
88
90
  nodes: new Set(state.partial.nodes),
89
91
  },
90
92
  diffFiles: state.diffFiles,
93
+ hasSkills: state.hasSkills,
91
94
  loose: state.loose,
92
95
  hostContexts: state.hostContexts,
93
96
  importers: state.importers,
@@ -164,6 +167,7 @@ const is = async (state) => {
164
167
  },
165
168
  current: node,
166
169
  diffFiles: state.diffFiles,
170
+ hasSkills: state.hasSkills,
167
171
  initial: state.initial,
168
172
  loose: true,
169
173
  partial: {
@@ -210,6 +214,7 @@ const not = async (state) => {
210
214
  },
211
215
  current: node,
212
216
  diffFiles: state.diffFiles,
217
+ hasSkills: state.hasSkills,
213
218
  initial: state.initial,
214
219
  loose: state.loose,
215
220
  partial: {
@@ -331,6 +336,8 @@ const pseudoSelectors = new Map(Object.entries({
331
336
  severity,
332
337
  shell,
333
338
  shrinkwrap,
339
+ skill,
340
+ skills,
334
341
  squat,
335
342
  suspicious,
336
343
  tracker,
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { EdgeLike, NodeLike } from '@vltpkg/types';
2
2
  import type { SecurityArchiveLike, PackageScore } from '@vltpkg/security-archive';
3
3
  import type { DepID } from '@vltpkg/dep-id';
4
- import type { PostcssNode } from '@vltpkg/dss-parser';
4
+ import type { PartPseudoName, PostcssNode } from '@vltpkg/dss-parser';
5
5
  export type HostContextsMapResult = {
6
6
  initialEdges: EdgeLike[];
7
7
  initialNodes: NodeLike[];
@@ -31,6 +31,22 @@ export type DiffFilesProvider = (commitish: string) => Set<string>;
31
31
  * keychain / registry-client dependency.
32
32
  */
33
33
  export type GetAuthHeader = (url: string) => Promise<string | undefined>;
34
+ /**
35
+ * Whether an installed package ships agent skills; for `:skills`.
36
+ * Keeps fs out of this package.
37
+ */
38
+ export type HasSkills = (node: NodeLike) => Promise<boolean>;
39
+ /**
40
+ * One top-level selector of a query and the nodes it matched. `name`
41
+ * and `patterns` are set when it ends in a part selector, e.g.
42
+ * `#a:skill(x, y*)`.
43
+ */
44
+ export type QueryPart<N = QueryResponseNode> = {
45
+ name?: PartPseudoName;
46
+ /** as typed, `*` = glob; for matching only, never paths */
47
+ patterns?: string[];
48
+ nodes: N[];
49
+ };
34
50
  export type ParserState = {
35
51
  cancellable: () => Promise<void>;
36
52
  collect: GraphSelectionState;
@@ -38,6 +54,7 @@ export type ParserState = {
38
54
  current: PostcssNode;
39
55
  diffFiles?: DiffFilesProvider;
40
56
  getAuthHeader?: GetAuthHeader;
57
+ hasSkills?: HasSkills;
41
58
  hostContexts?: HostContextsMap;
42
59
  importers: Set<NodeLike>;
43
60
  initial: GraphSelectionState;
@@ -48,6 +65,12 @@ export type ParserState = {
48
65
  signal: AbortSignal;
49
66
  walk: ParserFn;
50
67
  partial: GraphSelectionState;
68
+ /** set by `:skill()`, read and cleared by `walk()` */
69
+ part?: Pick<QueryPart, 'name' | 'patterns'>;
70
+ /** one entry per top-level selector, only when opted in */
71
+ parts?: QueryPart<NodeLike>[];
72
+ /** part selectors this consumer allows */
73
+ partSelectors?: ReadonlySet<PartPseudoName>;
51
74
  retries: number;
52
75
  securityArchive: SecurityArchiveLike | undefined;
53
76
  scopeIDs?: DepID[];
@@ -59,6 +82,11 @@ export type QueryResponse = {
59
82
  importers: QueryResponseNode[];
60
83
  comment: string;
61
84
  specificity: Specificity;
85
+ /**
86
+ * one entry per top-level selector, only with `partSelectors` and
87
+ * a non-empty query
88
+ */
89
+ parts?: QueryPart[];
62
90
  };
63
91
  export type QueryResponseEdge = Omit<EdgeLike, 'from' | 'to'> & {
64
92
  from: QueryResponseNode;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/query",
3
3
  "description": "Query syntax parser that retrieves items from a graph",
4
- "version": "1.4.0",
4
+ "version": "1.5.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,13 +13,13 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/dep-id": "1.4.0",
17
- "@vltpkg/dss-parser": "1.4.0",
18
- "@vltpkg/error-cause": "1.4.0",
19
- "@vltpkg/security-archive": "1.4.0",
20
- "@vltpkg/semver": "1.4.0",
21
- "@vltpkg/types": "1.4.0",
22
- "@vltpkg/user-agent": "1.4.0",
16
+ "@vltpkg/dep-id": "1.5.0",
17
+ "@vltpkg/dss-parser": "1.5.0",
18
+ "@vltpkg/error-cause": "1.5.0",
19
+ "@vltpkg/security-archive": "1.5.0",
20
+ "@vltpkg/semver": "1.5.0",
21
+ "@vltpkg/types": "1.5.0",
22
+ "@vltpkg/user-agent": "1.5.0",
23
23
  "minimatch": "^10.1.1",
24
24
  "p-retry": "^7.1.1",
25
25
  "postcss-selector-parser": "^7.1.1"
@@ -27,7 +27,7 @@
27
27
  "devDependencies": {
28
28
  "@eslint/js": "^9.39.1",
29
29
  "@types/node": "^22.19.2",
30
- "@vltpkg/spec": "1.4.0",
30
+ "@vltpkg/spec": "1.5.0",
31
31
  "eslint": "^9.39.1",
32
32
  "prettier": "^3.7.4",
33
33
  "tap": "^21.8.0",
@@ -62,6 +62,7 @@ Package properties:
62
62
  | `:prerelease` | version has prerelease part (`1.0.0-beta.1`) |
63
63
  | `:built` | built during reify |
64
64
  | `:scanned` | has Socket security metadata |
65
+ | `:skills` | ships agent skills (`SKILL.md`) |
65
66
 
66
67
  ## Pseudo-classes (take arguments)
67
68
 
@@ -82,6 +83,7 @@ Package properties:
82
83
  | `:diff(ref)` | files changed vs git ref | `:diff(main)` |
83
84
  | `:hostname(host)` | upstream hostname | `:hostname(github.com)` |
84
85
  | `:registry(name)` | configured registry name | `:registry(npm)` |
86
+ | `:skill(names)` | agent skills; --allow-skills/vlt skills | `#pkg:skill(a, "b c", x*)` |
85
87
 
86
88
  ## Security insights (Socket-powered, network call)
87
89
 
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { joinDepIDTuple } from '@vltpkg/dep-id/browser'
2
2
  import {
3
3
  parse,
4
+ partPseudoNames,
4
5
  isPostcssNodeWithChildren,
5
6
  asPostcssNodeWithChildren,
6
7
  isSelectorNode,
@@ -18,14 +19,17 @@ import type { SecurityArchiveLike } from '@vltpkg/security-archive'
18
19
  import type {
19
20
  PostcssNode,
20
21
  PostcssNodeWithChildren,
22
+ PartPseudoName,
21
23
  } from '@vltpkg/dss-parser'
22
24
  import type {
23
25
  DiffFilesProvider,
24
26
  GetAuthHeader,
27
+ HasSkills,
25
28
  HostContextsMap,
26
29
  ParsedSelectorToken,
27
30
  ParserState,
28
31
  ParserFn,
32
+ QueryPart,
29
33
  QueryResponse,
30
34
  QueryResponseNode,
31
35
  QueryResponseEdge,
@@ -40,6 +44,7 @@ export type SearchOptions = {
40
44
  }
41
45
 
42
46
  const noopFn = async (state: ParserState) => state
47
+ const partPseudos = new Set<string>(partPseudoNames)
43
48
 
44
49
  /**
45
50
  * `.dev` and a bare `dev` are the two ways a css habit shows up in a
@@ -51,7 +56,8 @@ const unsupportedName = (state: ParserState) => {
51
56
  const name = text.replace(/^[.#]/, '')
52
57
  return queryError('Unsupported selector', state.current, {
53
58
  wanted:
54
- pseudoSelectorNames.has(name) ? `:${name}`
59
+ pseudoSelectorNames.has(name) && !partPseudos.has(name) ?
60
+ `:${name}`
55
61
  : name ? `#${name}`
56
62
  : undefined,
57
63
  })
@@ -151,6 +157,13 @@ export const walk = async (
151
157
  for (const node of state.partial.nodes) {
152
158
  state.collect.nodes.add(node)
153
159
  }
160
+ if (state.parts && node.parent?.type === 'root') {
161
+ state.parts.push({
162
+ ...state.part,
163
+ nodes: [...state.partial.nodes],
164
+ })
165
+ delete state.part
166
+ }
154
167
  }
155
168
  }
156
169
  return state
@@ -165,6 +178,10 @@ export type QueryOptions = {
165
178
  hostContexts?: HostContextsMap
166
179
  diffFiles?: DiffFilesProvider
167
180
  getAuthHeader?: GetAuthHeader
181
+ /** for `:skills`; unset: `:skills` throws */
182
+ hasSkills?: HasSkills
183
+ /** part selectors to allow, e.g. `['skill']`; others throw */
184
+ partSelectors?: readonly PartPseudoName[]
168
185
  }
169
186
 
170
187
  // A list of known security selectors that rely on
@@ -223,12 +240,14 @@ export class Query {
223
240
  #cache: Map<string, QueryResponse>
224
241
  #diffFiles: DiffFilesProvider | undefined
225
242
  #getAuthHeader: GetAuthHeader | undefined
243
+ #hasSkills: HasSkills | undefined
226
244
  #edges: Set<EdgeLike>
227
245
  #nodes: Set<NodeLike>
228
246
  #importers: Set<NodeLike>
229
247
  #retries: number
230
248
  #securityArchive: SecurityArchiveLike | undefined
231
249
  #hostContexts: HostContextsMap | undefined
250
+ #partSelectors: ReadonlySet<PartPseudoName>
232
251
 
233
252
  /**
234
253
  * Helper method to determine if a given query string is using any of
@@ -282,10 +301,14 @@ export class Query {
282
301
  hostContexts,
283
302
  diffFiles,
284
303
  getAuthHeader,
304
+ hasSkills,
305
+ partSelectors,
285
306
  }: QueryOptions) {
307
+ this.#partSelectors = new Set(partSelectors)
286
308
  this.#cache = new Map()
287
309
  this.#diffFiles = diffFiles
288
310
  this.#getAuthHeader = getAuthHeader
311
+ this.#hasSkills = hasSkills
289
312
  this.#edges = edges
290
313
  this.#nodes = nodes
291
314
  this.#importers = importers
@@ -524,6 +547,7 @@ export class Query {
524
547
  collect,
525
548
  comment,
526
549
  importers: stateResultImporters,
550
+ parts,
527
551
  specificity,
528
552
  } = await walk({
529
553
  cancellable: async () => {
@@ -544,9 +568,13 @@ export class Query {
544
568
  comment: '',
545
569
  diffFiles: this.#diffFiles,
546
570
  getAuthHeader: this.#getAuthHeader,
571
+ hasSkills: this.#hasSkills,
547
572
  loose,
548
573
  importers,
549
574
  partial: { nodes, edges },
575
+ ...(this.#partSelectors.size ?
576
+ { parts: [], partSelectors: this.#partSelectors }
577
+ : undefined),
550
578
  retries: this.#retries,
551
579
  signal,
552
580
  securityArchive: this.#securityArchive,
@@ -562,6 +590,8 @@ export class Query {
562
590
  importers: this.#getQueryResponseNodes(stateResultImporters),
563
591
  comment,
564
592
  specificity,
593
+ // part nodes are a subset of `nodes`, already hydrated above
594
+ ...(parts ? { parts: parts as QueryPart[] } : undefined),
565
595
  }
566
596
  this.#cache.set(query, res)
567
597
  return res
@@ -0,0 +1,59 @@
1
+ import { asPseudoNode } from '@vltpkg/dss-parser'
2
+ import { queryError } from '../error.ts'
3
+ import { removeQuotes } from './helpers.ts'
4
+ import type { ParserState } from '../types.ts'
5
+
6
+ /**
7
+ * `:skill(<pattern>, ...)`: picks agent skills of the packages its
8
+ * selector matched. Never filters nodes: the walker stores the
9
+ * patterns in the selector's `parts` entry. Opt-in only.
10
+ */
11
+ export const skill = async (state: ParserState) => {
12
+ const curr = asPseudoNode(state.current)
13
+ const sel = curr.parent
14
+ const fail = (message: string) => queryError(message, state.current)
15
+ if (
16
+ sel?.parent?.type !== 'root' ||
17
+ sel.nodes
18
+ .slice(sel.index(curr) + 1)
19
+ .some(n => n.type !== 'comment')
20
+ ) {
21
+ throw fail(':skill() must end a top-level selector')
22
+ }
23
+ if (!state.partSelectors?.has('skill')) {
24
+ throw fail(
25
+ ':skill() only applies to --allow-skills and vlt skills',
26
+ )
27
+ }
28
+ if (!curr.nodes.length) throw fail('Missing skill name in :skill()')
29
+ const patterns = curr.nodes.map(arg => {
30
+ let pattern = ''
31
+ for (const n of arg.nodes) {
32
+ if (n.type === 'comment') continue
33
+ // `a|b` is tag `b` in namespace `a`: never drop the `a`
34
+ if (
35
+ (n.type === 'tag' || n.type === 'universal') &&
36
+ !String(n).includes('|')
37
+ ) {
38
+ pattern += n.value
39
+ } else if (n.type === 'string') {
40
+ if (n.value.startsWith("'")) {
41
+ throw fail(
42
+ 'Invalid skill name in :skill(), use double quotes',
43
+ )
44
+ }
45
+ pattern += removeQuotes(n.value)
46
+ } else {
47
+ throw fail(
48
+ n.type === 'combinator' ?
49
+ 'Invalid skill name in :skill(), quote names with spaces'
50
+ : 'Invalid skill name in :skill()',
51
+ )
52
+ }
53
+ }
54
+ if (!pattern) throw fail('Missing skill name in :skill()')
55
+ return pattern
56
+ })
57
+ state.part = { name: 'skill', patterns }
58
+ return state
59
+ }
@@ -0,0 +1,29 @@
1
+ import { asPseudoNode } from '@vltpkg/dss-parser'
2
+ import { queryError } from '../error.ts'
3
+ import { clear, removeDanglingEdges, removeNode } from './helpers.ts'
4
+ import type { ParserState } from '../types.ts'
5
+
6
+ /**
7
+ * `:skills`: matches installed packages that ship agent skills, as
8
+ * told by the `hasSkills` provider.
9
+ */
10
+ export const skills = async (state: ParserState) => {
11
+ // `:skills(x)`: likely a `:skill(x)` typo
12
+ if (asPseudoNode(state.current).nodes.length) {
13
+ if (state.loose) return clear(state)
14
+ throw queryError(':skills takes no arguments', state.current, {
15
+ wanted: ':skill',
16
+ })
17
+ }
18
+ if (!state.hasSkills) {
19
+ throw queryError(
20
+ 'The :skills selector is not supported here',
21
+ state.current,
22
+ )
23
+ }
24
+ for (const node of state.partial.nodes) {
25
+ if (!(await state.hasSkills(node))) removeNode(state, node)
26
+ }
27
+ removeDanglingEdges(state)
28
+ return state
29
+ }
package/src/pseudo.ts CHANGED
@@ -54,6 +54,8 @@ import { semverParser as semver } from './pseudo/semver.ts'
54
54
  import { severity } from './pseudo/severity.ts'
55
55
  import { spec } from './pseudo/spec.ts'
56
56
  import { shrinkwrap } from './pseudo/shrinkwrap.ts'
57
+ import { skill } from './pseudo/skill.ts'
58
+ import { skills } from './pseudo/skills.ts'
57
59
  import { squat } from './pseudo/squat.ts'
58
60
  import { suspicious } from './pseudo/suspicious.ts'
59
61
  import { tracker } from './pseudo/tracker.ts'
@@ -99,6 +101,7 @@ const has = async (state: ParserState) => {
99
101
  nodes: new Set(state.partial.nodes),
100
102
  },
101
103
  diffFiles: state.diffFiles,
104
+ hasSkills: state.hasSkills,
102
105
  loose: state.loose,
103
106
  hostContexts: state.hostContexts,
104
107
  importers: state.importers,
@@ -182,6 +185,7 @@ const is = async (state: ParserState) => {
182
185
  },
183
186
  current: node,
184
187
  diffFiles: state.diffFiles,
188
+ hasSkills: state.hasSkills,
185
189
  initial: state.initial,
186
190
  loose: true,
187
191
  partial: {
@@ -229,6 +233,7 @@ const not = async (state: ParserState) => {
229
233
  },
230
234
  current: node,
231
235
  diffFiles: state.diffFiles,
236
+ hasSkills: state.hasSkills,
232
237
  initial: state.initial,
233
238
  loose: state.loose,
234
239
  partial: {
@@ -358,6 +363,8 @@ const pseudoSelectors = new Map<string, ParserFn>(
358
363
  severity,
359
364
  shell,
360
365
  shrinkwrap,
366
+ skill,
367
+ skills,
361
368
  squat,
362
369
  suspicious,
363
370
  tracker,