@sdods/cli 0.17.0 → 0.19.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/.tsbuildinfo +1 -1
- package/dist/commands/import.d.ts +7 -0
- package/dist/commands/import.js +147 -0
- package/dist/commands/run.js +31 -4
- package/dist/program.js +2 -0
- package/package.json +9 -9
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { planCucumberImport } from '@sdods/core';
|
|
3
|
+
import { type CliContext } from '../context.js';
|
|
4
|
+
/** Every file of the proposed project, keyed by its path from the repository root. */
|
|
5
|
+
export declare function cucumberProjectFiles(ctx: Pick<CliContext, 'rootDir' | 'registry'>, plan: ReturnType<typeof planCucumberImport>, envName: string): Record<string, string>;
|
|
6
|
+
export declare function register(program: Command): void;
|
|
7
|
+
//# sourceMappingURL=import.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { stringify as toYaml } from 'yaml';
|
|
5
|
+
import { ProjectConfigSchema, SlugSchema } from '@sdods/contracts';
|
|
6
|
+
import { PROJECT_FILE, SdodsError, analyzePlaywrightSuite, planCucumberImport } from '@sdods/core';
|
|
7
|
+
import { createContext } from '../context.js';
|
|
8
|
+
import { projectTemplateFiles } from '../templates/project.js';
|
|
9
|
+
import { json, ok, out } from '../ui.js';
|
|
10
|
+
/**
|
|
11
|
+
* `sdods import cucumber|playwright <dir>`: map an existing suite into an SDODS project, as a
|
|
12
|
+
* proposal a person accepts (docs/adr/0001-import-existing-suites.md). Never writes the tree.
|
|
13
|
+
*/
|
|
14
|
+
function sourceDir(dir) {
|
|
15
|
+
const abs = resolve(dir);
|
|
16
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory()) {
|
|
17
|
+
throw new SdodsError('CONFIG_INVALID', `${dir} is not a directory.`, { exitCode: 2 });
|
|
18
|
+
}
|
|
19
|
+
return abs;
|
|
20
|
+
}
|
|
21
|
+
function checkSlug(ctx, slug) {
|
|
22
|
+
SlugSchema.parse(slug);
|
|
23
|
+
if (existsSync(join(ctx.registry.projectsDir, slug, PROJECT_FILE))) {
|
|
24
|
+
throw new SdodsError('CONFIG_INVALID', `Project "${slug}" already exists.`, {
|
|
25
|
+
hint: 'Pick another --project slug; an import proposes a new project.',
|
|
26
|
+
exitCode: 2,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return slug;
|
|
30
|
+
}
|
|
31
|
+
/** Every file of the proposed project, keyed by its path from the repository root. */
|
|
32
|
+
export function cucumberProjectFiles(ctx, plan, envName) {
|
|
33
|
+
const config = ProjectConfigSchema.parse({
|
|
34
|
+
slug: plan.slug,
|
|
35
|
+
name: plan.slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
36
|
+
description: `Imported from a Cucumber-JS suite by sdods import cucumber.`,
|
|
37
|
+
layers: plan.layers,
|
|
38
|
+
browsers: ['chromium'],
|
|
39
|
+
envs: { default: envName, available: [envName] },
|
|
40
|
+
tags: { extra: plan.extraTags },
|
|
41
|
+
});
|
|
42
|
+
const scaffold = projectTemplateFiles({
|
|
43
|
+
config,
|
|
44
|
+
envName,
|
|
45
|
+
uiUrl: 'http://localhost:3000',
|
|
46
|
+
apiUrl: 'http://localhost:3000/api',
|
|
47
|
+
});
|
|
48
|
+
for (const key of Object.keys(scaffold))
|
|
49
|
+
if (key.startsWith('features/'))
|
|
50
|
+
delete scaffold[key];
|
|
51
|
+
const project = {
|
|
52
|
+
...scaffold,
|
|
53
|
+
[PROJECT_FILE]: `# SDODS project, imported by sdods import cucumber. Reference: https://docs.sdods.com/docs/reference/project-yaml\n${toYaml({
|
|
54
|
+
slug: config.slug,
|
|
55
|
+
name: config.name,
|
|
56
|
+
description: config.description,
|
|
57
|
+
layers: config.layers,
|
|
58
|
+
browsers: config.browsers,
|
|
59
|
+
testIdAttribute: config.testIdAttribute,
|
|
60
|
+
tags: { suites: config.tags.suites, extra: config.tags.extra, roles: config.tags.roles },
|
|
61
|
+
envs: config.envs,
|
|
62
|
+
heal: { enabled: true },
|
|
63
|
+
retries: { ci: 2, local: 0 },
|
|
64
|
+
}, { lineWidth: 100 })}`,
|
|
65
|
+
'IMPORT-REPORT.md': plan.report,
|
|
66
|
+
};
|
|
67
|
+
for (const f of plan.features)
|
|
68
|
+
project[f.path] = f.content;
|
|
69
|
+
if (plan.stepsFile)
|
|
70
|
+
project['steps/imported.steps.ts'] = plan.stepsFile;
|
|
71
|
+
const base = relative(ctx.rootDir, join(ctx.registry.projectsDir, plan.slug));
|
|
72
|
+
return Object.fromEntries(Object.entries(project).map(([k, v]) => [join(base, k), v]));
|
|
73
|
+
}
|
|
74
|
+
export function register(program) {
|
|
75
|
+
const cmd = program
|
|
76
|
+
.command('import')
|
|
77
|
+
.description('Map an existing Cucumber-JS or Playwright suite into an SDODS project (as a proposal)');
|
|
78
|
+
cmd
|
|
79
|
+
.command('cucumber <dir>')
|
|
80
|
+
.description('Read a Cucumber-JS suite (features, step definitions, cucumber.js) and propose an SDODS project')
|
|
81
|
+
.requiredOption('-p, --project <slug>', 'slug of the new project')
|
|
82
|
+
.option('--env <name>', 'first environment name', 'local')
|
|
83
|
+
.option('--module <name>', 'folder under features/ for the imported features', 'imported')
|
|
84
|
+
.option('--dry-run', 'print the report and write nothing, not even a proposal')
|
|
85
|
+
.action(async (dir, opts, command) => {
|
|
86
|
+
const ctx = createContext(command);
|
|
87
|
+
const slug = checkSlug(ctx, opts.project);
|
|
88
|
+
const plan = planCucumberImport(sourceDir(dir), { slug, module: opts.module });
|
|
89
|
+
if (plan.features.length === 0) {
|
|
90
|
+
throw new SdodsError('CONFIG_INVALID', `No .feature files under ${dir}.`, { exitCode: 2 });
|
|
91
|
+
}
|
|
92
|
+
const files = cucumberProjectFiles(ctx, plan, opts.env);
|
|
93
|
+
if (opts.dryRun) {
|
|
94
|
+
if (ctx.opts.json)
|
|
95
|
+
return json({ plan, files: Object.keys(files) });
|
|
96
|
+
out(plan.report);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { ProposalStore } = await import('@sdods/mcp');
|
|
100
|
+
const manifest = new ProposalStore(ctx.rootDir).create({
|
|
101
|
+
role: 'importer',
|
|
102
|
+
project: slug,
|
|
103
|
+
env: opts.env,
|
|
104
|
+
summary: `Import ${plan.mappings.length} scenario(s) from ${plan.features.length} Cucumber-JS feature file(s) into ${slug}: ${plan.steps.core.length} step(s) reuse the SDODS library, ${plan.steps.stubbed.length} stubbed to port.`,
|
|
105
|
+
files: Object.entries(files).map(([path, content]) => ({
|
|
106
|
+
path,
|
|
107
|
+
content,
|
|
108
|
+
op: 'add',
|
|
109
|
+
})),
|
|
110
|
+
});
|
|
111
|
+
if (ctx.opts.json)
|
|
112
|
+
return json({ proposal: manifest, plan });
|
|
113
|
+
ok(`proposal ${manifest.id}: ${manifest.files.length} file(s) for projects/${slug}`);
|
|
114
|
+
out(pc.dim(` ${plan.mappings.length} scenario(s), ${plan.steps.stubbed.length} step stub(s), ${plan.manual.length} item(s) to port by hand`));
|
|
115
|
+
out(` review: ${pc.cyan(`sdods proposals show ${manifest.id}`)}`);
|
|
116
|
+
out(` accept: ${pc.cyan(`sdods proposals accept ${manifest.id}`)}`);
|
|
117
|
+
});
|
|
118
|
+
cmd
|
|
119
|
+
.command('playwright <dir>')
|
|
120
|
+
.description('Analyse a Playwright Test suite and propose the report (converts nothing yet)')
|
|
121
|
+
.requiredOption('-p, --project <slug>', 'slug the suite would be imported as')
|
|
122
|
+
.option('--dry-run', 'print the report and write nothing')
|
|
123
|
+
.action(async (dir, opts, command) => {
|
|
124
|
+
const ctx = createContext(command);
|
|
125
|
+
const slug = checkSlug(ctx, opts.project);
|
|
126
|
+
const analysis = analyzePlaywrightSuite(sourceDir(dir), slug);
|
|
127
|
+
if (opts.dryRun) {
|
|
128
|
+
if (ctx.opts.json)
|
|
129
|
+
return json(analysis);
|
|
130
|
+
out(analysis.report);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const { ProposalStore } = await import('@sdods/mcp');
|
|
134
|
+
const base = relative(ctx.rootDir, join(ctx.registry.projectsDir, slug));
|
|
135
|
+
const manifest = new ProposalStore(ctx.rootDir).create({
|
|
136
|
+
role: 'importer',
|
|
137
|
+
project: slug,
|
|
138
|
+
summary: `Analysis of a Playwright suite (${analysis.specFiles.length} spec file(s), ${analysis.tests} test(s)); converts nothing yet.`,
|
|
139
|
+
files: [{ path: join(base, 'IMPORT-REPORT.md'), content: analysis.report, op: 'add' }],
|
|
140
|
+
});
|
|
141
|
+
if (ctx.opts.json)
|
|
142
|
+
return json({ proposal: manifest, analysis });
|
|
143
|
+
ok(`proposal ${manifest.id}: the analysis report for ${slug}`);
|
|
144
|
+
out(` review: ${pc.cyan(`sdods proposals show ${manifest.id}`)}`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=import.js.map
|
package/dist/commands/run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { isAbsolute, join, relative, resolve as resolvePath } from 'node:path';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { RecordingModeSchema, newRunId, runFiles, } from '@sdods/contracts';
|
|
6
|
-
import { SdodsError, CLI_OVERRIDES_ENV, VERSION, checkPoolCapacity, collectGateEvidence, effectiveWorkers, evaluateGates, formatFindings, hasGates, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, poolAccountsFor, poolDemandByRole, poolTooSmallError, redactRunTraces, serializeCliOverrides, setupTierOf, } from '@sdods/core';
|
|
5
|
+
import { RecordingModeSchema, isRunId, newRunId, runFiles, } from '@sdods/contracts';
|
|
6
|
+
import { SdodsError, CLI_OVERRIDES_ENV, VERSION, checkPoolCapacity, ciHealSummary, collectGateEvidence, collectHealEvents, effectiveWorkers, evaluateGates, formatFindings, hasGates, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, poolAccountsFor, poolDemandByRole, poolTooSmallError, redactRunTraces, serializeCliOverrides, setupTierOf, } from '@sdods/core';
|
|
7
7
|
import { analyzeChangeImpact } from '@sdods/mcp';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { toolCommand, workspaceBin } from '../workspace-bin.js';
|
|
@@ -68,7 +68,7 @@ function addRunOptions(cmd) {
|
|
|
68
68
|
.option('--fail-on-flaky', 'exit 1 when any test is flaky')
|
|
69
69
|
.option('--max-failures <n>', 'stop after n failures', parseIntFlag('max-failures'))
|
|
70
70
|
.option('--timeout <ms>', 'per-test timeout override', parseIntFlag('timeout'))
|
|
71
|
-
.option('--reporter <name>', 'reporter
|
|
71
|
+
.option('--reporter <name>', 'add a reporter: junit (XML), allure (allure-results), json, blob, or any Playwright reporter; name=path sets the output (repeatable)', collect, [])
|
|
72
72
|
.option('--reporter-mode <mode>', 'default | server | quiet')
|
|
73
73
|
.option('--trigger <kind>', 'cli | ui | ci | agent | mcp | schedule', 'cli')
|
|
74
74
|
.option('--allow-empty', 'exit 0 when the selection matches no scenario (by default that is exit 2)')
|
|
@@ -87,6 +87,13 @@ export function register(program) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
export async function runCommand(flags, cmd) {
|
|
90
|
+
// The run id names `.sdods/generated/<runId>`, which is removed recursively when the run ends,
|
|
91
|
+
// so `--run-id ../..` must be refused before anything touches the disk.
|
|
92
|
+
if (flags.runId !== undefined && !isRunId(flags.runId))
|
|
93
|
+
throw new SdodsError('CONFIG_INVALID', `Invalid run id "${flags.runId}".`, {
|
|
94
|
+
hint: 'Use letters, digits, ".", "_" or "-", not starting with "." (or omit --run-id).',
|
|
95
|
+
exitCode: 2,
|
|
96
|
+
});
|
|
90
97
|
const ctx = createContext(cmd);
|
|
91
98
|
const entry = ctx.registry.pick(flags.project);
|
|
92
99
|
const projectCfg = entry.config;
|
|
@@ -469,6 +476,26 @@ export async function runCommand(flags, cmd) {
|
|
|
469
476
|
warn(`trace redaction failed for ${relative(ctx.rootDir, e.file)}: ${e.error}`);
|
|
470
477
|
}
|
|
471
478
|
const summary = readSummary(runDir, runId, exitCode, Date.now() - started);
|
|
479
|
+
// A heal in CI means the page changed under the suite. It must never pass silently
|
|
480
|
+
// (heal.ciMode, default warn), so every one is listed as an annotation and in the step summary.
|
|
481
|
+
if (cfg.runtime.ci) {
|
|
482
|
+
const heals = ciHealSummary(collectHealEvents(runDir));
|
|
483
|
+
if (heals) {
|
|
484
|
+
if (process.env.GITHUB_ACTIONS === 'true' && !ctx.opts.json)
|
|
485
|
+
for (const a of heals.annotations)
|
|
486
|
+
console.log(a);
|
|
487
|
+
else
|
|
488
|
+
warn(heals.line);
|
|
489
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
490
|
+
try {
|
|
491
|
+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${heals.markdown}\n`);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// a read-only or missing summary file must not fail the run
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
472
499
|
// Process gates are judged here, from the run that just finished, before anything publishes it.
|
|
473
500
|
// One shard of several holds only part of the run, so its pass rate and its a11y/perf evidence
|
|
474
501
|
// are not the run's; a sharded process is gated where the shards are merged
|
package/dist/program.js
CHANGED
|
@@ -40,6 +40,7 @@ import { register as registerUpgrade } from './commands/upgrade.js';
|
|
|
40
40
|
import { register as registerFeedback } from './commands/feedback.js';
|
|
41
41
|
import { register as registerLoad } from './commands/load.js';
|
|
42
42
|
import { register as registerMatrix } from './commands/matrix.js';
|
|
43
|
+
import { register as registerImport } from './commands/import.js';
|
|
43
44
|
/**
|
|
44
45
|
* All commands are registered eagerly (cheap) but each module keeps heavy imports
|
|
45
46
|
* (server, agents, DB, browser runner) inside its action via dynamic import.
|
|
@@ -101,6 +102,7 @@ export function buildProgram() {
|
|
|
101
102
|
registerFeedback(program);
|
|
102
103
|
registerLoad(program);
|
|
103
104
|
registerMatrix(program);
|
|
105
|
+
registerImport(program);
|
|
104
106
|
return program;
|
|
105
107
|
}
|
|
106
108
|
//# sourceMappingURL=program.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"funding": "https://sdods.com/sponsor/",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"node": ">=22"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sdods/contracts": "0.
|
|
30
|
-
"@sdods/core": "0.
|
|
29
|
+
"@sdods/contracts": "0.19.0",
|
|
30
|
+
"@sdods/core": "0.19.0",
|
|
31
31
|
"commander": "^15.0.0",
|
|
32
32
|
"execa": "^10.2.1",
|
|
33
33
|
"picocolors": "^1.1.1",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"zod": "^4.6.7",
|
|
37
37
|
"@playwright/test": "^1.64.0",
|
|
38
38
|
"playwright-core": "^1.64.0",
|
|
39
|
-
"@sdods/integrations": "0.
|
|
40
|
-
"@sdods/mcp": "0.
|
|
41
|
-
"@sdods/agents": "0.
|
|
42
|
-
"@sdods/db": "0.
|
|
39
|
+
"@sdods/integrations": "0.19.0",
|
|
40
|
+
"@sdods/mcp": "0.19.0",
|
|
41
|
+
"@sdods/agents": "0.19.0",
|
|
42
|
+
"@sdods/db": "0.19.0",
|
|
43
43
|
"csv-parse": "^7.0.3",
|
|
44
|
-
"@sdods/server": "0.
|
|
44
|
+
"@sdods/server": "0.19.0"
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://sdods.com",
|
|
47
47
|
"bugs": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
|
-
"url": "git+https://github.com/YarlisAISolutions/
|
|
52
|
+
"url": "git+https://github.com/YarlisAISolutions/sdods.git",
|
|
53
53
|
"directory": "packages/cli"
|
|
54
54
|
},
|
|
55
55
|
"keywords": [
|