@sdods/cli 0.3.2 → 0.5.2
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/browsers.d.ts +16 -2
- package/dist/commands/browsers.js +116 -13
- package/dist/commands/config.js +19 -11
- package/dist/commands/doctor.js +26 -22
- package/dist/commands/features.js +6 -4
- package/dist/commands/init.js +4 -2
- package/dist/commands/integrations.js +11 -40
- package/dist/commands/mcp.js +18 -9
- package/dist/commands/project.js +322 -6
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +156 -15
- package/dist/commands/serve.js +30 -10
- package/dist/notify.d.ts +63 -0
- package/dist/notify.js +91 -0
- package/dist/runner-compat.d.ts +19 -0
- package/dist/runner-compat.js +47 -0
- package/package.json +10 -10
- package/templates/.claude/skills/sdods-run/SKILL.md +1 -1
- package/templates/.claude/skills/sdods-start-ui/SKILL.md +313 -0
- package/templates/.env.example +1 -0
- package/templates/projects/demo-shop/sdods.project.yaml +7 -1
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
type Engine = 'chromium' | 'firefox' | 'webkit';
|
|
2
3
|
export interface BrowserStatus {
|
|
3
4
|
name: string;
|
|
4
|
-
engine:
|
|
5
|
+
engine: Engine;
|
|
6
|
+
/** The branded channel this browser launches, when it is not the bundled build. */
|
|
7
|
+
channel: string | null;
|
|
5
8
|
installed: boolean;
|
|
6
9
|
executable: string | null;
|
|
7
10
|
playwrightVersion: string;
|
|
8
11
|
}
|
|
9
12
|
export declare function browserStatuses(names?: string[]): Promise<BrowserStatus[]>;
|
|
10
|
-
/** Install browser engines (used by `browsers install`, `doctor --fix`, `init`). */
|
|
13
|
+
/** Install browser engines and channels (used by `browsers install`, `doctor --fix`, `init`). */
|
|
11
14
|
export declare function installBrowsers(opts?: {
|
|
12
15
|
browsers?: string[];
|
|
13
16
|
withDeps?: boolean;
|
|
14
17
|
cwd?: string;
|
|
15
18
|
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Opens and closes each browser once.
|
|
21
|
+
*
|
|
22
|
+
* A browser that has never run sets up its profile on first launch, and an automation run started
|
|
23
|
+
* before that finishes dies with "Target page, context or browser has been closed" — seen on a
|
|
24
|
+
* freshly installed Edge, where the first `sdods run -b edge` failed every scenario and the
|
|
25
|
+
* identical rerun passed every one. Anywhere a browser is provisioned and used in the same job,
|
|
26
|
+
* this makes the first run deterministic.
|
|
27
|
+
*/
|
|
28
|
+
export declare function warmupBrowsers(names: string[]): Promise<string[]>;
|
|
16
29
|
export declare function register(program: Command): void;
|
|
30
|
+
export {};
|
|
17
31
|
//# sourceMappingURL=browsers.d.ts.map
|
|
@@ -1,19 +1,60 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { execa } from 'execa';
|
|
4
5
|
import pc from 'picocolors';
|
|
5
6
|
import { BrowserSchema } from '@sdods/contracts';
|
|
6
7
|
import { SdodsError } from '@sdods/core';
|
|
7
8
|
import { createContext } from '../context.js';
|
|
8
9
|
import { collect, json, ok, table } from '../ui.js';
|
|
10
|
+
// Typed against BrowserName (not `string`) so a new browser is a compile error here rather than a
|
|
11
|
+
// silent fall-through to chromium.
|
|
9
12
|
const ENGINE_OF = {
|
|
10
13
|
chromium: 'chromium',
|
|
14
|
+
edge: 'chromium',
|
|
11
15
|
firefox: 'firefox',
|
|
12
16
|
webkit: 'webkit',
|
|
13
17
|
'mobile-chrome': 'chromium',
|
|
14
18
|
'mobile-safari': 'webkit',
|
|
15
19
|
};
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Browsers that are a system install of a branded channel rather than a Playwright download.
|
|
22
|
+
* `playwright install msedge` runs the vendor installer; there is nothing in the browsers cache to
|
|
23
|
+
* find afterwards, which is why these need their own install target and their own detection.
|
|
24
|
+
*/
|
|
25
|
+
const CHANNEL_OF = { edge: 'msedge' };
|
|
26
|
+
/** Browsers Playwright downloads, i.e. the ones a bare `browsers list` can expect to be present. */
|
|
27
|
+
const DOWNLOADED_BROWSERS = Object.keys(ENGINE_OF).filter((b) => !CHANNEL_OF[b]);
|
|
28
|
+
/**
|
|
29
|
+
* Where the stable Microsoft Edge binary lives, mirroring Playwright's own channel resolution.
|
|
30
|
+
* `playwright-core`'s `executablePath()` cannot answer this: for a channel browser it returns the
|
|
31
|
+
* bundled Chromium path, which exists on nearly every machine and would report Edge as installed
|
|
32
|
+
* when it is not.
|
|
33
|
+
*/
|
|
34
|
+
function channelExecutable(channel) {
|
|
35
|
+
if (channel !== 'msedge')
|
|
36
|
+
return null;
|
|
37
|
+
if (process.platform === 'darwin')
|
|
38
|
+
return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
|
|
39
|
+
if (process.platform === 'linux')
|
|
40
|
+
return '/opt/microsoft/msedge/msedge';
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
const suffix = join('Microsoft', 'Edge', 'Application', 'msedge.exe');
|
|
43
|
+
const roots = [
|
|
44
|
+
process.env.LOCALAPPDATA,
|
|
45
|
+
process.env.PROGRAMFILES,
|
|
46
|
+
process.env['PROGRAMFILES(X86)'],
|
|
47
|
+
].filter((r) => Boolean(r));
|
|
48
|
+
for (const root of roots) {
|
|
49
|
+
const candidate = join(root, suffix);
|
|
50
|
+
if (existsSync(candidate))
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
return roots.length ? join(roots[0], suffix) : null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
export async function browserStatuses(names = DOWNLOADED_BROWSERS) {
|
|
17
58
|
const require = createRequire(import.meta.url);
|
|
18
59
|
let pw;
|
|
19
60
|
let version = 'unknown';
|
|
@@ -25,6 +66,7 @@ export async function browserStatuses(names = Object.keys(ENGINE_OF)) {
|
|
|
25
66
|
return names.map((name) => ({
|
|
26
67
|
name,
|
|
27
68
|
engine: ENGINE_OF[name] ?? 'chromium',
|
|
69
|
+
channel: CHANNEL_OF[name] ?? null,
|
|
28
70
|
installed: false,
|
|
29
71
|
executable: null,
|
|
30
72
|
playwrightVersion: version,
|
|
@@ -32,9 +74,10 @@ export async function browserStatuses(names = Object.keys(ENGINE_OF)) {
|
|
|
32
74
|
}
|
|
33
75
|
return names.map((name) => {
|
|
34
76
|
const engine = ENGINE_OF[name] ?? 'chromium';
|
|
77
|
+
const channel = CHANNEL_OF[name] ?? null;
|
|
35
78
|
let executable = null;
|
|
36
79
|
try {
|
|
37
|
-
executable = pw[engine].executablePath();
|
|
80
|
+
executable = channel ? channelExecutable(channel) : pw[engine].executablePath();
|
|
38
81
|
}
|
|
39
82
|
catch {
|
|
40
83
|
executable = null;
|
|
@@ -42,51 +85,111 @@ export async function browserStatuses(names = Object.keys(ENGINE_OF)) {
|
|
|
42
85
|
return {
|
|
43
86
|
name,
|
|
44
87
|
engine,
|
|
88
|
+
channel,
|
|
45
89
|
installed: Boolean(executable && existsSync(executable)),
|
|
46
90
|
executable,
|
|
47
91
|
playwrightVersion: version,
|
|
48
92
|
};
|
|
49
93
|
});
|
|
50
94
|
}
|
|
51
|
-
/** Install browser engines (used by `browsers install`, `doctor --fix`, `init`). */
|
|
95
|
+
/** Install browser engines and channels (used by `browsers install`, `doctor --fix`, `init`). */
|
|
52
96
|
export async function installBrowsers(opts = {}) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
const names = (opts.browsers?.length ? opts.browsers : DOWNLOADED_BROWSERS);
|
|
98
|
+
const engines = [...new Set(names.filter((b) => !CHANNEL_OF[b]).map((b) => ENGINE_OF[b] ?? b))];
|
|
99
|
+
const channels = [...new Set(names.map((b) => CHANNEL_OF[b]).filter((c) => !!c))];
|
|
100
|
+
if (engines.length) {
|
|
101
|
+
const args = ['playwright', 'install', ...(opts.withDeps ? ['--with-deps'] : []), ...engines];
|
|
102
|
+
await execa('npx', args, { stdio: 'inherit', cwd: opts.cwd });
|
|
103
|
+
}
|
|
104
|
+
// Channels are a system install run by the vendor's own installer, so `--with-deps` does not
|
|
105
|
+
// apply and they cannot share the engine invocation.
|
|
106
|
+
for (const channel of channels) {
|
|
107
|
+
await execa('npx', ['playwright', 'install', channel], { stdio: 'inherit', cwd: opts.cwd });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Opens and closes each browser once.
|
|
112
|
+
*
|
|
113
|
+
* A browser that has never run sets up its profile on first launch, and an automation run started
|
|
114
|
+
* before that finishes dies with "Target page, context or browser has been closed" — seen on a
|
|
115
|
+
* freshly installed Edge, where the first `sdods run -b edge` failed every scenario and the
|
|
116
|
+
* identical rerun passed every one. Anywhere a browser is provisioned and used in the same job,
|
|
117
|
+
* this makes the first run deterministic.
|
|
118
|
+
*/
|
|
119
|
+
export async function warmupBrowsers(names) {
|
|
120
|
+
const pw = (await import('playwright-core'));
|
|
121
|
+
const warmed = [];
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
const engine = ENGINE_OF[name] ?? 'chromium';
|
|
124
|
+
const channel = CHANNEL_OF[name];
|
|
125
|
+
const browser = await pw[engine].launch({ headless: true, ...(channel ? { channel } : {}) });
|
|
126
|
+
await browser.close();
|
|
127
|
+
warmed.push(name);
|
|
128
|
+
}
|
|
129
|
+
return warmed;
|
|
58
130
|
}
|
|
59
131
|
export function register(program) {
|
|
60
132
|
const browsers = program.command('browsers').description('Install and list browser engines');
|
|
61
133
|
browsers
|
|
62
134
|
.command('install')
|
|
63
|
-
.description('Install
|
|
135
|
+
.description('Install browsers (default: chromium, firefox, webkit)')
|
|
136
|
+
// The positional form reads naturally and is what the docs have always shown, but Commander
|
|
137
|
+
// rejects excess positionals by default, so `browsers install chromium` failed outright.
|
|
138
|
+
.argument('[browsers...]', 'browsers to install, e.g. chromium edge')
|
|
64
139
|
.option('-b, --browser <name>', 'browser to install (repeatable)', collect, [])
|
|
65
140
|
.option('--with-deps', 'also install OS dependencies (Linux CI)')
|
|
66
141
|
.option('-p, --project <slug>', 'install the browsers declared by a project')
|
|
67
|
-
.action(async (opts, cmd) => {
|
|
142
|
+
.action(async (positional, opts, cmd) => {
|
|
68
143
|
const ctx = createContext(cmd);
|
|
69
|
-
let list = opts.browser;
|
|
144
|
+
let list = [...positional, ...opts.browser];
|
|
70
145
|
if (opts.project)
|
|
71
146
|
list = ctx.registry.get(opts.project).browsers;
|
|
72
147
|
for (const b of list)
|
|
73
148
|
BrowserSchema.parse(b);
|
|
74
149
|
await installBrowsers({ browsers: list, withDeps: opts.withDeps, cwd: ctx.rootDir });
|
|
75
|
-
ok(`Installed ${list.length ? list.join(', ') : '
|
|
150
|
+
ok(`Installed ${list.length ? list.join(', ') : DOWNLOADED_BROWSERS.join(', ')}`);
|
|
151
|
+
});
|
|
152
|
+
browsers
|
|
153
|
+
.command('warmup')
|
|
154
|
+
.description('Launch each browser once so the first real run is not its first launch')
|
|
155
|
+
.option('-b, --browser <name>', 'browser to warm up (repeatable)', collect, [])
|
|
156
|
+
.option('-p, --project <slug>', 'warm up the browsers declared by a project')
|
|
157
|
+
.action(async (opts, cmd) => {
|
|
158
|
+
const ctx = createContext(cmd);
|
|
159
|
+
let list = opts.browser;
|
|
160
|
+
if (!list.length && opts.project)
|
|
161
|
+
list = ctx.registry.get(opts.project).browsers;
|
|
162
|
+
if (!list.length)
|
|
163
|
+
list = [...DOWNLOADED_BROWSERS];
|
|
164
|
+
for (const b of list)
|
|
165
|
+
BrowserSchema.parse(b);
|
|
166
|
+
const warmed = await warmupBrowsers(list);
|
|
167
|
+
ok(`Warmed up ${warmed.join(', ')}`);
|
|
76
168
|
});
|
|
77
169
|
browsers
|
|
78
170
|
.command('list')
|
|
79
171
|
.description('Show which browsers are installed and where; exits 1 if any is missing')
|
|
80
172
|
.option('-p, --project <slug>', 'limit to the browsers declared by a project')
|
|
173
|
+
.option('-b, --browser <name>', 'limit to one browser (repeatable)', collect, [])
|
|
81
174
|
.action(async (opts, cmd) => {
|
|
82
175
|
const ctx = createContext(cmd);
|
|
83
|
-
|
|
176
|
+
// A bare `browsers list` reports the browsers Playwright downloads. Channel browsers are a
|
|
177
|
+
// system install that most machines will not have, and exiting 1 for a missing Edge nobody
|
|
178
|
+
// asked for would turn this into a permanently failing command.
|
|
179
|
+
const names = opts.browser.length
|
|
180
|
+
? opts.browser
|
|
181
|
+
: opts.project
|
|
182
|
+
? ctx.registry.get(opts.project).browsers
|
|
183
|
+
: DOWNLOADED_BROWSERS;
|
|
184
|
+
for (const b of names)
|
|
185
|
+
BrowserSchema.parse(b);
|
|
84
186
|
const statuses = await browserStatuses(names);
|
|
85
187
|
if (ctx.opts.json)
|
|
86
188
|
return json(statuses);
|
|
87
189
|
table(statuses.map((s) => ({
|
|
88
190
|
browser: s.name,
|
|
89
191
|
engine: s.engine,
|
|
192
|
+
channel: s.channel ?? '',
|
|
90
193
|
installed: s.installed ? pc.green('yes') : pc.red('no'),
|
|
91
194
|
playwright: s.playwrightVersion,
|
|
92
195
|
executable: s.executable ?? '',
|
package/dist/commands/config.js
CHANGED
|
@@ -16,13 +16,17 @@ export function registerConfigCommands(program) {
|
|
|
16
16
|
const cfg = ctx.registry.resolve(opts.project, opts.env);
|
|
17
17
|
const view = opts.showSecrets ? cfg : redact(cfg);
|
|
18
18
|
if (opts.explain) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
// Values come from the same redacted tree as the other views, so --explain can never show
|
|
20
|
+
// what plain `config show` hides (it used to apply a weaker regex of its own).
|
|
21
|
+
const tree = { project: view.project, env: view.env, runtime: view.runtime };
|
|
22
|
+
const rows = explainConfig(cfg).map((r) => {
|
|
23
|
+
const value = valueAt(tree, r.path);
|
|
24
|
+
return {
|
|
25
|
+
path: r.path,
|
|
26
|
+
layer: r.layer,
|
|
27
|
+
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
26
30
|
if (ctx.opts.json)
|
|
27
31
|
return json(rows);
|
|
28
32
|
return table(rows, ['path', 'layer', 'value']);
|
|
@@ -71,9 +75,13 @@ export function registerConfigCommands(program) {
|
|
|
71
75
|
process.exitCode = 2;
|
|
72
76
|
});
|
|
73
77
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
function valueAt(tree, path) {
|
|
79
|
+
let cur = tree;
|
|
80
|
+
for (const seg of path.split('.')) {
|
|
81
|
+
if (cur === null || typeof cur !== 'object')
|
|
82
|
+
return undefined;
|
|
83
|
+
cur = cur[seg];
|
|
84
|
+
}
|
|
85
|
+
return cur;
|
|
78
86
|
}
|
|
79
87
|
//# sourceMappingURL=config.js.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,6 +4,8 @@ import { execa } from 'execa';
|
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { collectVarRefs, loadDotEnvLayer, loadEnvFile } from '@sdods/core';
|
|
6
6
|
import { createContext } from '../context.js';
|
|
7
|
+
import { browserStatuses } from './browsers.js';
|
|
8
|
+
import { MIN_PLAYWRIGHT_FOR_STEP_RESULTS, installedPlaywrightVersion, stepResultsWarning, } from '../runner-compat.js';
|
|
7
9
|
import { json, out } from '../ui.js';
|
|
8
10
|
export function registerDoctorCommand(program) {
|
|
9
11
|
program
|
|
@@ -23,6 +25,17 @@ export function registerDoctorCommand(program) {
|
|
|
23
25
|
});
|
|
24
26
|
checks.push(await versionCheck('bun', ['--version'], 'optional: bun install/build are faster'));
|
|
25
27
|
checks.push(await versionCheck('runner', ['playwright', '--version'], 'install workspace dependencies: bun install (or npm install)', 'npx'));
|
|
28
|
+
const runnerVersion = installedPlaywrightVersion(ctx.rootDir);
|
|
29
|
+
if (runnerVersion) {
|
|
30
|
+
checks.push({
|
|
31
|
+
name: 'step results',
|
|
32
|
+
ok: !stepResultsWarning(runnerVersion),
|
|
33
|
+
detail: stepResultsWarning(runnerVersion)
|
|
34
|
+
? `@playwright/test ${runnerVersion} records every Gherkin step SKIPPED in messages.ndjson`
|
|
35
|
+
: `@playwright/test ${runnerVersion} (>= ${MIN_PLAYWRIGHT_FOR_STEP_RESULTS})`,
|
|
36
|
+
fix: `npm i -D @playwright/test@^${MIN_PLAYWRIGHT_FOR_STEP_RESULTS} (or bun add -d)`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
26
39
|
const browsers = await browserCheck();
|
|
27
40
|
checks.push(...browsers);
|
|
28
41
|
if (opts.fix && browsers.some((b) => !b.ok)) {
|
|
@@ -297,27 +310,18 @@ async function versionCheck(name, args, fix, bin = name) {
|
|
|
297
310
|
}
|
|
298
311
|
}
|
|
299
312
|
async function browserCheck() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
out.push({
|
|
314
|
-
name: `browser:${b}`,
|
|
315
|
-
ok: false,
|
|
316
|
-
detail: 'browser runner not resolvable',
|
|
317
|
-
fix: 'install workspace dependencies: bun install (or npm install)',
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return out;
|
|
313
|
+
// Reuses `browsers list`'s detection so the two commands can never disagree about what is
|
|
314
|
+
// installed — and so channel browsers are probed by path rather than by the bundled engine's
|
|
315
|
+
// executable, which exists whether or not the branded browser does.
|
|
316
|
+
const statuses = await browserStatuses(['chromium', 'edge', 'firefox', 'webkit']);
|
|
317
|
+
return statuses.map((s) => ({
|
|
318
|
+
name: `browser:${s.name}`,
|
|
319
|
+
ok: s.installed,
|
|
320
|
+
detail: s.installed ? (s.executable ?? 'installed') : 'not installed',
|
|
321
|
+
fix: `sdods browsers install -b ${s.name}`,
|
|
322
|
+
// Edge is a system browser a project opts into. A machine that does not run Edge suites should
|
|
323
|
+
// not get a red doctor for not having it.
|
|
324
|
+
optional: Boolean(s.channel),
|
|
325
|
+
}));
|
|
322
326
|
}
|
|
323
327
|
//# sourceMappingURL=doctor.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relative } from 'node:path';
|
|
2
|
-
import { listFeatureFiles, parseFeatureFile, scenariosOf } from '@sdods/core';
|
|
2
|
+
import { listFeatureFiles, normalizeTagExpr, parseFeatureFile, parseTagExpr, scenariosOf, } from '@sdods/core';
|
|
3
3
|
import { createContext } from '../context.js';
|
|
4
4
|
import { json, table } from '../ui.js';
|
|
5
5
|
export function register(program) {
|
|
@@ -8,18 +8,20 @@ export function register(program) {
|
|
|
8
8
|
.command('list')
|
|
9
9
|
.description('List features with module, layer, suite, scenario count and tags')
|
|
10
10
|
.requiredOption('-p, --project <slug>', 'project slug')
|
|
11
|
-
.option('--tags <expr>', 'only scenarios
|
|
11
|
+
.option('--tags <expr>', 'only scenarios matching this tag expression (as in `sdods run`)')
|
|
12
12
|
.option('--scenarios', 'one row per scenario instead of per feature')
|
|
13
13
|
.action((opts, cmd) => {
|
|
14
14
|
const ctx = createContext(cmd);
|
|
15
15
|
const cfg = ctx.registry.resolve(opts.project);
|
|
16
|
+
const tagExpr = normalizeTagExpr(opts.tags);
|
|
17
|
+
const selector = tagExpr ? parseTagExpr(tagExpr) : undefined;
|
|
16
18
|
const rows = [];
|
|
17
19
|
for (const file of listFeatureFiles(cfg.project.root)) {
|
|
18
20
|
const rel = relative(cfg.project.root, file).replace(/\\/g, '/');
|
|
19
21
|
const parsed = parseFeatureFile(file);
|
|
20
22
|
const mod = ctx.registry.moduleOfFeature(opts.project, rel)?.name ?? '';
|
|
21
|
-
const scenarios = scenariosOf(parsed).filter((s) => !
|
|
22
|
-
if (!scenarios.length &&
|
|
23
|
+
const scenarios = scenariosOf(parsed).filter((s) => !selector || selector.evaluate(s.tags));
|
|
24
|
+
if (!scenarios.length && selector)
|
|
23
25
|
continue;
|
|
24
26
|
if (opts.scenarios) {
|
|
25
27
|
for (const s of scenarios) {
|
package/dist/commands/init.js
CHANGED
|
@@ -136,7 +136,9 @@ export async function initWorkspace(target, flags) {
|
|
|
136
136
|
'@sdods/cli': dep('@sdods/cli'),
|
|
137
137
|
'@sdods/core': dep('@sdods/core'),
|
|
138
138
|
'@sdods/contracts': dep('@sdods/contracts'),
|
|
139
|
-
|
|
139
|
+
// 1.63 is the floor, not a preference: below it an npm-installed workspace records every
|
|
140
|
+
// Gherkin step SKIPPED in messages.ndjson (see runner-compat.ts).
|
|
141
|
+
'@playwright/test': flags.link && flags.pm === 'bun' ? 'link:@playwright/test' : '^1.63.0',
|
|
140
142
|
'playwright-bdd': flags.link && flags.pm === 'bun' ? 'link:playwright-bdd' : '^9.2.0',
|
|
141
143
|
},
|
|
142
144
|
devDependencies: {
|
|
@@ -338,7 +340,7 @@ Docs: https://docs.sdods.com
|
|
|
338
340
|
browsersInstalled = res.exitCode === 0;
|
|
339
341
|
}
|
|
340
342
|
if (flags.claude) {
|
|
341
|
-
const res = await execa('npx', ['sdods', 'agent', 'install-claude'], {
|
|
343
|
+
const res = await execa('npx', ['-y', '@sdods/cli', 'agent', 'install-claude'], {
|
|
342
344
|
cwd: target,
|
|
343
345
|
reject: false,
|
|
344
346
|
stdio: 'pipe',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_ARTIFACTS_DIR } from '@sdods/core';
|
|
5
5
|
import { createContext } from '../context.js';
|
|
6
|
+
import { LINKS_FILE, notifyRun } from '../notify.js';
|
|
6
7
|
import { json, ok, out, table, warn } from '../ui.js';
|
|
7
|
-
const LINKS_FILE = ['.sdods', 'issue-links.json'];
|
|
8
8
|
/** `sdods integrations test|notify|sync|links` — GitHub and Jira without the server or a database. */
|
|
9
9
|
export function register(program) {
|
|
10
10
|
const cmd = program
|
|
@@ -15,6 +15,7 @@ export function register(program) {
|
|
|
15
15
|
.description('Check credentials and reachability for each enabled provider')
|
|
16
16
|
.requiredOption('-p, --project <slug>', 'project slug')
|
|
17
17
|
.option('--provider <name>', 'only this provider (github|jira)')
|
|
18
|
+
.option('--create-labels', 'create configured GitHub labels the repository is missing')
|
|
18
19
|
.action(async (opts, c) => {
|
|
19
20
|
const ctx = createContext(c);
|
|
20
21
|
const entry = ctx.registry.entry(opts.project);
|
|
@@ -45,13 +46,14 @@ export function register(program) {
|
|
|
45
46
|
});
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
48
|
-
const res = await p.provider.test();
|
|
49
|
+
const res = await p.provider.test({ createMissingLabels: Boolean(opts.createLabels) });
|
|
49
50
|
rows.push({
|
|
50
51
|
provider: p.name,
|
|
51
52
|
enabled: true,
|
|
52
53
|
ok: res.ok,
|
|
53
54
|
detail: res.detail,
|
|
54
55
|
secrets: fmtSecrets(p.secrets),
|
|
56
|
+
...(ctx.opts.json && res.labels ? { labels: res.labels } : {}),
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
if (ctx.opts.json)
|
|
@@ -77,47 +79,16 @@ export function register(program) {
|
|
|
77
79
|
.option('--artifacts-dir <dir>', 'artifacts root', DEFAULT_ARTIFACTS_DIR)
|
|
78
80
|
.action(async (opts, c) => {
|
|
79
81
|
const ctx = createContext(c);
|
|
80
|
-
const {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
hint: 'Pass --artifacts-dir or check the run id (sdods report --last).',
|
|
86
|
-
exitCode: 2,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
const summary = buildRunSummaryFromFiles(runDir, {
|
|
82
|
+
const { summary, slug, actions: results, } = await notifyRun({
|
|
83
|
+
rootDir: ctx.rootDir,
|
|
84
|
+
registry: ctx.registry,
|
|
85
|
+
artifactsRoot: join(ctx.rootDir, opts.artifactsDir),
|
|
86
|
+
runId: opts.runId,
|
|
90
87
|
projectSlug: opts.project,
|
|
91
88
|
reportUrl: opts.reportUrl,
|
|
92
|
-
});
|
|
93
|
-
const slug = summary.run.projectSlug;
|
|
94
|
-
const entry = ctx.registry.entry(slug);
|
|
95
|
-
const providers = await getProviders(entry.config, {
|
|
96
|
-
only: opts.provider ? [opts.provider] : undefined,
|
|
97
|
-
projectRoot: entry.root,
|
|
98
|
-
});
|
|
99
|
-
const ictx = createIntegrationContext({
|
|
100
|
-
store: new FileIssueLinkStore(join(ctx.rootDir, ...LINKS_FILE)),
|
|
101
|
-
logger: new Logger('integrations'),
|
|
102
89
|
dryRun: Boolean(opts.dryRun),
|
|
103
|
-
|
|
90
|
+
only: opts.provider ? [opts.provider] : undefined,
|
|
104
91
|
});
|
|
105
|
-
const results = [];
|
|
106
|
-
for (const p of providers) {
|
|
107
|
-
if (!p.enabled)
|
|
108
|
-
continue;
|
|
109
|
-
if (p.initError) {
|
|
110
|
-
results.push({ provider: p.name, kind: 'skipped', detail: p.initError });
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const res = await p.provider.onRunFinished(summary, ictx);
|
|
115
|
-
results.push(...res.actions);
|
|
116
|
-
}
|
|
117
|
-
catch (e) {
|
|
118
|
-
results.push({ provider: p.name, kind: 'error', detail: e.message });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
92
|
if (ctx.opts.json)
|
|
122
93
|
return json({ run: summary.run, totals: summary.totals, actions: results });
|
|
123
94
|
const t = summary.totals;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { SdodsError } from '@sdods/core';
|
|
3
|
+
import { ALL_CAPABILITIES, DEFAULT_CAPABILITIES } from '@sdods/mcp';
|
|
3
4
|
import { createContext } from '../context.js';
|
|
4
5
|
import { json, ok, out } from '../ui.js';
|
|
5
6
|
const CLIENTS = ['claude', 'codex', 'cursor', 'vscode', 'windsurf'];
|
|
7
|
+
/**
|
|
8
|
+
* The default capability list, derived rather than restated. It used to be spelled out in three
|
|
9
|
+
* places in this file — the fallback, the flag default and the "did the user change it" sentinel —
|
|
10
|
+
* so adding a capability silently left the CLI handing out the old set.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_CAPS = DEFAULT_CAPABILITIES.join(',');
|
|
6
13
|
function capsOf(raw) {
|
|
7
14
|
if (!raw || raw === 'all')
|
|
8
|
-
return raw === 'all' ? 'all' : [
|
|
15
|
+
return raw === 'all' ? 'all' : [...DEFAULT_CAPABILITIES];
|
|
9
16
|
return raw
|
|
10
17
|
.split(',')
|
|
11
18
|
.map((s) => s.trim())
|
|
@@ -17,7 +24,7 @@ export function register(program) {
|
|
|
17
24
|
.description('Start the SDODS MCP server (stdio by default) or install client configuration')
|
|
18
25
|
.option('-p, --project <slug>', 'default project for tools and prompts')
|
|
19
26
|
.option('-e, --env <name>', 'default environment')
|
|
20
|
-
.option('--caps <list>',
|
|
27
|
+
.option('--caps <list>', `capabilities: ${ALL_CAPABILITIES.join(',')} or "all"`, DEFAULT_CAPS)
|
|
21
28
|
.option('--http', 'serve streamable HTTP instead of stdio')
|
|
22
29
|
.option('--port <n>', 'HTTP port', '4001')
|
|
23
30
|
.option('--host <host>', 'HTTP host', '127.0.0.1')
|
|
@@ -79,7 +86,7 @@ export function register(program) {
|
|
|
79
86
|
.option('--caps <list>')
|
|
80
87
|
.option('--http-url <url>', 'configure the HTTP transport instead of stdio')
|
|
81
88
|
.option('--token-placeholder <text>', 'placeholder written for the bearer token')
|
|
82
|
-
.option('--
|
|
89
|
+
.option('--with-playwright', 'also register the raw upstream Playwright MCP server (ungoverned; the sdods server already wraps it as browser_*)')
|
|
83
90
|
.option('--file', 'always write the config file instead of calling `claude mcp add` / `codex mcp add`')
|
|
84
91
|
.option('--print', 'print the snippet instead of writing files')
|
|
85
92
|
.action(async (client, opts, cmd) => {
|
|
@@ -96,11 +103,10 @@ export function register(program) {
|
|
|
96
103
|
const o = {
|
|
97
104
|
project: opts.project ?? parent.project,
|
|
98
105
|
env: opts.env ?? parent.env,
|
|
99
|
-
caps: opts.caps ??
|
|
100
|
-
(parent.caps !== 'core,analyze,run,data,schedules' ? parent.caps : undefined),
|
|
106
|
+
caps: opts.caps ?? (parent.caps !== DEFAULT_CAPS ? parent.caps : undefined),
|
|
101
107
|
httpUrl: opts.httpUrl,
|
|
102
108
|
tokenPlaceholder: opts.tokenPlaceholder,
|
|
103
|
-
withPlaywright: opts.
|
|
109
|
+
withPlaywright: opts.withPlaywright === true,
|
|
104
110
|
};
|
|
105
111
|
const s = snippets(o)[client];
|
|
106
112
|
if (opts.print || ctx.opts.json) {
|
|
@@ -117,7 +123,7 @@ export function register(program) {
|
|
|
117
123
|
const done = await registerViaClientCli(client, o, ctx.rootDir);
|
|
118
124
|
if (done) {
|
|
119
125
|
ok(`Registered the sdods MCP server with ${client} (${cliCommands(o)[client]})`);
|
|
120
|
-
if (o.withPlaywright && client === 'codex')
|
|
126
|
+
if (o.withPlaywright === true && client === 'codex')
|
|
121
127
|
out(pc.dim('Also registered the bundled Playwright MCP server as "playwright".'));
|
|
122
128
|
return;
|
|
123
129
|
}
|
|
@@ -131,7 +137,10 @@ export function register(program) {
|
|
|
131
137
|
/** `claude mcp add` (project scope) or `codex mcp add`; false when the CLI is unavailable. */
|
|
132
138
|
async function registerViaClientCli(client, o, cwd) {
|
|
133
139
|
const { execa } = await import('execa');
|
|
134
|
-
|
|
140
|
+
// `npx sdods` resolves only when the cwd happens to sit inside a checkout that has the binary in
|
|
141
|
+
// node_modules/.bin. From anywhere else npx goes to the registry, where no package named `sdods`
|
|
142
|
+
// exists, and the client reports CONNECTION_CLOSED. Name the package that owns the bin instead.
|
|
143
|
+
const stdio = ['npx', '-y', '@sdods/cli', 'mcp'];
|
|
135
144
|
if (o.project)
|
|
136
145
|
stdio.push('--project', o.project);
|
|
137
146
|
if (o.env)
|
|
@@ -168,7 +177,7 @@ async function registerViaClientCli(client, o, cwd) {
|
|
|
168
177
|
const ok1 = await run(addArgs);
|
|
169
178
|
if (!ok1)
|
|
170
179
|
return false;
|
|
171
|
-
if (o.withPlaywright
|
|
180
|
+
if (o.withPlaywright === true) {
|
|
172
181
|
const pw = client === 'claude'
|
|
173
182
|
? [
|
|
174
183
|
'mcp',
|