burgee 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,18 +10,14 @@
10
10
  * WCAG 2.2 sets 4.5:1 for body text and 3:1 for large text and for the parts of
11
11
  * a graphic you need in order to understand it. A logo's bars are the latter, so
12
12
  * 3:1 is the floor used here — see `AA`.
13
+ *
14
+ * The measuring is `roundel`'s, not ours. Colour is the layer below this one, and until
15
+ * 2026-09-09 both packages carried the same forty lines of WCAG luminance — identical
16
+ * constants, identical maths, differing only in which package name the hex error says.
17
+ * What is left here is the part that is actually about a burgee: which pairs of a flag's
18
+ * own colours have to clear the floor, and how to say so to a person running `burgee brand`.
13
19
  */
14
- /** The floors WCAG 2.2 sets, as ratios. */
15
- export declare const AA: {
16
- /** Body text against its background. */
17
- readonly TEXT: 4.5;
18
- /** Large text, UI components, and meaningful parts of a graphic. */
19
- readonly GRAPHIC: 3;
20
- };
21
- /** WCAG relative luminance. */
22
- export declare function luminance(hex: string): number;
23
- /** The WCAG contrast ratio between two colours. Order does not matter. */
24
- export declare function contrast(a: string, b: string): number;
20
+ import { AA, contrast, luminance } from 'roundel/contrast';
25
21
  /** Mix two colours in sRGB. Enough for reading a gradient stop, not for colour science. */
26
22
  export declare function mix(a: string, b: string, t: number): string;
27
23
  export interface ContrastFinding {
@@ -68,3 +64,5 @@ export interface AuditInput {
68
64
  * intrinsic pair and fails a ground has a page problem, not a logo problem.
69
65
  */
70
66
  export declare function auditBurgee(brand: AuditInput, grounds?: readonly string[]): ContrastFinding[];
67
+ /** Re-exported so a caller reading a burgee's contrast needs one import, not two. */
68
+ export { AA, contrast, luminance };
package/dist/contrast.js CHANGED
@@ -1,38 +1,6 @@
1
- export const AA = {
2
- TEXT: 4.5,
3
- GRAPHIC: 3,
4
- };
1
+ import { AA, channels, contrast, luminance } from 'roundel/contrast';
5
2
  const SRGB_MAX = 255;
6
- const LINEAR_THRESHOLD = 0.03928;
7
- const LINEAR_DIVISOR = 12.92;
8
- const GAMMA_OFFSET = 0.055;
9
- const GAMMA_SCALE = 1.055;
10
- const GAMMA_EXPONENT = 2.4;
11
- const LUMA = { r: 0.2126, g: 0.7152, b: 0.0722 };
12
- const CONTRAST_OFFSET = 0.05;
13
- const RED_AT = 1;
14
- const GREEN_AT = 3;
15
- const BLUE_AT = 5;
16
- const HEX_PAIRS = [RED_AT, GREEN_AT, BLUE_AT];
17
3
  const HEX_RADIX = 16;
18
- const SHORT_HEX_LENGTH = 4;
19
- function channels(hex) {
20
- const full = hex.length === SHORT_HEX_LENGTH
21
- ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`
22
- : hex;
23
- if (!/^#[0-9a-fA-F]{6}$/.test(full))
24
- throw new Error(`burgee: "${hex}" is not a hex colour`);
25
- const parsed = HEX_PAIRS.map((i) => Number.parseInt(full.slice(i, i + 2), HEX_RADIX) / SRGB_MAX);
26
- return parsed;
27
- }
28
- export function luminance(hex) {
29
- const [r, g, b] = channels(hex).map((v) => v <= LINEAR_THRESHOLD ? v / LINEAR_DIVISOR : ((v + GAMMA_OFFSET) / GAMMA_SCALE) ** GAMMA_EXPONENT);
30
- return LUMA.r * r + LUMA.g * g + LUMA.b * b;
31
- }
32
- export function contrast(a, b) {
33
- const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
34
- return (hi + CONTRAST_OFFSET) / (lo + CONTRAST_OFFSET);
35
- }
36
4
  export function mix(a, b, t) {
37
5
  const [ca, cb] = [channels(a), channels(b)];
38
6
  const hex = ca
@@ -91,3 +59,4 @@ export function auditBurgee(brand, grounds = []) {
91
59
  }
92
60
  return findings;
93
61
  }
62
+ export { AA, contrast, luminance };
package/dist/execute.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { parseArgs } from 'node:util';
3
+ import { ConfigError, explain, resolve as resolveLayers } from 'seniority';
3
4
  import { detectAgent } from './agent.js';
4
5
  import { ExitCode, isExitCode } from './exit-code.js';
5
6
  import { renderHelp } from './help.js';
@@ -7,7 +8,6 @@ import { Manifest } from './manifest.js';
7
8
  import { serveMcp } from './mcp.js';
8
9
  import { camel, kebab } from './names.js';
9
10
  import { nearestPackage } from './pkg.js';
10
- import { ConfigError, explain, resolve as resolveLayers } from './precedence.js';
11
11
  import { commandSchemaOf, machineJson, schemaOf, summaryOf } from './schema.js';
12
12
  import { checkDefinition, checkRelations, coerce, UsageError } from './validate.js';
13
13
  function helpFields(c) {
@@ -159,7 +159,7 @@ function packageLayer(pkg, name) {
159
159
  return typeof field === 'object' && field !== null && !Array.isArray(field) ? { path: pkg.path, data: field } : undefined;
160
160
  }
161
161
  async function configLayers(name, values, io) {
162
- const { discover } = await import('./config.js');
162
+ const { discover } = await import('seniority');
163
163
  const explicit = values['config'];
164
164
  const disabled = values['noConfig'] === true;
165
165
  const loaded = await discover({ name, cwd: io.cwd, env: io.env, ...(typeof explicit === 'string' ? { explicit } : {}), disabled });
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@ export { camel, checkDefinition, kebab, UsageError } from './validate.js';
11
11
  export { AGENT_PROBES, detectAgent, type AgentProbe, type Detection } from './agent.js';
12
12
  export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
13
13
  export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf, type Invoke, type ServeOptions, type Tool, type ToolAnnotations } from './mcp.js';
14
- export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from './precedence.js';
14
+ export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority';
15
15
  export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
16
16
  export { definePlugin, Manifest, type ArgumentSpec, type CommandNode, type Effects, type Example, type Hook, type LazyModule, type OptionSpec, type ActionRequiredSpec, type Plugin, type Relation, type RunContext, type StandardResult, type StandardSchemaV1, } from './manifest.js';
package/dist/index.js CHANGED
@@ -4,6 +4,6 @@ export { camel, checkDefinition, kebab, UsageError } from './validate.js';
4
4
  export { AGENT_PROBES, detectAgent } from './agent.js';
5
5
  export { renderHelp } from './help.js';
6
6
  export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf } from './mcp.js';
7
- export { ConfigError, envName, explain, resolve, screaming } from './precedence.js';
7
+ export { ConfigError, envName, explain, resolve, screaming } from 'seniority';
8
8
  export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf } from './schema.js';
9
9
  export { definePlugin, Manifest, } from './manifest.js';
package/dist/schema.d.ts CHANGED
@@ -42,10 +42,42 @@ export interface CommandSchema {
42
42
  plugin?: string;
43
43
  arguments: ArgumentSpec[];
44
44
  options: Record<string, OptionSpec>;
45
+ /**
46
+ * The constraints between options (S2/S6), which `validate.ts` already enforces and the
47
+ * schema did not publish. Without them an agent can only discover that `--a` conflicts
48
+ * with `--b` by sending both and reading exit 2 — a round trip per constraint, and under
49
+ * E1 an exit 2 means *rewrite the command*, so it may well send the same pair again.
50
+ *
51
+ * Omitted entirely when a command declares none, so a reader can tell "no constraints"
52
+ * from "constraints not published".
53
+ */
54
+ relations?: PublishedRelation[];
45
55
  examples: Example[];
46
56
  /** The arguments and options as one JSON Schema object — what an MCP tool call takes. */
47
57
  inputSchema: JsonSchema;
48
58
  }
59
+ /**
60
+ * A relation as JSON can carry it.
61
+ *
62
+ * `implies` takes either another option's name or a **predicate over the values**, and a
63
+ * function cannot be published. `JSON.stringify` turns it into `null` without a word, which
64
+ * would hand an agent `["force", null]` and let it conclude the constraint is malformed
65
+ * rather than unevaluable. So a predicate becomes the string `"(predicate)"`: the pair is
66
+ * still visible, and what is missing says so.
67
+ */
68
+ export type PublishedRelation = {
69
+ exactlyOneOf: readonly string[];
70
+ } | {
71
+ atLeastOneOf: readonly string[];
72
+ } | {
73
+ atMostOneOf: readonly string[];
74
+ } | {
75
+ conflicts: readonly string[];
76
+ } | {
77
+ implies: readonly [string, string];
78
+ };
79
+ /** The marker a predicate leaves behind. Not a name any option can have — it has parentheses. */
80
+ export declare const PREDICATE = "(predicate)";
49
81
  export interface ProgramSchema {
50
82
  schemaVersion: 1;
51
83
  name: string;
package/dist/schema.js CHANGED
@@ -1,4 +1,11 @@
1
1
  import { kebab } from './names.js';
2
+ export const PREDICATE = '(predicate)';
3
+ function publishable(relation) {
4
+ if (!('implies' in relation))
5
+ return relation;
6
+ const [option, consequent] = relation.implies;
7
+ return { implies: [option, typeof consequent === 'function' ? PREDICATE : consequent] };
8
+ }
2
9
  function argumentProperty(a) {
3
10
  const p = a.variadic === true ? { type: 'array', items: { type: 'string' } } : { type: 'string' };
4
11
  if (a.description !== undefined)
@@ -76,6 +83,8 @@ export function commandSchemaOf(node, root) {
76
83
  out.lazy = true;
77
84
  if (node.plugin !== undefined)
78
85
  out.plugin = node.plugin;
86
+ if (node.relations !== undefined && node.relations.length > 0)
87
+ out.relations = node.relations.map(publishable);
79
88
  return out;
80
89
  }
81
90
  export function runnable(manifest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "burgee",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "An agent-native CLI framework, drop-in compatible with commander and yargs. One declaration; help, --json, --schema, --mcp and completions all projected from it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -95,6 +95,10 @@
95
95
  "access": "public",
96
96
  "provenance": true
97
97
  },
98
+ "dependencies": {
99
+ "roundel": "^0.3.0",
100
+ "seniority": "^0.1.0"
101
+ },
98
102
  "devDependencies": {
99
103
  "vitest": "^5.0.0"
100
104
  },
package/dist/config.d.ts DELETED
@@ -1,28 +0,0 @@
1
- import { type Layer } from './precedence.js';
2
- export interface Discovery {
3
- name: string;
4
- cwd: string;
5
- env: Record<string, string | undefined>;
6
- /** `--config <path>`. */
7
- explicit?: string;
8
- /** `--no-config`. */
9
- disabled?: boolean;
10
- }
11
- export interface Loaded extends Layer {
12
- /** The files merged, outermost first — what `--explain` shows. */
13
- chain: string[];
14
- }
15
- /** Objects merge recursively; anything else, the later value wins. */
16
- export declare function deepMerge(base: Record<string, unknown>, over: Record<string, unknown>): Record<string, unknown>;
17
- /** Load a file and everything it extends, outermost first, the file's own keys winning. */
18
- export declare function loadWithExtends(path: string, seen?: string[]): Promise<Loaded>;
19
- /** The discovery order as candidate paths, first hit wins; each entry says why it was tried. */
20
- export declare function candidates(d: Discovery): {
21
- path: string;
22
- reason: string;
23
- }[];
24
- /**
25
- * Discover and load. `package.json#<name>` is not a file here — the engine reads the
26
- * owning package.json itself and treats the field as its own layer, below config.
27
- */
28
- export declare function discover(d: Discovery): Promise<Loaded | undefined>;
package/dist/config.js DELETED
@@ -1,98 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
- import { dirname, isAbsolute, join, resolve } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- import { ConfigError } from './precedence.js';
6
- const EXTENSIONS = ['.json', '.mjs', '.js', '.cjs'];
7
- const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
8
- export function deepMerge(base, over) {
9
- const out = new Map(Object.entries(base));
10
- for (const [k, v] of Object.entries(over)) {
11
- const prev = out.get(k);
12
- out.set(k, isObject(prev) && isObject(v) ? deepMerge(prev, v) : v);
13
- }
14
- return Object.fromEntries(out);
15
- }
16
- async function loadFile(path) {
17
- if (path.endsWith('.json')) {
18
- try {
19
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
20
- if (!isObject(parsed))
21
- throw new ConfigError(`${path} must contain an object`);
22
- return parsed;
23
- }
24
- catch (cause) {
25
- if (cause instanceof ConfigError)
26
- throw cause;
27
- throw new ConfigError(`${path} is not valid JSON`, cause instanceof Error ? cause.message : undefined);
28
- }
29
- }
30
- const mod = (await import(pathToFileURL(path).href));
31
- const value = typeof mod.default === 'function' ? await mod.default() : mod.default;
32
- if (!isObject(value))
33
- throw new ConfigError(`${path} must export an object (or a function returning one)`);
34
- return value;
35
- }
36
- function resolveExtends(spec, from) {
37
- if (spec.startsWith('.') || isAbsolute(spec))
38
- return resolve(dirname(from), spec);
39
- try {
40
- return createRequire(from).resolve(spec);
41
- }
42
- catch {
43
- throw new ConfigError(`${from} extends "${spec}", which cannot be resolved`, 'use a relative path or an installed package');
44
- }
45
- }
46
- function extendsList(value) {
47
- if (typeof value === 'string')
48
- return [value];
49
- return Array.isArray(value) ? value.map(String) : [];
50
- }
51
- export async function loadWithExtends(path, seen = []) {
52
- if (seen.includes(path))
53
- throw new ConfigError(`config extends itself: ${[...seen, path].join(' → ')}`);
54
- const own = await loadFile(path);
55
- const parents = own['extends'];
56
- const specs = extendsList(parents);
57
- let data = {};
58
- const chain = [];
59
- for (const spec of specs) {
60
- const parent = await loadWithExtends(resolveExtends(spec, path), [...seen, path]);
61
- data = deepMerge(data, parent.data);
62
- chain.push(...parent.chain);
63
- }
64
- const { extends: _ignored, ...rest } = own;
65
- return { path, data: deepMerge(data, rest), chain: [...chain, path] };
66
- }
67
- const userConfigDir = (env) => {
68
- const base = env['XDG_CONFIG_HOME'] ?? (env['HOME'] === undefined ? undefined : join(env['HOME'], '.config'));
69
- return base;
70
- };
71
- export function candidates(d) {
72
- const out = [];
73
- const fromEnv = d.env[`${d.name.toUpperCase().replaceAll('-', '_')}_CONFIG`];
74
- if (fromEnv !== undefined)
75
- out.push({ path: resolve(d.cwd, fromEnv), reason: `${d.name.toUpperCase().replaceAll('-', '_')}_CONFIG` });
76
- for (const ext of EXTENSIONS)
77
- out.push({ path: join(d.cwd, `${d.name}.config${ext}`), reason: 'current directory' });
78
- const user = userConfigDir(d.env);
79
- if (user !== undefined)
80
- out.push({ path: join(user, d.name, 'config.json'), reason: 'user config directory' });
81
- return out;
82
- }
83
- export async function discover(d) {
84
- if (d.disabled === true)
85
- return undefined;
86
- if (d.explicit !== undefined) {
87
- const path = resolve(d.cwd, d.explicit);
88
- if (!existsSync(path))
89
- throw new ConfigError(`config file not found: ${path}`, 'check --config, or drop it to use discovery');
90
- return await loadWithExtends(path);
91
- }
92
- const found = candidates(d)
93
- .map((c) => c.path)
94
- .find((path) => existsSync(path));
95
- if (found === undefined)
96
- return undefined;
97
- return await loadWithExtends(found);
98
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * One precedence order, fixed and not configurable (commander-env V1–V3, V5):
3
- *
4
- * flag > env > config file > package.json field > default
5
- *
6
- * `resolve` is pure — it takes the layers and returns the values with their provenance —
7
- * which is what makes `--explain` trustworthy and `meta.provenance` cheap. Env applies
8
- * only to the options the running command declares (yargs #873), never to a sibling's.
9
- */
10
- import { type OptionSpec } from './manifest.js';
11
- export type Source = 'flag' | 'env' | 'config' | 'package' | 'default';
12
- export interface Provenance {
13
- source: Source;
14
- /** The env name, the config file, or `package.json` — where a person would look. */
15
- location?: string;
16
- }
17
- export interface Candidate {
18
- source: Source;
19
- location: string;
20
- /** `undefined` when the layer had nothing for this option. */
21
- value: unknown;
22
- }
23
- export interface Layer {
24
- path: string;
25
- data: Record<string, unknown>;
26
- }
27
- export interface Layers {
28
- /** What the user typed: only options present on the command line. */
29
- flags: Record<string, unknown>;
30
- env: Record<string, string | undefined>;
31
- /** With a prefix, an option without `env:` reads `PREFIX_OPTION_NAME` (R2). */
32
- envPrefix?: string;
33
- config?: Layer;
34
- /** The `package.json` field named after the program, when present. */
35
- pkg?: Layer;
36
- }
37
- export interface Resolution {
38
- values: Record<string, unknown>;
39
- provenance: Record<string, Provenance>;
40
- candidates: Record<string, Candidate[]>;
41
- }
42
- /** A value that cannot be used as configured: exit CONFIG (3), never a stack (E1, E3). */
43
- export declare class ConfigError extends Error {
44
- readonly hint?: string | undefined;
45
- constructor(message: string, hint?: string | undefined);
46
- }
47
- /** `region` → `REGION`, `dryRun` → `DRY_RUN`, `log-level` → `LOG_LEVEL` (yargs #2005: never camel-cased back). */
48
- export declare function screaming(name: string): string;
49
- export declare function envName(name: string, spec: OptionSpec, prefix: string | undefined): string | undefined;
50
- /** Booleans from env accept one spelling each way (R7). */
51
- export declare function envBoolean(raw: string): boolean | undefined;
52
- /** Every declared option, resolved through the layers; a missing required one is left undefined for the caller to report. */
53
- export declare function resolve(specs: Record<string, OptionSpec>, layers: Layers): Resolution;
54
- /** `--explain <option>`: the winning source and every candidate it beat, or that was unset (V3). */
55
- export declare function explain(name: string, resolution: Resolution): string;
@@ -1,100 +0,0 @@
1
- export class ConfigError extends Error {
2
- hint;
3
- constructor(message, hint) {
4
- super(message);
5
- this.hint = hint;
6
- }
7
- }
8
- export function screaming(name) {
9
- return name
10
- .replaceAll(/([a-z0-9])([A-Z])/g, '$1_$2')
11
- .replaceAll('-', '_')
12
- .toUpperCase();
13
- }
14
- export function envName(name, spec, prefix) {
15
- if (spec.env !== undefined)
16
- return spec.env;
17
- return prefix === undefined ? undefined : `${prefix}_${screaming(name)}`;
18
- }
19
- const TRUE = new Set(['1', 'true', 'yes']);
20
- const FALSE = new Set(['0', 'false', 'no']);
21
- export function envBoolean(raw) {
22
- const v = raw.trim().toLowerCase();
23
- if (TRUE.has(v))
24
- return true;
25
- if (FALSE.has(v))
26
- return false;
27
- return undefined;
28
- }
29
- function fromEnv(name, spec, layers) {
30
- const variable = envName(name, spec, layers.envPrefix);
31
- if (variable === undefined)
32
- return undefined;
33
- if (spec.type === 'boolean' && layers.envPrefix !== undefined) {
34
- const negated = `${layers.envPrefix}_NO_${screaming(name)}`;
35
- if (layers.env[negated] !== undefined) {
36
- throw new ConfigError(`${negated} is not supported`, `set ${variable}=false instead`);
37
- }
38
- }
39
- const raw = layers.env[variable];
40
- if (raw === undefined)
41
- return { source: 'env', location: variable, value: undefined };
42
- if (spec.type !== 'boolean')
43
- return { source: 'env', location: variable, value: raw };
44
- const parsed = envBoolean(raw);
45
- if (parsed === undefined)
46
- throw new ConfigError(`${variable}="${raw}" is not a boolean`, `use ${variable}=true or ${variable}=false`);
47
- return { source: 'env', location: variable, value: parsed };
48
- }
49
- function candidatesFor(name, spec, layers) {
50
- const out = [{ source: 'flag', location: `--${name}`, value: layers.flags[name] }];
51
- const env = fromEnv(name, spec, layers);
52
- if (env !== undefined)
53
- out.push(env);
54
- if (layers.config !== undefined)
55
- out.push({ source: 'config', location: layers.config.path, value: layers.config.data[name] });
56
- if (layers.pkg !== undefined)
57
- out.push({ source: 'package', location: layers.pkg.path, value: layers.pkg.data[name] });
58
- out.push({ source: 'default', location: 'default', value: spec.default });
59
- return out;
60
- }
61
- export function resolve(specs, layers) {
62
- const values = new Map();
63
- const provenance = new Map();
64
- const candidates = new Map();
65
- for (const [name, spec] of Object.entries(specs)) {
66
- const list = candidatesFor(name, spec, layers);
67
- candidates.set(name, list);
68
- const winner = list.find((c) => c.value !== undefined);
69
- if (winner === undefined)
70
- continue;
71
- values.set(name, winner.value);
72
- provenance.set(name, winner.source === 'default' ? { source: 'default' } : { source: winner.source, location: winner.location });
73
- }
74
- return { values: Object.fromEntries(values), provenance: Object.fromEntries(provenance), candidates: Object.fromEntries(candidates) };
75
- }
76
- const describe = (c) => {
77
- switch (c.source) {
78
- case 'flag':
79
- return `flag ${c.location}`;
80
- case 'env':
81
- return `env ${c.location}`;
82
- case 'config':
83
- return `config file ${c.location}`;
84
- case 'package':
85
- return `package.json field in ${c.location}`;
86
- case 'default':
87
- return 'default';
88
- }
89
- };
90
- export function explain(name, resolution) {
91
- const list = resolution.candidates[name];
92
- if (list === undefined)
93
- return `${name} is not an option of this command\n`;
94
- const winner = list.find((c) => c.value !== undefined);
95
- const head = winner === undefined ? `${name} is unset` : `${name} = ${JSON.stringify(winner.value)} from ${describe(winner)}`;
96
- const rest = list
97
- .filter((c) => c !== winner)
98
- .map((c) => `${describe(c)} ${c.value === undefined ? '(unset)' : JSON.stringify(c.value)}`);
99
- return `${head}\n${rest.length === 0 ? '' : ` candidates: ${rest.join(', ')}\n`}`;
100
- }