@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 +7 -2
- package/dist/index.js +23 -4
- package/dist/pseudo/skill.d.ts +7 -0
- package/dist/pseudo/skill.js +52 -0
- package/dist/pseudo/skills.d.ts +6 -0
- package/dist/pseudo/skills.js +26 -0
- package/dist/pseudo.js +7 -0
- package/dist/types.d.ts +29 -1
- package/package.json +9 -9
- package/skills/dss-query/REFERENCE.md +2 -0
- package/src/index.ts +31 -1
- package/src/pseudo/skill.ts +59 -0
- package/src/pseudo/skills.ts +29 -0
- package/src/pseudo.ts +7 -0
- package/src/types.ts +31 -1
- package/tap-snapshots/test/pseudo/skills.ts.test.cjs +19 -0
- package/test/index.ts +99 -0
- package/test/pseudo/skill.ts +189 -0
- package/test/pseudo/skills.ts +90 -0
- package/tsconfig.publish.tsbuildinfo +1 -1
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 {
|
|
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)
|
|
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,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
|
+
"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.
|
|
17
|
-
"@vltpkg/dss-parser": "1.
|
|
18
|
-
"@vltpkg/error-cause": "1.
|
|
19
|
-
"@vltpkg/security-archive": "1.
|
|
20
|
-
"@vltpkg/semver": "1.
|
|
21
|
-
"@vltpkg/types": "1.
|
|
22
|
-
"@vltpkg/user-agent": "1.
|
|
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.
|
|
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)
|
|
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,
|