@quonfig/cli 0.2.3 → 0.3.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/README.md +195 -60
- package/dist/agent/install-skill.d.ts +37 -0
- package/dist/agent/install-skill.js +110 -0
- package/dist/agent/skill-bundle.generated.d.ts +6 -0
- package/dist/agent/skill-bundle.generated.js +21 -0
- package/dist/codegen/json-schema-to-zod.js +99 -24
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +4 -2
- package/dist/commands/agent/install-skill.d.ts +12 -0
- package/dist/commands/agent/install-skill.js +83 -0
- package/dist/commands/cleanup/list.js +2 -1
- package/dist/commands/cleanup/remove.js +17 -6
- package/dist/commands/cleanup/status.js +2 -2
- package/dist/commands/cleanup.js +4 -3
- package/dist/commands/config-schema.js +3 -0
- package/dist/commands/contexts/search.d.ts +15 -0
- package/dist/commands/contexts/search.js +78 -0
- package/dist/commands/contexts.d.ts +6 -0
- package/dist/commands/contexts.js +10 -0
- package/dist/commands/set-default.js +5 -1
- package/dist/init/templates.js +17 -36
- package/dist/util/encryption.js +3 -1
- package/dist/verify/schema-error-reshape.d.ts +5 -0
- package/dist/verify/schema-error-reshape.js +23 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +239 -6
- package/package.json +2 -3
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { SKILL_NAME } from '../../agent/skill-bundle.generated.js';
|
|
4
5
|
import { APICommand } from '../../index.js';
|
|
5
6
|
import { getAppUrl } from '../../util/domain-urls.js';
|
|
6
7
|
import nameArg from '../../util/name-arg.js';
|
|
7
|
-
|
|
8
|
+
// The public Quonfig agent skill (github.com/quonfig/skills, installed with
|
|
9
|
+
// `qfg agent install-skill` or the Claude Code plugin). Its
|
|
10
|
+
// references/retire-a-flag.md is the code-removal half of this workflow.
|
|
11
|
+
const SKILL_REFERENCE = 'references/retire-a-flag.md';
|
|
8
12
|
const GITIGNORE_ENTRY = '.qf/cleanup/';
|
|
9
13
|
// SDK client method names worth grepping for in the customer's codebase.
|
|
10
14
|
// Based on the surfaces the official Quonfig SDKs expose (sdk-node /
|
|
@@ -121,14 +125,16 @@ function ensureGitignoreEntry(cwd) {
|
|
|
121
125
|
}
|
|
122
126
|
export default class CleanupRemove extends APICommand {
|
|
123
127
|
static args = { ...nameArg };
|
|
124
|
-
static description = `Write a cleanup payload for a ready-for-cleanup flag and hand off to the
|
|
128
|
+
static description = `Write a cleanup payload for a ready-for-cleanup flag and hand off to the Quonfig agent skill.
|
|
125
129
|
|
|
126
130
|
Modeled on \`qfg migrate my-code\` — this command never edits source files
|
|
127
131
|
itself. It validates that the flag is marked readyForCleanup=true, refuses to
|
|
128
132
|
proceed if there are still evals_2d > 0 (use --force to override), writes
|
|
129
133
|
\`.qf/cleanup/<key>.json\` describing the flag's current rule shape +
|
|
130
|
-
telemetry, and prints
|
|
131
|
-
|
|
134
|
+
telemetry, and prints how to have your coding agent remove the call sites with
|
|
135
|
+
the quonfig agent skill (its references/retire-a-flag.md), which asks the
|
|
136
|
+
engineer which value should "win" and applies the inlining. Install the skill
|
|
137
|
+
with \`qfg agent install-skill\` or \`claude plugin install quonfig@quonfig\`.
|
|
132
138
|
|
|
133
139
|
The payload deliberately does NOT suggest a winning value; that's the
|
|
134
140
|
engineer's call. Run \`qfg cleanup status <key>\` first if you want to inspect
|
|
@@ -208,9 +214,14 @@ telemetry before retiring.`;
|
|
|
208
214
|
this.log(`Payload: ${relPayloadPath}`);
|
|
209
215
|
this.log(`Evals (2d): ${totals.evals_2d}${payload.forced ? ' (--force overrode the safety gate)' : ''}`);
|
|
210
216
|
this.log('');
|
|
211
|
-
this.log(`To remove this flag's call sites,
|
|
217
|
+
this.log(`To remove this flag's call sites, ask your coding agent to retire it with the quonfig agent skill`);
|
|
218
|
+
this.log(`(${SKILL_REFERENCE} covers this workflow), e.g. in Claude Code:`);
|
|
212
219
|
this.log('');
|
|
213
|
-
this.log(` claude "
|
|
220
|
+
this.log(` claude "Retire the Quonfig flag ${key} using ${relPayloadPath}"`);
|
|
221
|
+
this.log('');
|
|
222
|
+
this.log(`No quonfig skill in this repo yet? Install it first:`);
|
|
223
|
+
this.log(` qfg agent install-skill # writes .claude/skills/quonfig/`);
|
|
224
|
+
this.log(` # or: claude plugin marketplace add quonfig/skills && claude plugin install quonfig@quonfig`);
|
|
214
225
|
this.log('');
|
|
215
226
|
this.log(`The skill will:`);
|
|
216
227
|
this.log(` 1. Read ${relPayloadPath}`);
|
|
@@ -42,13 +42,13 @@ export default class CleanupStatus extends APICommand {
|
|
|
42
42
|
static description = `Drill into one ready-for-cleanup flag — show telemetry across all environments and the current rule shape.
|
|
43
43
|
|
|
44
44
|
Use this after \`qfg cleanup list\` to inspect a specific flag before handing
|
|
45
|
-
removal off to the
|
|
45
|
+
removal off to the Quonfig agent skill. The eval counts come from
|
|
46
46
|
analytics.configSparklines (the same backing data the per-flag sparklines on
|
|
47
47
|
the flag detail page use), summed into 24h/2d/7d/30d windows so you can decide
|
|
48
48
|
whether it's safe to retire.
|
|
49
49
|
|
|
50
50
|
Pass --json for the structured object including the full rule shape per
|
|
51
|
-
environment — the
|
|
51
|
+
environment — the agent skill consumes this directly.`;
|
|
52
52
|
static examples = [
|
|
53
53
|
'<%= config.bin %> <%= command.id %> my.flag.key',
|
|
54
54
|
'<%= config.bin %> <%= command.id %> my.flag.key --json',
|
package/dist/commands/cleanup.js
CHANGED
|
@@ -4,14 +4,15 @@ export default class Cleanup extends BaseCommand {
|
|
|
4
4
|
|
|
5
5
|
The cleanup workflow turns a flag's "Ready for cleanup" marker into an end-to-end
|
|
6
6
|
removal flow: surface the flags that are safe to retire, hand the actual code
|
|
7
|
-
removal to the
|
|
8
|
-
|
|
7
|
+
removal to your coding agent with the Quonfig agent skill (install it with
|
|
8
|
+
\`qfg agent install-skill\`), then delete the flag definition once the call
|
|
9
|
+
sites are gone.
|
|
9
10
|
|
|
10
11
|
Lifecycle (high-level):
|
|
11
12
|
1. Owner flips readyForCleanup=true in the UI (already exists)
|
|
12
13
|
2. qfg cleanup list see candidates + telemetry
|
|
13
14
|
3. qfg cleanup status <key> drill into one flag
|
|
14
|
-
4. qfg cleanup remove <key> handoff to the
|
|
15
|
+
4. qfg cleanup remove <key> handoff to the agent skill
|
|
15
16
|
5. PR merges, SDK redeploys, telemetry confirms 0 evals
|
|
16
17
|
6. qfg cleanup verify <key> (optional) confirm safe
|
|
17
18
|
7. qfg delete <key> remove the flag definition`;
|
|
@@ -89,6 +89,9 @@ CRITERION FIELDS
|
|
|
89
89
|
Each criterion object has:
|
|
90
90
|
operator (required) — see operator table below
|
|
91
91
|
propertyName (required for PROP_* operators) — user property, e.g. "user.key", "user.plan"
|
|
92
|
+
To target specific customers or users, match on the context KEY
|
|
93
|
+
("organization.key", "user.key"), not a slug, email or name.
|
|
94
|
+
Find a key from a name with: qfg contexts search organization <name>
|
|
92
95
|
valueToMatch (required for all except ALWAYS_TRUE / IS_PRESENT / IS_NOT_PRESENT) — typed value to compare against
|
|
93
96
|
|
|
94
97
|
────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JsonObj } from '../../result.js';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
export default class ContextsSearch extends APICommand {
|
|
4
|
+
static args: {
|
|
5
|
+
contextType: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
6
|
+
query: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static description: string;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
limit: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<JsonObj | void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
export default class ContextsSearch extends APICommand {
|
|
4
|
+
static args = {
|
|
5
|
+
contextType: Args.string({
|
|
6
|
+
description: 'Context type to search, e.g. organization or user (the part before ".key" in a rule)',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
query: Args.string({
|
|
10
|
+
description: 'Case-insensitive text matched against each context key and its reported properties (e.g. a name)',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = `Find a context's key from a name, to target it in a rule.
|
|
15
|
+
|
|
16
|
+
Targeting rules should match a context by its KEY (<contextType>.key, e.g.
|
|
17
|
+
organization.key or user.key), not by a slug, email or name. This searches the
|
|
18
|
+
contexts your SDKs have reported through telemetry, matching the query against
|
|
19
|
+
each context's key and properties, and prints each match's key, its name and
|
|
20
|
+
its email (blank when the context reported none), most recently seen first.
|
|
21
|
+
|
|
22
|
+
Only contexts your SDKs have reported are searchable. If nothing matches, the key
|
|
23
|
+
is unknown: get it from whoever owns that customer rather than targeting a slug
|
|
24
|
+
or email instead.
|
|
25
|
+
|
|
26
|
+
Pass a key as the query to see its name and email.
|
|
27
|
+
|
|
28
|
+
--json prints {contexts: [{key, name, email?}]}: name is null when the context
|
|
29
|
+
reported no name, and email is present only when it reported one.`;
|
|
30
|
+
static examples = [
|
|
31
|
+
'<%= config.bin %> <%= command.id %> organization formhealth',
|
|
32
|
+
'<%= config.bin %> <%= command.id %> user barry@example.com --environment production',
|
|
33
|
+
'<%= config.bin %> <%= command.id %> organization formhealth --json',
|
|
34
|
+
];
|
|
35
|
+
static flags = {
|
|
36
|
+
environment: Flags.string({
|
|
37
|
+
description: 'Only contexts reported from this environment (default: every environment)',
|
|
38
|
+
}),
|
|
39
|
+
limit: Flags.integer({
|
|
40
|
+
default: 20,
|
|
41
|
+
description: 'Maximum number of matches (1-100)',
|
|
42
|
+
max: 100,
|
|
43
|
+
min: 1,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
async run() {
|
|
47
|
+
const { args, flags } = await this.parse(ContextsSearch);
|
|
48
|
+
const request = await this.apiClient.post('/api/v1/contexts/searchKeys', {
|
|
49
|
+
contextType: args.contextType,
|
|
50
|
+
environment: flags.environment,
|
|
51
|
+
limit: flags.limit,
|
|
52
|
+
query: args.query,
|
|
53
|
+
workspaceId: this.workspaceId,
|
|
54
|
+
});
|
|
55
|
+
if (!request.ok) {
|
|
56
|
+
const serverError = request.error;
|
|
57
|
+
const errorMsg = serverError?.error || serverError?.json?.message || `Failed to search contexts: ${request.status}`;
|
|
58
|
+
return this.err(errorMsg, { serverError: request.error });
|
|
59
|
+
}
|
|
60
|
+
const contexts = (request.json?.contexts ?? []);
|
|
61
|
+
if (contexts.length === 0) {
|
|
62
|
+
this.log(`No context seen via telemetry matched "${args.query}" (type ${args.contextType}), so the key is unknown.\n` +
|
|
63
|
+
'Only contexts your SDKs have reported are searchable. Get the key from whoever owns that ' +
|
|
64
|
+
'customer; do not target a slug or email instead.');
|
|
65
|
+
return { contexts: [] };
|
|
66
|
+
}
|
|
67
|
+
const keyWidth = Math.max(...contexts.map((c) => c.key.length), 'KEY'.length);
|
|
68
|
+
const nameWidth = Math.max(...contexts.map((c) => (c.name ?? '').length), 'NAME'.length);
|
|
69
|
+
const row = (key, name, email) => `${key.padEnd(keyWidth)} ${name.padEnd(nameWidth)} ${email}`.trimEnd();
|
|
70
|
+
const lines = [row('KEY', 'NAME', 'EMAIL')];
|
|
71
|
+
for (const context of contexts) {
|
|
72
|
+
lines.push(row(context.key, context.name ?? '', context.email ?? ''));
|
|
73
|
+
}
|
|
74
|
+
lines.push('', `Target these in a rule with propertyName "${args.contextType}.key".`);
|
|
75
|
+
this.log(lines.join('\n'));
|
|
76
|
+
return { contexts };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
export default class Contexts extends BaseCommand {
|
|
3
|
+
static description = 'Look up contexts (organizations, users, ...) your SDKs have reported, to target them by key.';
|
|
4
|
+
static examples = ['<%= config.bin %> contexts search organization formhealth'];
|
|
5
|
+
async run() {
|
|
6
|
+
this.log('Use one of the contexts subcommands:');
|
|
7
|
+
this.log(' qfg contexts search CONTEXT_TYPE QUERY [--environment ENV] [--limit N] [--json]');
|
|
8
|
+
this.log(' # find a context key by name, e.g. qfg contexts search organization formhealth');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -32,7 +32,11 @@ To set a percentage rollout (gradual rollout / A/B test / canary deploy) instead
|
|
|
32
32
|
qfg set-rollout my.flag --environment production --true-percent 20
|
|
33
33
|
|
|
34
34
|
To see all current values and rules for a flag:
|
|
35
|
-
qfg info my.flag
|
|
35
|
+
qfg info my.flag
|
|
36
|
+
|
|
37
|
+
To target specific organizations or users, write rules on their KEY
|
|
38
|
+
(organization.key, user.key), not a slug or email; find a key from a name with:
|
|
39
|
+
qfg contexts search organization formhealth`;
|
|
36
40
|
static examples = [
|
|
37
41
|
'<%= config.bin %> <%= command.id %> my.flag.name # prompts for value and env',
|
|
38
42
|
'<%= config.bin %> <%= command.id %> my.flag.name --value=true --environment=staging',
|
package/dist/init/templates.js
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
* user-editable. Each starts with a comment so users know not to
|
|
6
6
|
* hand-edit them.
|
|
7
7
|
*/
|
|
8
|
+
import { SKILL_FILES } from '../agent/skill-bundle.generated.js';
|
|
9
|
+
/**
|
|
10
|
+
* The qfg-vs-MCP steering and "setting a value keeps targeting" rules are
|
|
11
|
+
* shared with the Quonfig agent skill (github.com/quonfig/skills,
|
|
12
|
+
* references/writes.md). The skill is the single source; the cli vendors it
|
|
13
|
+
* at build time (scripts/sync-agent-skill.mjs) and AGENTS.md embeds it here.
|
|
14
|
+
*/
|
|
15
|
+
const SHARED_WRITES_SECTION = SKILL_FILES.find((f) => f.path === 'references/writes.md')
|
|
16
|
+
.content.trim()
|
|
17
|
+
.replaceAll(/^## /gm, '### ');
|
|
8
18
|
const MANAGED_HEADER = '<!-- Managed by `qfg init` — do not edit manually. Run `qfg init` to update. -->';
|
|
9
19
|
// ── README.md ──────────────────────────────────────────────────────────
|
|
10
20
|
export function readmeTemplate() {
|
|
@@ -196,48 +206,19 @@ This is a Quonfig workspace repository. See \`CLAUDE.md\` for the complete schem
|
|
|
196
206
|
5. Files must be in the correct directory for their \`type\`.
|
|
197
207
|
6. See \`CLAUDE.md\` for the full list of value types, operators, and constraints.
|
|
198
208
|
|
|
199
|
-
## \`qfg
|
|
209
|
+
## Hosted workspaces: \`qfg\`, the MCP, and how writes behave
|
|
200
210
|
|
|
201
211
|
Only relevant if this workspace is *also* hosted on a Quonfig account. On the
|
|
202
212
|
fully-local path, editing the JSON in this repo is the whole story.
|
|
203
213
|
|
|
204
|
-
|
|
205
|
-
already on disk, so reading them costs nothing.
|
|
206
|
-
- **Slack, claude.ai, or anywhere without a checkout** -> use the Quonfig MCP
|
|
207
|
-
server. It reads and writes the hosted workspace directly. Do not try to shell
|
|
208
|
-
out to \`qfg\` from a surface that has no repo.
|
|
209
|
-
|
|
210
|
-
Both surfaces cover the same everyday loop — create a flag or config, change what
|
|
211
|
-
an environment serves, set a log level. (\`qfg\` has all of it today; the MCP's
|
|
212
|
-
create/config/log-level verbs land with its v2 write surface.)
|
|
213
|
-
|
|
214
|
-
## Setting a value KEEPS that environment's targeting rules
|
|
215
|
-
|
|
216
|
-
Both surfaces write the same way, so learn it once:
|
|
217
|
-
|
|
218
|
-
\`qfg set-default\` (alias \`toggle\`), \`qfg set-rollout\`, and the MCP
|
|
219
|
-
\`set_flag\`/\`set_config\` verbs replace exactly ONE rule — the environment's
|
|
220
|
-
**fallback**, the unconditional rule at the end of its rule list that decides
|
|
221
|
-
what users receive when no targeting rule matches. Every targeting rule above
|
|
222
|
-
it is kept, and both surfaces report how many. An environment with no rules of
|
|
223
|
-
its own has them copied from default first, so inherited targeting is kept too.
|
|
224
|
-
|
|
225
|
-
| Surface | By default | To set the value for EVERYONE |
|
|
226
|
-
|---------|------------|-------------------------------|
|
|
227
|
-
| \`qfg set-default\` / \`qfg set-rollout\` | Keeps targeting; reports "Kept N targeting rule(s)" | add \`--replace-targeting\` |
|
|
228
|
-
| MCP \`set_flag\` / \`set_config\` | Keeps targeting; reports the kept count in its result | send \`replaceTargeting: true\` |
|
|
229
|
-
|
|
230
|
-
Setting the value for everyone DELETES that environment's targeting rules. They
|
|
231
|
-
stay in git history, and the write returns a \`previousCommitSha\` to restore from.
|
|
214
|
+
${SHARED_WRITES_SECTION}
|
|
232
215
|
|
|
233
|
-
|
|
234
|
-
rules your value is about to sit next to. For anything a fallback cannot express —
|
|
235
|
-
multi-rule targeting, reordering, editing one targeting rule — edit the JSON
|
|
236
|
-
directly or use MCP \`set_document\`.
|
|
216
|
+
## Application repos
|
|
237
217
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
218
|
+
This file covers the workspace (config) repo. In an application repo that
|
|
219
|
+
*reads* these flags through a Quonfig SDK, install the Quonfig agent skill:
|
|
220
|
+
\`qfg agent install-skill\` (or \`claude plugin install quonfig@quonfig\`).
|
|
221
|
+
It covers SDK setup, creating and rolling out flags, and the MCP tools.
|
|
241
222
|
`;
|
|
242
223
|
}
|
|
243
224
|
// ── Pre-commit hook ────────────────────────────────────────────────────
|
package/dist/util/encryption.js
CHANGED
|
@@ -33,7 +33,9 @@ export function decrypt(encryptedString, keyStringHex) {
|
|
|
33
33
|
const encryptedBytes = forge.util.hexToBytes(encryptedDataPart);
|
|
34
34
|
const ivBytes = forge.util.hexToBytes(ivPart);
|
|
35
35
|
const tagBytes = forge.util.hexToBytes(authTagPart);
|
|
36
|
-
// Create decipher
|
|
36
|
+
// Create decipher. node-forge AES-GCM with the IV passed to start() below, not
|
|
37
|
+
// Node's deprecated IV-less crypto.createDecipher that this rule targets.
|
|
38
|
+
// nosemgrep: javascript.node-crypto.security.create-de-cipher-no-iv.create-de-cipher-no-iv
|
|
37
39
|
const decipher = forge.cipher.createDecipher('AES-GCM', keyBytes);
|
|
38
40
|
decipher.start({
|
|
39
41
|
iv: ivBytes,
|
|
@@ -59,6 +59,11 @@ export declare function findOneOfTag(branches: unknown[], root: unknown): OneOfT
|
|
|
59
59
|
* a single `oneOfTag` error at the union's position.
|
|
60
60
|
*/
|
|
61
61
|
export declare function reshapeOneOfErrors<E extends SchemaErrorLike>(errors: readonly E[], rootSchema: unknown, value: unknown): (E | SchemaErrorLike)[];
|
|
62
|
+
/**
|
|
63
|
+
* The two 0-based indexes a duplicate-items violation names, lower first,
|
|
64
|
+
* or undefined for any other message. Lets a caller mark both rows.
|
|
65
|
+
*/
|
|
66
|
+
export declare function duplicateItemIndexes(message: string): [number, number] | undefined;
|
|
62
67
|
/** One error -> message. Undefined means "skip it, a nested error says why". */
|
|
63
68
|
export declare function describeSchemaError(error: SchemaErrorLike): string | undefined;
|
|
64
69
|
/**
|
|
@@ -207,6 +207,25 @@ export function reshapeOneOfErrors(errors, rootSchema, value) {
|
|
|
207
207
|
});
|
|
208
208
|
return result;
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* `uniqueItems` wording. Ajv says `(items ## 3 and 1 are identical)`:
|
|
212
|
+
* 0-based, later item first, with a stray `##`. Lists are numbered from 1
|
|
213
|
+
* everywhere the user reads them, so name the pair by row number, lower
|
|
214
|
+
* first: `(items 2 and 4 are identical)`.
|
|
215
|
+
*/
|
|
216
|
+
function duplicateItemsMessage(first, second) {
|
|
217
|
+
const [a, b] = [first, second].sort((x, y) => x - y);
|
|
218
|
+
return `must NOT have duplicate items (items ${a + 1} and ${b + 1} are identical)`;
|
|
219
|
+
}
|
|
220
|
+
const DUPLICATE_ITEMS_MESSAGE = /^must NOT have duplicate items \(items (\d+) and (\d+) are identical\)$/;
|
|
221
|
+
/**
|
|
222
|
+
* The two 0-based indexes a duplicate-items violation names, lower first,
|
|
223
|
+
* or undefined for any other message. Lets a caller mark both rows.
|
|
224
|
+
*/
|
|
225
|
+
export function duplicateItemIndexes(message) {
|
|
226
|
+
const match = DUPLICATE_ITEMS_MESSAGE.exec(message);
|
|
227
|
+
return match ? [Number(match[1]) - 1, Number(match[2]) - 1] : undefined;
|
|
228
|
+
}
|
|
210
229
|
function stringifyAllowed(entry) {
|
|
211
230
|
return typeof entry === 'string' ? entry : JSON.stringify(entry);
|
|
212
231
|
}
|
|
@@ -223,6 +242,10 @@ export function describeSchemaError(error) {
|
|
|
223
242
|
return Array.isArray(params.allowedValues)
|
|
224
243
|
? `${message}: ${params.allowedValues.map(stringifyAllowed).join(', ')}`
|
|
225
244
|
: message;
|
|
245
|
+
case 'uniqueItems':
|
|
246
|
+
return typeof params.i === 'number' && typeof params.j === 'number'
|
|
247
|
+
? duplicateItemsMessage(params.i, params.j)
|
|
248
|
+
: message;
|
|
226
249
|
case 'propertyNames':
|
|
227
250
|
// The nested keyword error (which carries `propertyName`) says why.
|
|
228
251
|
return undefined;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.
|
|
1
|
+
declare const _default: "0.3.0";
|
|
2
2
|
export default _default;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export default '0.
|
|
2
|
+
export default '0.3.0';
|