canary-test-cli 5.15.0 → 6.0.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.
Files changed (79) hide show
  1. package/agent/frameworks/registry.json +655 -0
  2. package/bin/canary.js +20 -15
  3. package/dist/doctor-manifest.d.ts +94 -0
  4. package/dist/doctor.d.ts +67 -0
  5. package/dist/engine/analysis/cli.js +270 -0
  6. package/dist/engine/analysis/engine.js +146 -0
  7. package/dist/engine/analysis/reports.js +0 -0
  8. package/dist/engine/analysis/rows.js +9 -0
  9. package/dist/engine/cli-commands.js +618 -0
  10. package/dist/engine/cli-common.js +60 -0
  11. package/dist/engine/cli.core.js +208 -0
  12. package/dist/engine/cli.js +31 -0
  13. package/dist/engine/company-knowledge-cli.js +201 -0
  14. package/dist/engine/core/ci-env.js +33 -0
  15. package/dist/engine/core/classifier.js +192 -0
  16. package/dist/engine/core/company-knowledge.js +765 -0
  17. package/dist/engine/core/config-validation.js +74 -0
  18. package/dist/engine/core/detection.js +48 -0
  19. package/dist/engine/core/domain-scanner.js +212 -0
  20. package/dist/engine/core/environment-detect.js +410 -0
  21. package/dist/engine/core/executor.js +181 -0
  22. package/dist/engine/core/feedback.js +93 -0
  23. package/dist/engine/core/fixture-scanner.js +173 -0
  24. package/dist/engine/core/framework-registry.js +123 -0
  25. package/dist/engine/core/mcp-validator.js +218 -0
  26. package/dist/engine/core/metadata-scanner.js +147 -0
  27. package/dist/engine/core/migrator.js +1112 -0
  28. package/dist/engine/core/overlays.js +176 -0
  29. package/dist/engine/core/pattern-healer.js +147 -0
  30. package/dist/engine/core/pattern-matcher.js +255 -0
  31. package/dist/engine/core/quality-scorer.js +213 -0
  32. package/dist/engine/core/recommender.js +152 -0
  33. package/dist/engine/core/reporter.js +211 -0
  34. package/dist/engine/core/scaffolder.js +236 -0
  35. package/dist/engine/core/skill-registry.js +522 -0
  36. package/dist/engine/core/static-linter.js +237 -0
  37. package/dist/engine/core/ticket-updater.js +639 -0
  38. package/dist/engine/core/workflow-discovery.js +693 -0
  39. package/dist/engine/guardian/agent-tier.js +338 -0
  40. package/dist/engine/guardian/analysis-emit.js +201 -0
  41. package/dist/engine/guardian/cli.js +787 -0
  42. package/dist/engine/guardian/coverage.js +1055 -0
  43. package/dist/engine/guardian/delta-emitter.js +46 -0
  44. package/dist/engine/guardian/diff-extractor.js +257 -0
  45. package/dist/engine/guardian/hard-gate.js +373 -0
  46. package/dist/engine/guardian/impact-mapper.js +121 -0
  47. package/dist/engine/guardian/pr-check.js +975 -0
  48. package/dist/engine/guardian/pr-comment.js +200 -0
  49. package/dist/engine/guardian/summary-emitter.js +94 -0
  50. package/dist/engine/guardian/tier.js +58 -0
  51. package/dist/engine/history/cli.js +303 -0
  52. package/dist/engine/history/detector.js +68 -0
  53. package/dist/engine/history/ndjson-store.js +177 -0
  54. package/dist/engine/history/record.js +14 -0
  55. package/dist/engine/history/schema.js +59 -0
  56. package/dist/engine/history/store.js +47 -0
  57. package/dist/engine/history/supabase-store.js +113 -0
  58. package/dist/engine/main-deps.js +105 -0
  59. package/dist/engine/mcp-server.js +647 -0
  60. package/dist/engine/package.json +4 -0
  61. package/dist/engine/skills-cli.js +181 -0
  62. package/dist/engine/ui/banner.js +50 -0
  63. package/dist/engine/util/coalesce.js +12 -0
  64. package/dist/engine/util/round.js +43 -0
  65. package/dist/engine/workflow-cli.js +242 -0
  66. package/dist/engine-checks.d.ts +49 -0
  67. package/dist/overlay-commands.d.ts +81 -0
  68. package/dist/overlay-conflicts.d.ts +33 -0
  69. package/dist/overlay-lint.d.ts +19 -0
  70. package/dist/overlays-registry.d.ts +74 -0
  71. package/dist/reporters/testtracker.d.ts +89 -0
  72. package/dist/reporters/testtracker.js +195 -0
  73. package/dist/router.d.ts +12 -0
  74. package/dist/router.js +4 -4
  75. package/dist/skill-requirements.d.ts +57 -0
  76. package/dist/source-spec.d.ts +20 -0
  77. package/package.json +30 -6
  78. package/bin/canary +0 -0
  79. package/scripts/install.js +0 -104
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The main `canary` command -- faithful commander port of `agent/cli.py`
3
+ * (`app`), the culmination of the CLI wave. Mounts the already-ported sub-apps
4
+ * (`guardian`, `history`, `analyze`) plus the inline `skills` / `workflow` /
5
+ * `company-knowledge` sub-apps, and the ~15 top-level commands. Additive: the
6
+ * Python `agent.cli:app` stays the shipping entry point until a later cutover.
7
+ *
8
+ * Conventions follow `guardian/cli.ts` (see `cli-common.ts`): a
9
+ * {@link createCanaryCommand} factory wired to an injectable {@link MainDeps},
10
+ * `CliExit` for business exits, `normalizeUsageExit` on the program AND every
11
+ * subcommand so usage errors exit 2 (typer/click) not commander's default 1, and
12
+ * an eager `-V/--version` global option + `_main` callback analog.
13
+ *
14
+ * Sub-app mounting note: `guardian`/`history`/`analyze` are mounted as FRESH
15
+ * instances via their `createXCommand()` factories (functionally identical to
16
+ * the `guardianCommand`/`historyCommand`/`analyzeCommand` singletons but safe to
17
+ * mount into multiple `createCanaryCommand()` calls, e.g. across tests).
18
+ */
19
+ import { Command, Option } from 'commander';
20
+ import { CliExit, normalizeUsageExit } from './cli-common.js';
21
+ import { createAnalyzeCommand } from './analysis/cli.js';
22
+ import { doctorStub, feedbackCmd, flakeCheckCmd, frameworksCmd, healTestCmd, initCmd, migrateCmd, overlayStub, recommendCmd, reviewTestCmd, runCmd, setupCmd, ticketUpdateCmd, upgradeCmd, versionCmd, } from './cli-commands.js';
23
+ import { buildCompanyKnowledgeCommand } from './company-knowledge-cli.js';
24
+ import { createGuardianCommand } from './guardian/cli.js';
25
+ import { createHistoryCommand } from './history/cli.js';
26
+ import { buildSkillsCommand } from './skills-cli.js';
27
+ import { buildWorkflowCommand } from './workflow-cli.js';
28
+ import { defaultMainDeps } from './main-deps.js';
29
+ /** Build a fresh `canary` command wired to `depsInit` (defaults fill any gap). */
30
+ export function createCanaryCommand(depsInit = {}) {
31
+ const deps = { ...defaultMainDeps(), ...depsInit };
32
+ const program = new Command();
33
+ program
34
+ .name('canary')
35
+ .description('Canary -- AI-powered test automation agent.')
36
+ .option('-V, --version', 'Show version and exit.')
37
+ .exitOverride(normalizeUsageExit)
38
+ .hook('preAction', () => {
39
+ if (program.opts()['version']) {
40
+ versionCmd(deps);
41
+ throw new CliExit(0);
42
+ }
43
+ })
44
+ .action(() => {
45
+ if (program.opts()['version']) {
46
+ versionCmd(deps);
47
+ throw new CliExit(0);
48
+ }
49
+ // Python `typer.Typer(no_args_is_help=True)` prints help and exits 2 on a
50
+ // bare `canary` invocation (a usage exit), NOT 0.
51
+ program.outputHelp();
52
+ throw new CliExit(2);
53
+ });
54
+ program
55
+ .command('recommend')
56
+ .description('Classify a test prompt and recommend the best framework -- no API key required.')
57
+ .argument('<prompt>')
58
+ .option('--json', 'Output as JSON for tool integration.')
59
+ .action((prompt, opts) => {
60
+ recommendCmd(prompt, opts, deps);
61
+ });
62
+ program
63
+ .command('frameworks')
64
+ .description('List the supported testing frameworks and how to run each.')
65
+ .option('--json', 'Dump the registry as JSON for tool integration.')
66
+ .action((opts) => {
67
+ frameworksCmd(opts, deps);
68
+ });
69
+ program
70
+ .command('feedback')
71
+ .description('Report a bug, UX issue, doc gap, or idea.')
72
+ .argument('[message]', 'Your feedback message.')
73
+ .addOption(new Option('--category <category>', 'bug | ux | docs | idea').default('idea'))
74
+ .option('--json', 'Emit the payload + issue URL as JSON.')
75
+ .option('--open', 'Open the pre-filled issue in a browser.')
76
+ .action((message, opts) => {
77
+ feedbackCmd(message, opts, deps);
78
+ });
79
+ program
80
+ .command('run')
81
+ .description("Execute a test file using Canary's integrated executor.")
82
+ .argument('<file_path>')
83
+ .argument('<framework>', 'Framework to use (e.g., playwright, pytest)')
84
+ .action((filePath, framework) => {
85
+ runCmd(filePath, framework, deps);
86
+ });
87
+ program
88
+ .command('init')
89
+ .description('Scaffold a test suite, or -- with no framework -- show setup options.')
90
+ .argument('[framework]', 'Framework to scaffold (playwright, vitest, pytest, k6). Omit to see options.')
91
+ .action((framework) => {
92
+ initCmd(framework, deps);
93
+ });
94
+ program
95
+ .command('setup')
96
+ .description('Set up canary in this repo -- interactive .canary/company.json wizard.')
97
+ .option('--force', 'Overwrite an existing .canary/company.json.')
98
+ .action((opts) => {
99
+ setupCmd(opts, deps);
100
+ });
101
+ program
102
+ .command('migrate')
103
+ .description("Migrate a harness-scaffolded test-suite project to Canary's layout.")
104
+ .addOption(new Option('-p, --path <path>', 'Project root to migrate (default: current directory).').default('.'))
105
+ .option('-f, --framework <framework>', 'Override auto-detected framework.')
106
+ .option('--from <overlay>', 'Tracked overlay (name or path) whose .canary/skills/ are deployed into the target.')
107
+ .option('-o, --overlay <path>', '[deprecated: use --from] Path to an overlay repo whose .canary/skills/ are deployed.')
108
+ .option('--apply', 'Write files. Without this flag the command is a dry run.')
109
+ .option('--check', 'Freshness gate: report drift without writing.')
110
+ .option('--json', 'Emit the report as JSON.')
111
+ .action((opts) => {
112
+ migrateCmd(opts, deps);
113
+ });
114
+ program
115
+ .command('review-test')
116
+ .description('Lint test files for quality issues -- no LLM or API key required.')
117
+ .argument('<path>', 'Test file or directory to lint.')
118
+ .option('--static', 'Run static-only analysis (no LLM).')
119
+ .option('--no-static', 'Disable static-only analysis.')
120
+ .option('-f, --framework <framework>', 'Force framework: pytest, playwright, vitest, k6.')
121
+ .option('--json', 'Output findings as JSON.')
122
+ .action((path, opts) => {
123
+ reviewTestCmd(path, opts, deps);
124
+ });
125
+ program
126
+ .command('flake-check')
127
+ .description('Detect flakiness patterns in test files -- no LLM or API key required.')
128
+ .argument('<path>', 'Test file or directory to check.')
129
+ .option('--json', 'Output findings as JSON.')
130
+ .action((path, opts) => {
131
+ flakeCheckCmd(path, opts, deps);
132
+ });
133
+ program
134
+ .command('heal-test')
135
+ .description('Apply deterministic pattern fixes to a test file -- no LLM required.')
136
+ .argument('<path>', 'Test file to heal.')
137
+ .option('--pattern', 'Apply regex-safe pattern fixes (no LLM).')
138
+ .option('--no-pattern', 'Disable pattern fixes.')
139
+ .option('--dry-run', 'Show what would change without writing.')
140
+ .option('--json', 'Output results as JSON.')
141
+ .action((path, opts) => {
142
+ healTestCmd(path, opts, deps);
143
+ });
144
+ program
145
+ .command('version')
146
+ .description('Show Canary version info.')
147
+ .action(() => {
148
+ versionCmd(deps);
149
+ });
150
+ program
151
+ .command('upgrade')
152
+ .description('Upgrade Canary to the latest published version.')
153
+ .option('--dry-run', 'Show what would change without upgrading.')
154
+ .action((opts) => {
155
+ upgradeCmd(opts, deps);
156
+ });
157
+ program
158
+ .command('overlay')
159
+ .description('Manage tracked overlays (requires the npm install of Canary).')
160
+ .allowUnknownOption(true)
161
+ .allowExcessArguments(true)
162
+ .argument('[args...]')
163
+ .action(() => {
164
+ overlayStub(deps);
165
+ });
166
+ program
167
+ .command('doctor')
168
+ .description('Diagnose your Canary setup (requires the npm install of Canary).')
169
+ .allowUnknownOption(true)
170
+ .allowExcessArguments(true)
171
+ .argument('[args...]')
172
+ .action(() => {
173
+ doctorStub(deps);
174
+ });
175
+ program
176
+ .command('ticket-update')
177
+ .description('Post a run comment and/or transition the linked ticket after a test run.')
178
+ .option('--test-file <path>', 'Test file to extract linkage from (default: last run).')
179
+ .option('--result <path>', 'Path to canary report JSON (default: auto-detect).')
180
+ .option('--dry-run', "Show what would be posted/transitioned; don't write.")
181
+ .option('--comment-only', 'Post comment but skip transition.')
182
+ .option('--transition-only', 'Transition only, skip comment.')
183
+ .option('--project <project>', 'Override auto-detected project key.')
184
+ .option('--ticket <ticket>', 'Override auto-detected ticket key (e.g. PROJ-1234).')
185
+ .action(async (opts) => {
186
+ await ticketUpdateCmd(opts, deps);
187
+ });
188
+ // Sub-apps (fresh instances -- see module docstring). The already-ported
189
+ // guardian/history/analyze factories carry their own deps; we forward the main
190
+ // out/err sinks so their output flows through the same channel (captured in
191
+ // tests, process.stdout in production).
192
+ const sinks = { out: deps.out, err: deps.err };
193
+ program.addCommand(createHistoryCommand(sinks));
194
+ program.addCommand(createAnalyzeCommand(sinks));
195
+ program.addCommand(createGuardianCommand(sinks));
196
+ program.addCommand(buildSkillsCommand(deps));
197
+ program.addCommand(buildWorkflowCommand(deps));
198
+ program.addCommand(buildCompanyKnowledgeCommand(deps));
199
+ // Propagate the usage-exit normalization to every top-level command (the
200
+ // sub-apps also set it on their own subcommands internally).
201
+ for (const sub of program.commands) {
202
+ sub.exitOverride(normalizeUsageExit);
203
+ }
204
+ return program;
205
+ }
206
+ /** The production `canary` command (process-backed defaults). */
207
+ export const canaryCommand = createCanaryCommand();
208
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // GENERATED by npm/scripts/build-engine.mjs -- do not edit by hand.
3
+ // Executable counterpart of the compiled command module (cli.core.js), which
4
+ // only exports createCanaryCommand. Mirrors ts/bin/canary.js.
5
+ import { createRequire } from 'node:module';
6
+
7
+ import { CommanderError } from 'commander';
8
+
9
+ import { createCanaryCommand } from './cli.core.js';
10
+ import { CliExit } from './cli-common.js';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ function readVersion() {
15
+ try {
16
+ return require('../../package.json').version || 'unknown';
17
+ } catch {
18
+ return 'unknown';
19
+ }
20
+ }
21
+
22
+ const program = createCanaryCommand({ pkgVersion: () => readVersion() });
23
+
24
+ try {
25
+ await program.parseAsync(process.argv.slice(2), { from: 'user' });
26
+ } catch (err) {
27
+ if (err instanceof CliExit) process.exit(err.code);
28
+ if (err instanceof CommanderError) process.exit(err.exitCode);
29
+ console.error(err);
30
+ process.exit(1);
31
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * `canary company-knowledge` sub-app -- faithful port of the `ck_app` commands
3
+ * in `agent/cli.py` (`show` + `init`). `init` is exported as {@link ckInitCmd}
4
+ * so the top-level `setup` alias can call it directly (Python `setup` -> `ck_init`).
5
+ *
6
+ * The interactive `init` wizard reads through {@link MainDeps.prompt} (typer.prompt
7
+ * analog); the production default reads piped stdin lines and falls back to the
8
+ * shown default, so a non-interactive/CI invocation keeps existing values.
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { Command, Option } from 'commander';
13
+ import pc from 'picocolors';
14
+ import { CliExit, jsonIndent2, normalizeUsageExit } from './cli-common.js';
15
+ import { CompanyKnowledge } from './core/company-knowledge.js';
16
+ import { CHECK, CROSS, WARN } from './main-deps.js';
17
+ function ckShowCmd(opts, deps) {
18
+ const ck = deps.loadCompanyKnowledge(opts.env ?? null);
19
+ if (ck.error && ck.isEmpty) {
20
+ deps.out(`${pc.red(CROSS)} ${ck.error}`);
21
+ throw new CliExit(1);
22
+ }
23
+ if (opts.json) {
24
+ deps.out(jsonIndent2(ck.toDict()));
25
+ return;
26
+ }
27
+ if (ck.isEmpty) {
28
+ deps.out(`${pc.yellow('No company knowledge configured.')} Create ${pc.bold('.canary/company.json')} or run ${pc.bold('canary company-knowledge init')}.`);
29
+ return;
30
+ }
31
+ const sourcesStr = ck.sources.length ? ck.sources.join(', ') : 'none';
32
+ deps.out(`${pc.bold(pc.green(`${CHECK} Company Knowledge`))} ${pc.dim(`sources: ${sourcesStr}`)}\n`);
33
+ if (ck.confluence_spaces.length) {
34
+ deps.out(`${pc.bold('Confluence spaces:')} ${ck.confluence_spaces.join(', ')}`);
35
+ }
36
+ if (ck.jira_projects.length) {
37
+ deps.out(`${pc.bold('Jira projects:')} ${ck.jira_projects.join(', ')}`);
38
+ }
39
+ if (ck.internal_doc_urls.length) {
40
+ deps.out(pc.bold('Reference docs:'));
41
+ for (const url of ck.internal_doc_urls)
42
+ deps.out(` ${url}`);
43
+ }
44
+ if (ck.internal_domains.length) {
45
+ deps.out(`${pc.bold('Internal domains:')} ${ck.internal_domains.join(', ')}`);
46
+ }
47
+ if (ck.mcp_servers.length) {
48
+ deps.out(`${pc.bold('MCP servers:')} ${ck.mcp_servers.join(', ')}`);
49
+ }
50
+ if (ck.claude_code_skills.length) {
51
+ deps.out(`${pc.bold('Claude Code skills:')} ${ck.claude_code_skills.join(', ')}`);
52
+ }
53
+ if (ck.dashboard_url) {
54
+ deps.out(`${pc.bold('Dashboard URL:')} ${ck.dashboard_url}`);
55
+ }
56
+ if (ck.otel_exporter_endpoint) {
57
+ deps.out(`${pc.bold('OTel endpoint:')} ${ck.otel_exporter_endpoint}`);
58
+ }
59
+ if (ck.notes) {
60
+ deps.out(`${pc.bold('Notes:')} ${ck.notes}`);
61
+ }
62
+ if (!ck.brand.isEmpty) {
63
+ const label = ck.brand.assets['company_name'] || '(unnamed)';
64
+ deps.out(`${pc.bold('Brand:')} ${label} ${pc.dim('(customer-facing reports)')}`);
65
+ }
66
+ if (ck.error) {
67
+ deps.out(`\n${pc.yellow(WARN)} ${ck.error}`);
68
+ }
69
+ if (ck.warnings.length) {
70
+ deps.out('');
71
+ for (const w of ck.warnings)
72
+ deps.out(`${pc.yellow(WARN)} ${w}`);
73
+ }
74
+ }
75
+ /**
76
+ * Interactive `.canary/company.json` wizard. Exported so `setup` can alias it.
77
+ */
78
+ export function ckInitCmd(opts, deps) {
79
+ const canaryDir = join(deps.cwd(), '.canary');
80
+ const outPath = join(canaryDir, 'company.json');
81
+ // Existing values become the shown defaults (load() returns empty when absent).
82
+ const existing = CompanyKnowledge.load(deps.cwd(), null, deps.home());
83
+ if (existsSync(outPath) && !opts.force) {
84
+ deps.out(`${pc.yellow(WARN)} ${pc.bold(outPath)} already exists.\nExisting values will be shown as defaults. Pass ${pc.bold('--force')} to start from scratch.`);
85
+ deps.out('');
86
+ }
87
+ const promptList = (label, current, hint = '') => {
88
+ const defaultStr = current.join(', ');
89
+ const displayDefault = defaultStr ? ` [${defaultStr}]` : '';
90
+ const text = `${label}${displayDefault} (comma-separated${hint ? ', ' + hint : ''}): `;
91
+ const raw = deps.prompt(text, defaultStr);
92
+ if (!raw.trim())
93
+ return current;
94
+ return raw
95
+ .split(',')
96
+ .map((v) => v.trim())
97
+ .filter((v) => v);
98
+ };
99
+ const promptStr = (label, current, hint = '') => {
100
+ const displayDefault = current ? ` [${current}]` : '';
101
+ const text = `${label}${displayDefault}${hint ? ` (${hint}): ` : ': '}`;
102
+ return deps.prompt(text, current).trim();
103
+ };
104
+ deps.out(pc.bold(pc.cyan('Canary Company Knowledge Setup')) + '\n');
105
+ deps.out('Enter values for each pointer field, or press Enter to keep the current value.');
106
+ deps.out(`Leave a field empty to skip it. ${pc.dim('Secrets are never accepted here.')}\n`);
107
+ const confluenceSpaces = promptList('Confluence space keys', existing.confluence_spaces, 'uppercase, e.g. QA');
108
+ const jiraProjects = promptList('Jira project keys', existing.jira_projects, 'uppercase, e.g. PROJ');
109
+ deps.out('\nInternal doc URLs (one per line, blank line to finish):');
110
+ const docUrls = [...existing.internal_doc_urls];
111
+ if (docUrls.length)
112
+ deps.out(` Current: ${docUrls.join(', ')}`);
113
+ for (;;) {
114
+ const url = deps.prompt(' URL (or Enter to finish)', '');
115
+ if (!url.trim())
116
+ break;
117
+ docUrls.push(url.trim());
118
+ }
119
+ const internalDomains = promptList('\nInternal hostnames', existing.internal_domains, 'e.g. corp.example.com');
120
+ const mcpServers = promptList('MCP server identifiers', existing.mcp_servers, 'e.g. plugin_atlassian_atlassian');
121
+ const claudeCodeSkills = promptList('Claude Code skill slugs', existing.claude_code_skills, 'e.g. team:skill-name');
122
+ const notesRaw = promptStr('\nFree-text notes for the LLM', existing.notes, 'no secrets');
123
+ const notes = notesRaw ? notesRaw.slice(0, 2048) : '';
124
+ deps.out(`\n${pc.bold('Brand assets')} ${pc.dim('(for customer-facing reports; all optional)')}`);
125
+ const existingBrand = existing.brand.assets;
126
+ const brand = { ...existingBrand };
127
+ const ba = (k) => existingBrand[k] ?? '';
128
+ const prompted = {
129
+ company_name: promptStr('Company name', ba('company_name'), 'e.g. Acme Corp'),
130
+ logo_path: promptStr('Logo path (in-repo)', ba('logo_path'), 'e.g. assets/logo.svg'),
131
+ logo_url: promptStr('Logo URL (if hosted)', ba('logo_url'), 'https://...'),
132
+ primary_color: promptStr('Primary color', ba('primary_color'), '#RRGGBB'),
133
+ secondary_color: promptStr('Secondary color', ba('secondary_color'), '#RRGGBB'),
134
+ text_color: promptStr('Text color', ba('text_color'), '#RRGGBB'),
135
+ background_color: promptStr('Background color', ba('background_color'), '#RRGGBB'),
136
+ footer_note: promptStr('Report footer note', ba('footer_note'), 'e.g. Acme QA report'),
137
+ };
138
+ for (const [k, v] of Object.entries(prompted)) {
139
+ if (v)
140
+ brand[k] = v;
141
+ }
142
+ const out = {};
143
+ if (confluenceSpaces.length) {
144
+ out['confluence_spaces'] = confluenceSpaces.map((v) => v.toUpperCase());
145
+ }
146
+ if (jiraProjects.length) {
147
+ out['jira_projects'] = jiraProjects.map((v) => v.toUpperCase());
148
+ }
149
+ if (docUrls.length)
150
+ out['internal_doc_urls'] = docUrls;
151
+ if (internalDomains.length) {
152
+ out['internal_domains'] = internalDomains.map((v) => v.toLowerCase());
153
+ }
154
+ if (mcpServers.length)
155
+ out['mcp_servers'] = mcpServers;
156
+ if (claudeCodeSkills.length) {
157
+ out['claude_code_skills'] = claudeCodeSkills.map((v) => v.toLowerCase());
158
+ }
159
+ if (notes)
160
+ out['notes'] = notes;
161
+ if (Object.keys(brand).length)
162
+ out['brand'] = brand;
163
+ mkdirSync(canaryDir, { recursive: true });
164
+ const gitignore = join(deps.cwd(), '.gitignore');
165
+ if (existsSync(gitignore)) {
166
+ const content = readFileSync(gitignore, 'utf-8');
167
+ if (!content.includes('.canary/')) {
168
+ writeFileSync(gitignore, content.replace(/\s+$/, '') + '\n.canary/\n', 'utf-8');
169
+ }
170
+ }
171
+ writeFileSync(outPath, jsonIndent2(out) + '\n', 'utf-8');
172
+ deps.out(`\n${pc.green(CHECK)} Written to ${pc.bold(outPath)}`);
173
+ deps.out(pc.dim('Verify with: canary company-knowledge show'));
174
+ }
175
+ /** Build the `company-knowledge` sub-app wired to `deps`. */
176
+ export function buildCompanyKnowledgeCommand(deps) {
177
+ const program = new Command('company-knowledge');
178
+ program
179
+ .description('Manage company-knowledge pointers in .canary/company.json.')
180
+ .exitOverride(normalizeUsageExit);
181
+ program
182
+ .command('show')
183
+ .description('Print the merged company-knowledge view.')
184
+ .option('-e, --env <env>', "Environment override layer to load (e.g. 'uat'). Defaults to CANARY_ENV.")
185
+ .option('--json', 'Emit raw JSON.')
186
+ .action((opts) => {
187
+ ckShowCmd(opts, deps);
188
+ });
189
+ program
190
+ .command('init')
191
+ .description('Interactively scaffold .canary/company.json.')
192
+ .addOption(new Option('--force', 'Overwrite an existing .canary/company.json.'))
193
+ .action((opts) => {
194
+ ckInitCmd(opts, deps);
195
+ });
196
+ for (const sub of program.commands) {
197
+ sub.exitOverride(normalizeUsageExit);
198
+ }
199
+ return program;
200
+ }
201
+ //# sourceMappingURL=company-knowledge-cli.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CI environment detection for Canary headless optimizations.
3
+ *
4
+ * Faithful TypeScript port of `agent/core/ci_env.py`.
5
+ *
6
+ * Python→TS nuances:
7
+ * - `os.environ.get(v)` → `process.env[v]` (an unset var is `undefined`).
8
+ * - Python's `any(os.environ.get(v) ...)` uses truthiness: a non-empty
9
+ * string is truthy, `""` is falsy, and `"0"` is truthy (non-empty). JS
10
+ * `Boolean(process.env[v])` matches exactly — `undefined`/`""` → false,
11
+ * `"0"`/`"true"` → true.
12
+ */
13
+ // Most platforms set CI=true; GitLab sets CI_SERVER; Bitbucket sets
14
+ // BITBUCKET_BUILD_NUMBER.
15
+ const CI_VARS = [
16
+ 'CI',
17
+ 'GITHUB_ACTIONS',
18
+ 'CIRCLECI',
19
+ 'TRAVIS',
20
+ 'CI_SERVER',
21
+ 'BITBUCKET_BUILD_NUMBER',
22
+ 'JENKINS_URL',
23
+ 'TEAMCITY_VERSION',
24
+ ];
25
+ /**
26
+ * Return true when a recognized CI environment variable is set and non-empty.
27
+ *
28
+ * Python: `is_ci`.
29
+ */
30
+ export function isCi() {
31
+ return CI_VARS.some((v) => Boolean(process.env[v]));
32
+ }
33
+ //# sourceMappingURL=ci-env.js.map
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Test Classifier — rule-based intent/test-type detection from a prompt.
3
+ *
4
+ * Faithful TypeScript port of `agent/core/classifier.py`. Confidence values are
5
+ * hand-picked heuristic priors (ordinal signal strength), NOT calibrated
6
+ * probabilities — see the Python module docstring. `TestClassifier` is a domain
7
+ * class name, not a test suite.
8
+ */
9
+ // HTTP verb + slash-prefixed path: "GET /users", "POST /items/{id}".
10
+ const HTTP_VERB_PATH_RE = /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+\//i;
11
+ // Bare uppercase HTTP verb (case-sensitive, to avoid English-word false hits).
12
+ const HTTP_VERB_RE = /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/;
13
+ // Explicit framework name → implied test_type. Order matters: the alternation
14
+ // is tried leftmost-first at the earliest match position (as in Python).
15
+ const FRAMEWORK_HINTS = [
16
+ ['playwright', 'e2e_ui'],
17
+ ['cypress', 'e2e_ui'],
18
+ ['vitest', 'frontend_unit'],
19
+ ['jest', 'frontend_unit'],
20
+ ['pytest', 'api'],
21
+ ['hurl', 'api'],
22
+ ['k6', 'performance'],
23
+ ['axe', 'accessibility'],
24
+ ['axe-core', 'accessibility'],
25
+ ['pa11y', 'accessibility'],
26
+ ['zap', 'security'],
27
+ ['backstopjs', 'visual'],
28
+ ['percy', 'visual'],
29
+ ['pact', 'contract'],
30
+ ['schemathesis', 'contract'],
31
+ ['chaos-toolkit', 'chaos'],
32
+ ['faker', 'synthetic_data'],
33
+ ['sdv', 'synthetic_data'],
34
+ ['opentelemetry', 'observability'],
35
+ ['maestro', 'mobile'],
36
+ ['appium', 'mobile'],
37
+ ['wdio', 'mobile'],
38
+ ['webdriverio', 'mobile'],
39
+ ['locust', 'load'],
40
+ ['gatling', 'load'],
41
+ ['stryker', 'mutation'],
42
+ ['mutmut', 'mutation'],
43
+ ['semgrep', 'static_analysis'],
44
+ ['testcontainers', 'integration'],
45
+ ['fast-check', 'property'],
46
+ ['fastcheck', 'property'],
47
+ ['hypothesis', 'property'],
48
+ ['promptfoo', 'llm_eval'],
49
+ ];
50
+ const HINT_TYPE = new Map(FRAMEWORK_HINTS);
51
+ // Specialized categories keyed by high-specificity phrases; first match wins.
52
+ const CATEGORY_KEYWORDS = [
53
+ ['accessibility', ['accessibility', 'a11y', 'wcag', 'screen reader']],
54
+ [
55
+ 'security',
56
+ [
57
+ 'security test',
58
+ 'pentest',
59
+ 'penetration test',
60
+ 'vulnerability scan',
61
+ 'owasp',
62
+ 'dast',
63
+ 'sast',
64
+ ],
65
+ ],
66
+ [
67
+ 'visual',
68
+ [
69
+ 'visual regression',
70
+ 'visual test',
71
+ 'screenshot test',
72
+ 'snapshot test',
73
+ 'pixel diff',
74
+ ],
75
+ ],
76
+ [
77
+ 'contract',
78
+ [
79
+ 'contract test',
80
+ 'consumer-driven contract',
81
+ 'pact test',
82
+ 'openapi contract',
83
+ 'schema contract',
84
+ ],
85
+ ],
86
+ [
87
+ 'chaos',
88
+ ['chaos engineering', 'chaos test', 'fault injection', 'resilience test'],
89
+ ],
90
+ [
91
+ 'synthetic_data',
92
+ ['synthetic data', 'fake data', 'test data generation', 'data generation'],
93
+ ],
94
+ [
95
+ 'observability',
96
+ [
97
+ 'observability',
98
+ 'telemetry',
99
+ 'distributed tracing',
100
+ 'instrumentation test',
101
+ ],
102
+ ],
103
+ [
104
+ 'mobile',
105
+ [
106
+ 'mobile test',
107
+ 'android test',
108
+ 'ios test',
109
+ 'react native test',
110
+ 'mobile app test',
111
+ ],
112
+ ],
113
+ ['mutation', ['mutation test', 'mutation testing', 'mutation score']],
114
+ [
115
+ 'static_analysis',
116
+ ['static analysis', 'lint rule', 'code smell', 'sonarqube'],
117
+ ],
118
+ ['load', ['soak test', 'spike test', 'concurrent users', 'load profile']],
119
+ [
120
+ 'integration',
121
+ ['integration test', 'integration testing', 'end-to-end integration'],
122
+ ],
123
+ [
124
+ 'property',
125
+ [
126
+ 'property-based',
127
+ 'property based',
128
+ 'property test',
129
+ 'invariant test',
130
+ 'quickcheck',
131
+ 'generative test',
132
+ ],
133
+ ],
134
+ [
135
+ 'llm_eval',
136
+ [
137
+ 'llm eval',
138
+ 'llm evaluation',
139
+ 'prompt regression',
140
+ 'prompt eval',
141
+ 'llm behavior',
142
+ 'llm regression',
143
+ ],
144
+ ],
145
+ ];
146
+ function escapeRe(s) {
147
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
148
+ }
149
+ const FRAMEWORK_HINT_RE = new RegExp('\\b(' + FRAMEWORK_HINTS.map(([k]) => escapeRe(k)).join('|') + ')\\b', 'i');
150
+ /** The lowercase framework name explicitly named in the prompt, if any. */
151
+ export function extractFrameworkHint(prompt) {
152
+ const m = FRAMEWORK_HINT_RE.exec(prompt);
153
+ return m ? m[1].toLowerCase() : null;
154
+ }
155
+ function result(testType, confidence) {
156
+ return { intent: 'generate_tests', test_type: testType, confidence };
157
+ }
158
+ function matchesAny(haystack, needles) {
159
+ return needles.some((n) => haystack.includes(n));
160
+ }
161
+ const PERFORMANCE_KEYWORDS = ['performance', 'load test', 'stress test'];
162
+ // [keywords, testType, confidence] — the generic fallbacks checked after the
163
+ // framework-hint and HTTP-signal rules; first match wins.
164
+ const TRAILING_RULES = [
165
+ [['api', 'endpoint', 'request'], 'api', 0.85],
166
+ [['component', 'react', 'frontend'], 'frontend_unit', 0.9],
167
+ [['login', 'checkout', 'user flow'], 'e2e_ui', 0.8],
168
+ ];
169
+ export class TestClassifier {
170
+ classify(prompt) {
171
+ const lower = prompt.toLowerCase();
172
+ if (matchesAny(lower, PERFORMANCE_KEYWORDS))
173
+ return result('performance', 0.95);
174
+ for (const [testType, keywords] of CATEGORY_KEYWORDS) {
175
+ if (matchesAny(lower, keywords))
176
+ return result(testType, 0.88);
177
+ }
178
+ const hint = FRAMEWORK_HINT_RE.exec(prompt);
179
+ if (hint)
180
+ return result(HINT_TYPE.get(hint[1].toLowerCase()), 0.95);
181
+ if (HTTP_VERB_PATH_RE.test(prompt))
182
+ return result('api', 0.95);
183
+ if (HTTP_VERB_RE.test(prompt))
184
+ return result('api', 0.85);
185
+ for (const [keywords, testType, confidence] of TRAILING_RULES) {
186
+ if (matchesAny(lower, keywords))
187
+ return result(testType, confidence);
188
+ }
189
+ return result('e2e_ui', 0.5);
190
+ }
191
+ }
192
+ //# sourceMappingURL=classifier.js.map