@sdods/cli 0.2.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/LICENSE +17 -0
- package/bin/sdods.js +19 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +219 -0
- package/dist/commands/analyze.d.ts +20 -0
- package/dist/commands/analyze.js +216 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +97 -0
- package/dist/commands/browsers.d.ts +17 -0
- package/dist/commands/browsers.js +103 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +79 -0
- package/dist/commands/coverage.d.ts +3 -0
- package/dist/commands/coverage.js +65 -0
- package/dist/commands/data.d.ts +7 -0
- package/dist/commands/data.js +188 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +241 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +323 -0
- package/dist/commands/env.d.ts +3 -0
- package/dist/commands/env.js +65 -0
- package/dist/commands/features.d.ts +3 -0
- package/dist/commands/features.js +57 -0
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/har.d.ts +3 -0
- package/dist/commands/har.js +124 -0
- package/dist/commands/heal.d.ts +4 -0
- package/dist/commands/heal.js +63 -0
- package/dist/commands/init.d.ts +38 -0
- package/dist/commands/init.js +363 -0
- package/dist/commands/insights.d.ts +7 -0
- package/dist/commands/insights.js +127 -0
- package/dist/commands/integrations.d.ts +14 -0
- package/dist/commands/integrations.js +306 -0
- package/dist/commands/lint.d.ts +3 -0
- package/dist/commands/lint.js +74 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +190 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.js +133 -0
- package/dist/commands/proposals.d.ts +3 -0
- package/dist/commands/proposals.js +106 -0
- package/dist/commands/record.d.ts +3 -0
- package/dist/commands/record.js +174 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +255 -0
- package/dist/commands/run.d.ts +41 -0
- package/dist/commands/run.js +369 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.js +337 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.js +38 -0
- package/dist/commands/steps.d.ts +8 -0
- package/dist/commands/steps.js +67 -0
- package/dist/commands/tokens.d.ts +3 -0
- package/dist/commands/tokens.js +110 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.js +104 -0
- package/dist/commands/upgrade.d.ts +14 -0
- package/dist/commands/upgrade.js +115 -0
- package/dist/commands/users.d.ts +3 -0
- package/dist/commands/users.js +183 -0
- package/dist/commands/watch.d.ts +7 -0
- package/dist/commands/watch.js +116 -0
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +83 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/program.d.ts +7 -0
- package/dist/program.js +90 -0
- package/dist/templates/project.d.ts +10 -0
- package/dist/templates/project.js +115 -0
- package/dist/ui.d.ts +16 -0
- package/dist/ui.js +67 -0
- package/package.json +61 -0
- package/templates/.claude/skills/sdods-record/SKILL.md +75 -0
- package/templates/.claude/skills/sdods-run/SKILL.md +54 -0
- package/templates/.env.example +62 -0
- package/templates/docker-compose.yml +40 -0
- package/templates/projects/demo-shop/.env.example +4 -0
- package/templates/projects/demo-shop/data/common/posts.yaml +11 -0
- package/templates/projects/demo-shop/data/common/products.json +8 -0
- package/templates/projects/demo-shop/data/common/users.csv +7 -0
- package/templates/projects/demo-shop/data/factories.ts +16 -0
- package/templates/projects/demo-shop/data/staging/users.csv +6 -0
- package/templates/projects/demo-shop/envs/local.yaml +17 -0
- package/templates/projects/demo-shop/envs/staging.yaml +23 -0
- package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--chromium/darwin/inventory.png +0 -0
- package/templates/projects/demo-shop/features/api/posts.feature +90 -0
- package/templates/projects/demo-shop/features/auth/data-driven-login.feature +17 -0
- package/templates/projects/demo-shop/features/auth/login.feature +32 -0
- package/templates/projects/demo-shop/features/auth/user-pool.feature +20 -0
- package/templates/projects/demo-shop/features/cart/cart.feature +33 -0
- package/templates/projects/demo-shop/features/hybrid/seed-then-verify.feature +28 -0
- package/templates/projects/demo-shop/features/inventory/inventory.feature +38 -0
- package/templates/projects/demo-shop/features/inventory/network-intercept.feature +29 -0
- package/templates/projects/demo-shop/features/inventory/visual.feature +10 -0
- package/templates/projects/demo-shop/har/staging/cart.har +1 -0
- package/templates/projects/demo-shop/har/staging/cart.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/login.har +1 -0
- package/templates/projects/demo-shop/har/staging/login.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/posts.api.har +2061 -0
- package/templates/projects/demo-shop/har/staging/posts.har.json +8 -0
- package/templates/projects/demo-shop/har/staging/products.har +1 -0
- package/templates/projects/demo-shop/har/staging/products.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/site.api.har +148 -0
- package/templates/projects/demo-shop/har/staging/site.har +1 -0
- package/templates/projects/demo-shop/har/staging/site.har.json +11 -0
- package/templates/projects/demo-shop/pages/CartPage.ts +20 -0
- package/templates/projects/demo-shop/pages/InventoryPage.ts +103 -0
- package/templates/projects/demo-shop/pages/LoginPage.ts +42 -0
- package/templates/projects/demo-shop/schemas/post.schema.json +13 -0
- package/templates/projects/demo-shop/sdods.project.yaml +185 -0
- package/templates/projects/demo-shop/steps/auth.ts +7 -0
- package/templates/projects/demo-shop/steps/fixtures.ts +39 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { SdodsError, DEFAULT_ARTIFACTS_DIR } from '@sdods/core';
|
|
6
|
+
import { createContext } from '../context.js';
|
|
7
|
+
import { json, out, table } from '../ui.js';
|
|
8
|
+
/** Recursively find `trace.zip` files (runner output) below a directory. */
|
|
9
|
+
export function findTraceZips(dir, depth = 8) {
|
|
10
|
+
const found = [];
|
|
11
|
+
const walk = (d, level) => {
|
|
12
|
+
if (level > depth || !existsSync(d))
|
|
13
|
+
return;
|
|
14
|
+
for (const name of readdirSync(d)) {
|
|
15
|
+
const p = join(d, name);
|
|
16
|
+
let st;
|
|
17
|
+
try {
|
|
18
|
+
st = statSync(p);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (st.isDirectory())
|
|
24
|
+
walk(p, level + 1);
|
|
25
|
+
else if (/^trace(-\d+)?\.zip$/.test(name))
|
|
26
|
+
found.push(p);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
walk(dir, 0);
|
|
30
|
+
return found.sort();
|
|
31
|
+
}
|
|
32
|
+
export function listRuns(artifactsDir) {
|
|
33
|
+
if (!existsSync(artifactsDir))
|
|
34
|
+
return [];
|
|
35
|
+
return readdirSync(artifactsDir)
|
|
36
|
+
.map((runId) => {
|
|
37
|
+
const dir = join(artifactsDir, runId);
|
|
38
|
+
try {
|
|
39
|
+
const st = statSync(dir);
|
|
40
|
+
return st.isDirectory() ? { runId, dir, mtime: st.mtimeMs } : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.filter((r) => r !== null)
|
|
47
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
48
|
+
}
|
|
49
|
+
export function register(program) {
|
|
50
|
+
program
|
|
51
|
+
.command('trace [zip]')
|
|
52
|
+
.description('Open traces: a zip file, the traces of a run (--run), or of the latest run (--last)')
|
|
53
|
+
.option('--last', 'use the most recent run (default when no zip or --run is given)')
|
|
54
|
+
.option('--run <id>', 'run id under the artifacts directory')
|
|
55
|
+
.option('--artifacts-dir <dir>', `artifacts directory (default: ${DEFAULT_ARTIFACTS_DIR})`)
|
|
56
|
+
.option('--list', 'list trace files instead of opening them')
|
|
57
|
+
.action(async (zip, opts, cmd) => {
|
|
58
|
+
const ctx = createContext(cmd);
|
|
59
|
+
let zips = [];
|
|
60
|
+
let source = '';
|
|
61
|
+
if (zip) {
|
|
62
|
+
const p = resolvePath(ctx.opts.cwd ?? process.cwd(), zip);
|
|
63
|
+
if (!existsSync(p))
|
|
64
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `No such file: ${p}`, { exitCode: 2 });
|
|
65
|
+
zips = statSync(p).isDirectory() ? findTraceZips(p) : [p];
|
|
66
|
+
source = p;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const artifactsDir = resolvePath(ctx.rootDir, opts.artifactsDir ?? process.env.SDODS_ARTIFACTS_DIR ?? DEFAULT_ARTIFACTS_DIR);
|
|
70
|
+
const runs = listRuns(artifactsDir);
|
|
71
|
+
const run = opts.run ? runs.find((r) => r.runId === opts.run) : runs[0];
|
|
72
|
+
if (!run) {
|
|
73
|
+
throw new SdodsError('CONFIG_NOT_FOUND', opts.run
|
|
74
|
+
? `Run ${opts.run} not found under ${artifactsDir}.`
|
|
75
|
+
: `No runs under ${artifactsDir}.`, {
|
|
76
|
+
hint: 'Run `sdods run …` first. Traces are kept on the first retry and on failure (trace: on-first-retry).',
|
|
77
|
+
exitCode: 2,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
zips = findTraceZips(run.dir);
|
|
81
|
+
source = run.dir;
|
|
82
|
+
}
|
|
83
|
+
if (!zips.length) {
|
|
84
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `No trace.zip under ${source}.`, {
|
|
85
|
+
hint: 'Traces are recorded on the first retry and for failures. Re-run with `--retries 1` or set trace: on in the project timeouts/use.',
|
|
86
|
+
exitCode: 2,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (opts.list || ctx.opts.json) {
|
|
90
|
+
if (ctx.opts.json)
|
|
91
|
+
return json({ source, traces: zips });
|
|
92
|
+
table(zips.map((z) => ({ trace: z })));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
out(pc.cyan(`Opening ${zips.length} trace(s) from ${source}`));
|
|
96
|
+
const res = await execa('npx', ['playwright', 'show-trace', ...zips], {
|
|
97
|
+
cwd: ctx.rootDir,
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
reject: false,
|
|
100
|
+
});
|
|
101
|
+
process.exitCode = res.exitCode ?? 0;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
export declare const SDODS_PACKAGES: readonly ["@sdods/cli", "@sdods/core", "@sdods/contracts", "@sdods/db", "@sdods/mcp", "@sdods/agents", "@sdods/integrations", "@sdods/server"];
|
|
3
|
+
export interface UpgradeRow {
|
|
4
|
+
package: string;
|
|
5
|
+
installed: string | null;
|
|
6
|
+
latest: string | null;
|
|
7
|
+
status: 'up-to-date' | 'outdated' | 'not-installed' | 'unavailable';
|
|
8
|
+
}
|
|
9
|
+
export declare function installedVersion(rootDir: string, pkg: string): string | null;
|
|
10
|
+
export declare function latestVersion(pkg: string, fetchImpl?: typeof fetch, registry?: string): Promise<string | null>;
|
|
11
|
+
export declare function checkUpgrades(rootDir: string, fetchImpl?: typeof fetch): Promise<UpgradeRow[]>;
|
|
12
|
+
export declare function register(program: Command): void;
|
|
13
|
+
export declare function detectPm(rootDir: string): 'bun' | 'pnpm' | 'npm';
|
|
14
|
+
//# sourceMappingURL=upgrade.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
import { VERSION } from '@sdods/core';
|
|
7
|
+
import { createContext } from '../context.js';
|
|
8
|
+
import { json, ok, out, table, warn } from '../ui.js';
|
|
9
|
+
export const SDODS_PACKAGES = [
|
|
10
|
+
'@sdods/cli',
|
|
11
|
+
'@sdods/core',
|
|
12
|
+
'@sdods/contracts',
|
|
13
|
+
'@sdods/db',
|
|
14
|
+
'@sdods/mcp',
|
|
15
|
+
'@sdods/agents',
|
|
16
|
+
'@sdods/integrations',
|
|
17
|
+
'@sdods/server',
|
|
18
|
+
];
|
|
19
|
+
export function installedVersion(rootDir, pkg) {
|
|
20
|
+
const file = join(rootDir, 'node_modules', pkg, 'package.json');
|
|
21
|
+
if (!existsSync(file))
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const v = JSON.parse(readFileSync(file, 'utf8')).version;
|
|
25
|
+
return v ?? null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function latestVersion(pkg, fetchImpl = fetch, registry = process.env.NPM_CONFIG_REGISTRY ?? 'https://registry.npmjs.org') {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetchImpl(`${registry.replace(/\/$/, '')}/${pkg}/latest`, {
|
|
34
|
+
signal: AbortSignal.timeout(8000),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
return null;
|
|
38
|
+
const body = (await res.json());
|
|
39
|
+
return body.version ?? null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function checkUpgrades(rootDir, fetchImpl) {
|
|
46
|
+
const rows = await Promise.all(SDODS_PACKAGES.map(async (pkg) => {
|
|
47
|
+
const installed = installedVersion(rootDir, pkg) ?? (pkg === '@sdods/cli' ? VERSION : null);
|
|
48
|
+
const latest = await latestVersion(pkg, fetchImpl);
|
|
49
|
+
let status;
|
|
50
|
+
if (!latest)
|
|
51
|
+
status = 'unavailable';
|
|
52
|
+
else if (!installed)
|
|
53
|
+
status = 'not-installed';
|
|
54
|
+
else
|
|
55
|
+
status =
|
|
56
|
+
semver.valid(installed) && semver.lt(installed, latest) ? 'outdated' : 'up-to-date';
|
|
57
|
+
return { package: pkg, installed, latest, status };
|
|
58
|
+
}));
|
|
59
|
+
return rows;
|
|
60
|
+
}
|
|
61
|
+
export function register(program) {
|
|
62
|
+
program
|
|
63
|
+
.command('upgrade')
|
|
64
|
+
.description('Check the npm registry for newer @sdods/* packages')
|
|
65
|
+
.option('--apply', 'install the latest versions with the package manager')
|
|
66
|
+
.option('--pm <manager>', 'bun | pnpm | npm (default: detected from lockfile)')
|
|
67
|
+
.action(async (opts, cmd) => {
|
|
68
|
+
const ctx = createContext(cmd);
|
|
69
|
+
const rows = await checkUpgrades(ctx.rootDir);
|
|
70
|
+
if (ctx.opts.json)
|
|
71
|
+
return json(rows);
|
|
72
|
+
table(rows.map((r) => ({
|
|
73
|
+
package: r.package,
|
|
74
|
+
installed: r.installed ?? pc.dim('—'),
|
|
75
|
+
latest: r.latest ?? pc.dim('?'),
|
|
76
|
+
status: r.status === 'outdated'
|
|
77
|
+
? pc.yellow(r.status)
|
|
78
|
+
: r.status === 'up-to-date'
|
|
79
|
+
? pc.green(r.status)
|
|
80
|
+
: pc.dim(r.status),
|
|
81
|
+
})));
|
|
82
|
+
const outdated = rows.filter((r) => r.status === 'outdated');
|
|
83
|
+
if (!outdated.length) {
|
|
84
|
+
if (rows.every((r) => r.status === 'unavailable'))
|
|
85
|
+
warn('Registry unreachable (offline?) — nothing compared.');
|
|
86
|
+
else
|
|
87
|
+
ok('Everything installed is up to date.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!opts.apply) {
|
|
91
|
+
out(pc.dim(`Run \`sdods upgrade --apply\` to install ${outdated.map((r) => r.package).join(', ')}.`));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const pm = opts.pm ?? detectPm(ctx.rootDir);
|
|
95
|
+
const specs = outdated.map((r) => `${r.package}@${r.latest}`);
|
|
96
|
+
const args = pm === 'bun'
|
|
97
|
+
? ['add', ...specs]
|
|
98
|
+
: pm === 'pnpm'
|
|
99
|
+
? ['add', ...specs]
|
|
100
|
+
: ['install', ...specs];
|
|
101
|
+
const res = await execa(pm, args, { cwd: ctx.rootDir, stdio: 'inherit', reject: false });
|
|
102
|
+
if (res.exitCode !== 0)
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
else
|
|
105
|
+
ok(`Upgraded ${specs.length} package(s) with ${pm}.`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function detectPm(rootDir) {
|
|
109
|
+
if (existsSync(join(rootDir, 'bun.lock')) || existsSync(join(rootDir, 'bun.lockb')))
|
|
110
|
+
return 'bun';
|
|
111
|
+
if (existsSync(join(rootDir, 'pnpm-lock.yaml')))
|
|
112
|
+
return 'pnpm';
|
|
113
|
+
return 'npm';
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=upgrade.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { SdodsError } from '@sdods/core';
|
|
2
|
+
import { createContext } from '../context.js';
|
|
3
|
+
import { json, ok, table } from '../ui.js';
|
|
4
|
+
async function openDb() {
|
|
5
|
+
const db = await import('@sdods/db');
|
|
6
|
+
return db.openDb();
|
|
7
|
+
}
|
|
8
|
+
export function register(program) {
|
|
9
|
+
const users = program
|
|
10
|
+
.command('users')
|
|
11
|
+
.description('Manage web UI users (stored in the platform database)');
|
|
12
|
+
users
|
|
13
|
+
.command('create')
|
|
14
|
+
.description('Create a user; --admin makes a platform admin and organization owner')
|
|
15
|
+
.requiredOption('--username <name>', 'login name')
|
|
16
|
+
.requiredOption('--password <password>', 'password (min 8 chars)')
|
|
17
|
+
.option('--admin', 'platform admin + owner of every organization without an owner')
|
|
18
|
+
.option('--role <role>', 'viewer | editor | admin', 'viewer')
|
|
19
|
+
.option('--email <email>', 'email')
|
|
20
|
+
.option('--org-owner', 'grant organization owner on orgs without an owner')
|
|
21
|
+
.action(async (opts, cmd) => {
|
|
22
|
+
const ctx = createContext(cmd);
|
|
23
|
+
if (String(opts.password).length < 8)
|
|
24
|
+
throw new SdodsError('CONFIG_INVALID', 'Password must be at least 8 characters.', {
|
|
25
|
+
exitCode: 2,
|
|
26
|
+
});
|
|
27
|
+
const adb = await openDb();
|
|
28
|
+
try {
|
|
29
|
+
const db = await import('@sdods/db');
|
|
30
|
+
const { hashPassword } = await import('@sdods/server');
|
|
31
|
+
if (await db.getUserByUsername(adb.db, opts.username))
|
|
32
|
+
throw new SdodsError('CONFIG_INVALID', `User ${opts.username} already exists.`, {
|
|
33
|
+
exitCode: 2,
|
|
34
|
+
});
|
|
35
|
+
const role = opts.admin ? 'admin' : opts.role;
|
|
36
|
+
if (!['viewer', 'editor', 'admin'].includes(role))
|
|
37
|
+
throw new SdodsError('CONFIG_INVALID', `Unknown role ${role}.`, { exitCode: 2 });
|
|
38
|
+
const id = await db.createUser(adb.db, adb.driver, {
|
|
39
|
+
username: opts.username,
|
|
40
|
+
passwordHash: await hashPassword(opts.password),
|
|
41
|
+
role,
|
|
42
|
+
email: opts.email ?? null,
|
|
43
|
+
});
|
|
44
|
+
// make sure the hierarchy exists before granting ownership
|
|
45
|
+
const { HierarchyService } = await import('@sdods/server');
|
|
46
|
+
const h = new HierarchyService(adb);
|
|
47
|
+
await h.sync(ctx.registry).catch(() => undefined);
|
|
48
|
+
const ownerOf = opts.admin || opts.orgOwner ? await h.bootstrapOwner(id) : [];
|
|
49
|
+
await db.audit(adb.db, {
|
|
50
|
+
actorType: 'cli',
|
|
51
|
+
action: 'user.create',
|
|
52
|
+
targetType: 'user',
|
|
53
|
+
targetId: id,
|
|
54
|
+
details: { username: opts.username, role },
|
|
55
|
+
});
|
|
56
|
+
if (ctx.opts.json)
|
|
57
|
+
return json({ id, username: opts.username, role, ownerOf });
|
|
58
|
+
ok(`Created ${role} user ${opts.username}${ownerOf.length ? ` (owner of ${ownerOf.join(', ')})` : ''}`);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await adb.close();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
users
|
|
65
|
+
.command('list')
|
|
66
|
+
.description('List users')
|
|
67
|
+
.action(async (_opts, cmd) => {
|
|
68
|
+
const ctx = createContext(cmd);
|
|
69
|
+
const adb = await openDb();
|
|
70
|
+
try {
|
|
71
|
+
const db = await import('@sdods/db');
|
|
72
|
+
const rows = (await db.listUsers(adb.db)).map((u) => ({
|
|
73
|
+
id: u.id,
|
|
74
|
+
username: u.username,
|
|
75
|
+
role: u.role,
|
|
76
|
+
active: u.active,
|
|
77
|
+
email: u.email ?? '',
|
|
78
|
+
lastLogin: u.lastLoginAt ?? '',
|
|
79
|
+
}));
|
|
80
|
+
if (ctx.opts.json)
|
|
81
|
+
return json(rows);
|
|
82
|
+
table(rows);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await adb.close();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
users
|
|
89
|
+
.command('set-role <username> <role>')
|
|
90
|
+
.description('Change a platform role (viewer | editor | admin)')
|
|
91
|
+
.action(async (username, role, cmd) => {
|
|
92
|
+
const ctx = createContext(cmd);
|
|
93
|
+
const adb = await openDb();
|
|
94
|
+
try {
|
|
95
|
+
const db = await import('@sdods/db');
|
|
96
|
+
const u = await db.getUserByUsername(adb.db, username);
|
|
97
|
+
if (!u)
|
|
98
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `User ${username} not found.`, {
|
|
99
|
+
exitCode: 2,
|
|
100
|
+
});
|
|
101
|
+
await db.updateUser(adb.db, adb.driver, u.id, {
|
|
102
|
+
role: role,
|
|
103
|
+
});
|
|
104
|
+
if (ctx.opts.json)
|
|
105
|
+
return json({ id: u.id, username, role });
|
|
106
|
+
ok(`${username} is now ${role}`);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await adb.close();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
users
|
|
113
|
+
.command('deactivate <username>')
|
|
114
|
+
.description('Deactivate a user (sessions and tokens stop working)')
|
|
115
|
+
.action(async (username, cmd) => {
|
|
116
|
+
const ctx = createContext(cmd);
|
|
117
|
+
const adb = await openDb();
|
|
118
|
+
try {
|
|
119
|
+
const db = await import('@sdods/db');
|
|
120
|
+
const u = await db.getUserByUsername(adb.db, username);
|
|
121
|
+
if (!u)
|
|
122
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `User ${username} not found.`, {
|
|
123
|
+
exitCode: 2,
|
|
124
|
+
});
|
|
125
|
+
await db.updateUser(adb.db, adb.driver, u.id, { active: false });
|
|
126
|
+
if (ctx.opts.json)
|
|
127
|
+
return json({ id: u.id, username, active: false });
|
|
128
|
+
ok(`${username} deactivated`);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await adb.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
users
|
|
135
|
+
.command('grant <username>')
|
|
136
|
+
.description('Grant a membership role: --org <slug> --role owner|admin|member, or --workspace <slug> --role admin|editor|viewer')
|
|
137
|
+
.option('--org <slug>', 'organization slug')
|
|
138
|
+
.option('--workspace <slug>', 'workspace slug')
|
|
139
|
+
.requiredOption('--role <role>', 'role')
|
|
140
|
+
.action(async (username, opts, cmd) => {
|
|
141
|
+
const ctx = createContext(cmd);
|
|
142
|
+
const adb = await openDb();
|
|
143
|
+
try {
|
|
144
|
+
const db = await import('@sdods/db');
|
|
145
|
+
const { HierarchyService } = await import('@sdods/server');
|
|
146
|
+
const h = new HierarchyService(adb);
|
|
147
|
+
await h.sync(ctx.registry);
|
|
148
|
+
const u = await db.getUserByUsername(adb.db, username);
|
|
149
|
+
if (!u)
|
|
150
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `User ${username} not found.`, {
|
|
151
|
+
exitCode: 2,
|
|
152
|
+
});
|
|
153
|
+
if (opts.workspace) {
|
|
154
|
+
const ws = (await h.workspaces()).find((w) => w.slug === opts.workspace);
|
|
155
|
+
if (!ws)
|
|
156
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `Workspace ${opts.workspace} not found.`, {
|
|
157
|
+
exitCode: 2,
|
|
158
|
+
});
|
|
159
|
+
await h.setWorkspaceRole(ws.id, u.id, opts.role);
|
|
160
|
+
}
|
|
161
|
+
else if (opts.org) {
|
|
162
|
+
const org = (await h.organizations()).find((o) => o.slug === opts.org);
|
|
163
|
+
if (!org)
|
|
164
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `Organization ${opts.org} not found.`, {
|
|
165
|
+
exitCode: 2,
|
|
166
|
+
});
|
|
167
|
+
await h.setOrgRole(org.id, u.id, opts.role);
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
throw new SdodsError('CONFIG_INVALID', 'Pass --org <slug> or --workspace <slug>.', {
|
|
171
|
+
exitCode: 2,
|
|
172
|
+
});
|
|
173
|
+
const list = await h.workspacesForUser(u.id, u.role);
|
|
174
|
+
if (ctx.opts.json)
|
|
175
|
+
return json(list);
|
|
176
|
+
ok(`${username}: ${list.map((w) => `${w.slug}=${w.role}`).join(', ') || '(no workspace access)'}`);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await adb.close();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=users.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Spec generation runs in watch mode while the interactive UI
|
|
4
|
+
* re-runs them. Both children share the same SDODS_* environment that `sdods run` uses.
|
|
5
|
+
*/
|
|
6
|
+
export declare function register(program: Command): void;
|
|
7
|
+
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { newRunId } from '@sdods/contracts';
|
|
5
|
+
import { SdodsError, listGeneratedProjects, normalizeTagExpr } from '@sdods/core';
|
|
6
|
+
import { createContext } from '../context.js';
|
|
7
|
+
import { collect, out } from '../ui.js';
|
|
8
|
+
/**
|
|
9
|
+
* Spec generation runs in watch mode while the interactive UI
|
|
10
|
+
* re-runs them. Both children share the same SDODS_* environment that `sdods run` uses.
|
|
11
|
+
*/
|
|
12
|
+
export function register(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('watch')
|
|
15
|
+
.description('regenerate specs on change and re-run them in interactive UI mode (hot reload)')
|
|
16
|
+
.option('-p, --project <slug>', 'project slug (default: the only project)')
|
|
17
|
+
.option('-e, --env <name>', 'environment name')
|
|
18
|
+
.option('-t, --tags <expr>', 'Cucumber tag expression')
|
|
19
|
+
.option('-l, --layer <layer>', 'ui | api | hybrid | recorded (repeatable)', collect, [])
|
|
20
|
+
.option('-b, --browser <name>', 'browser (repeatable)', collect, [])
|
|
21
|
+
.option('--headed', 'headed browser (only meaningful with --no-ui)')
|
|
22
|
+
.option('--no-ui', 'run the suite in watch mode instead of the interactive UI')
|
|
23
|
+
.action(async (flags, cmd) => {
|
|
24
|
+
const ctx = createContext(cmd);
|
|
25
|
+
const entry = ctx.registry.pick(flags.project);
|
|
26
|
+
const cfg = ctx.registry.resolve(entry.slug, flags.env, { runId: newRunId() });
|
|
27
|
+
const tags = normalizeTagExpr(flags.tags);
|
|
28
|
+
for (const l of flags.layer) {
|
|
29
|
+
if (!entry.config.layers.includes(l)) {
|
|
30
|
+
throw new SdodsError('CONFIG_INVALID', `Layer "${l}" is not enabled for ${entry.slug}.`, {
|
|
31
|
+
exitCode: 2,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const selection = {
|
|
36
|
+
project: entry.slug,
|
|
37
|
+
env: cfg.env.name,
|
|
38
|
+
layers: flags.layer.length ? flags.layer : undefined,
|
|
39
|
+
browsers: flags.browser.length ? flags.browser : undefined,
|
|
40
|
+
tags,
|
|
41
|
+
runId: cfg.runtime.runId,
|
|
42
|
+
};
|
|
43
|
+
const childEnv = {
|
|
44
|
+
...process.env,
|
|
45
|
+
SDODS_ROOT: ctx.rootDir,
|
|
46
|
+
SDODS_PROJECT: entry.slug,
|
|
47
|
+
SDODS_ENV: cfg.env.name,
|
|
48
|
+
SDODS_TAGS: tags ?? '',
|
|
49
|
+
SDODS_LAYERS: selection.layers?.join(',') ?? '',
|
|
50
|
+
SDODS_BROWSERS: selection.browsers?.join(',') ?? '',
|
|
51
|
+
SDODS_RUN_ID: cfg.runtime.runId,
|
|
52
|
+
SDODS_REPORTER_MODE: 'quiet',
|
|
53
|
+
};
|
|
54
|
+
const configPath = join(ctx.rootDir, 'sdods.runner.config.ts');
|
|
55
|
+
const names = listGeneratedProjects(ctx.registry, selection).map((p) => p.name);
|
|
56
|
+
if (!names.length) {
|
|
57
|
+
throw new SdodsError('CONFIG_INVALID', 'Nothing to watch: no run targets match the selection.', {
|
|
58
|
+
hint: 'Check --layer/--browser against the project yaml.',
|
|
59
|
+
exitCode: 2,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
out(pc.cyan(`Watching ${entry.slug} (${cfg.env.name}) → ${names.join(', ')}`));
|
|
63
|
+
const children = [];
|
|
64
|
+
const stop = () => {
|
|
65
|
+
for (const c of children)
|
|
66
|
+
c.kill('SIGTERM');
|
|
67
|
+
};
|
|
68
|
+
process.on('SIGINT', stop);
|
|
69
|
+
process.on('SIGTERM', stop);
|
|
70
|
+
// initial generation so UI mode has specs to show
|
|
71
|
+
const gen = await execa('npx', ['bddgen', '-c', configPath], {
|
|
72
|
+
cwd: ctx.rootDir,
|
|
73
|
+
env: childEnv,
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
reject: false,
|
|
76
|
+
});
|
|
77
|
+
if (gen.exitCode !== 0) {
|
|
78
|
+
throw new SdodsError('RUN_FAILED', 'bddgen failed; fix the errors above and retry.', {
|
|
79
|
+
exitCode: 1,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const watcher = execa('npx', ['bddgen', '-c', configPath, '--watch'], {
|
|
83
|
+
cwd: ctx.rootDir,
|
|
84
|
+
env: childEnv,
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
reject: false,
|
|
87
|
+
});
|
|
88
|
+
children.push(watcher);
|
|
89
|
+
const pwArgs = [
|
|
90
|
+
'playwright',
|
|
91
|
+
'test',
|
|
92
|
+
'-c',
|
|
93
|
+
configPath,
|
|
94
|
+
...names.flatMap((n) => ['--project', n]),
|
|
95
|
+
];
|
|
96
|
+
if (flags.noUi) {
|
|
97
|
+
pwArgs.push('--watch');
|
|
98
|
+
if (flags.headed)
|
|
99
|
+
pwArgs.push('--headed');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
pwArgs.push('--ui');
|
|
103
|
+
}
|
|
104
|
+
const pw = execa('npx', pwArgs, {
|
|
105
|
+
cwd: ctx.rootDir,
|
|
106
|
+
env: childEnv,
|
|
107
|
+
stdio: 'inherit',
|
|
108
|
+
reject: false,
|
|
109
|
+
});
|
|
110
|
+
children.push(pw);
|
|
111
|
+
const res = await pw;
|
|
112
|
+
stop();
|
|
113
|
+
process.exitCode = res.exitCode ?? 0;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Hierarchy commands: organization → workspaces → projects → modules → processes.
|
|
4
|
+
* File-based (no database needed). Membership roles are shown by `sdods users` / the web UI
|
|
5
|
+
* once a database is configured.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerWorkspaceCommands(program: Command): void;
|
|
8
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { WORKSPACE_FILE } from '@sdods/core';
|
|
3
|
+
import { createContext } from '../context.js';
|
|
4
|
+
import { json, out, table } from '../ui.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hierarchy commands: organization → workspaces → projects → modules → processes.
|
|
7
|
+
* File-based (no database needed). Membership roles are shown by `sdods users` / the web UI
|
|
8
|
+
* once a database is configured.
|
|
9
|
+
*/
|
|
10
|
+
export function registerWorkspaceCommands(program) {
|
|
11
|
+
const ws = program
|
|
12
|
+
.command('workspace')
|
|
13
|
+
.description('Organization, workspaces and their projects');
|
|
14
|
+
ws.command('tree')
|
|
15
|
+
.description(`Show the hierarchy from ${WORKSPACE_FILE} and projects/*`)
|
|
16
|
+
.action((_opts, cmd) => {
|
|
17
|
+
const ctx = createContext(cmd);
|
|
18
|
+
const tree = ctx.registry.tree();
|
|
19
|
+
if (ctx.opts.json)
|
|
20
|
+
return json(tree);
|
|
21
|
+
out(`${pc.bold('org')} ${tree.organization.name} ${pc.dim(`(${tree.organization.slug})`)}`);
|
|
22
|
+
for (const w of tree.workspaces) {
|
|
23
|
+
out(` ${pc.cyan('workspace')} ${w.workspace.name} ${pc.dim(`(${w.workspace.slug})`)}`);
|
|
24
|
+
if (w.projects.length === 0)
|
|
25
|
+
out(pc.dim(' (no projects)'));
|
|
26
|
+
for (const p of w.projects) {
|
|
27
|
+
out(` ${pc.green('project')} ${p.name} ${pc.dim(`(${p.slug})`)}`);
|
|
28
|
+
for (const m of p.modules) {
|
|
29
|
+
out(` ${pc.magenta('module')} ${m.name} ${pc.dim(`[${m.testingTypes.join(', ')}]`)}${m.tags.length ? pc.dim(` tags ${m.tags.join(' ')}`) : ''}`);
|
|
30
|
+
}
|
|
31
|
+
if (p.processes.length)
|
|
32
|
+
out(` ${pc.yellow('processes')} ${p.processes.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!ctx.registry.workspaceFilePath) {
|
|
36
|
+
out(pc.dim(`\nNo ${WORKSPACE_FILE} found; showing the built-in default organization/workspace.`));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
ws.command('list')
|
|
40
|
+
.description('List workspaces of the organization with project counts')
|
|
41
|
+
.action((_opts, cmd) => {
|
|
42
|
+
const ctx = createContext(cmd);
|
|
43
|
+
const rows = ctx.registry.workspaces().map((w) => ({
|
|
44
|
+
slug: w.slug,
|
|
45
|
+
name: w.name,
|
|
46
|
+
default: w.slug === ctx.registry.workspaceFile.defaultWorkspace ? '*' : '',
|
|
47
|
+
projects: ctx.registry
|
|
48
|
+
.projectsInWorkspace(w.slug)
|
|
49
|
+
.map((p) => p.slug)
|
|
50
|
+
.join(', '),
|
|
51
|
+
}));
|
|
52
|
+
if (ctx.opts.json)
|
|
53
|
+
return json(rows);
|
|
54
|
+
table(rows);
|
|
55
|
+
});
|
|
56
|
+
const processes = program
|
|
57
|
+
.command('processes')
|
|
58
|
+
.description('Named run recipes (pr-check, nightly-regression, release-gate …)');
|
|
59
|
+
processes
|
|
60
|
+
.command('list')
|
|
61
|
+
.description('List processes visible to a project (workspace defaults + project overrides)')
|
|
62
|
+
.requiredOption('-p, --project <slug>', 'project slug')
|
|
63
|
+
.action((opts, cmd) => {
|
|
64
|
+
const ctx = createContext(cmd);
|
|
65
|
+
const rows = ctx.registry.processesOf(opts.project).map((p) => ({
|
|
66
|
+
name: p.name,
|
|
67
|
+
trigger: p.trigger,
|
|
68
|
+
tags: p.tags ?? '',
|
|
69
|
+
layers: p.layers?.join(',') ?? '(all)',
|
|
70
|
+
browsers: p.browsers?.join(',') ?? '(project)',
|
|
71
|
+
env: p.env ?? '(default)',
|
|
72
|
+
gates: Object.entries(p.gates)
|
|
73
|
+
.filter(([, v]) => v !== undefined && v !== false)
|
|
74
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
75
|
+
.join(' '),
|
|
76
|
+
schedule: p.schedule ?? '',
|
|
77
|
+
}));
|
|
78
|
+
if (ctx.opts.json)
|
|
79
|
+
return json(ctx.registry.processesOf(opts.project));
|
|
80
|
+
table(rows);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { ProjectRegistry } from '@sdods/core';
|
|
3
|
+
export interface GlobalOptions {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
quiet?: boolean;
|
|
6
|
+
verbose?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
color?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CliContext {
|
|
11
|
+
rootDir: string;
|
|
12
|
+
registry: ProjectRegistry;
|
|
13
|
+
opts: GlobalOptions;
|
|
14
|
+
}
|
|
15
|
+
export declare function globalOptions(cmd: Command): GlobalOptions;
|
|
16
|
+
export declare function createContext(cmd: Command): CliContext;
|
|
17
|
+
//# sourceMappingURL=context.d.ts.map
|