@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.
Files changed (124) hide show
  1. package/LICENSE +17 -0
  2. package/bin/sdods.js +19 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/bin.d.ts +2 -0
  5. package/dist/bin.js +11 -0
  6. package/dist/commands/agent.d.ts +3 -0
  7. package/dist/commands/agent.js +219 -0
  8. package/dist/commands/analyze.d.ts +20 -0
  9. package/dist/commands/analyze.js +216 -0
  10. package/dist/commands/auth.d.ts +7 -0
  11. package/dist/commands/auth.js +97 -0
  12. package/dist/commands/browsers.d.ts +17 -0
  13. package/dist/commands/browsers.js +103 -0
  14. package/dist/commands/completion.d.ts +17 -0
  15. package/dist/commands/completion.js +143 -0
  16. package/dist/commands/config.d.ts +3 -0
  17. package/dist/commands/config.js +79 -0
  18. package/dist/commands/coverage.d.ts +3 -0
  19. package/dist/commands/coverage.js +65 -0
  20. package/dist/commands/data.d.ts +7 -0
  21. package/dist/commands/data.js +188 -0
  22. package/dist/commands/db.d.ts +3 -0
  23. package/dist/commands/db.js +241 -0
  24. package/dist/commands/doctor.d.ts +3 -0
  25. package/dist/commands/doctor.js +323 -0
  26. package/dist/commands/env.d.ts +3 -0
  27. package/dist/commands/env.js +65 -0
  28. package/dist/commands/features.d.ts +3 -0
  29. package/dist/commands/features.js +57 -0
  30. package/dist/commands/feedback.d.ts +7 -0
  31. package/dist/commands/feedback.js +96 -0
  32. package/dist/commands/har.d.ts +3 -0
  33. package/dist/commands/har.js +124 -0
  34. package/dist/commands/heal.d.ts +4 -0
  35. package/dist/commands/heal.js +63 -0
  36. package/dist/commands/init.d.ts +38 -0
  37. package/dist/commands/init.js +363 -0
  38. package/dist/commands/insights.d.ts +7 -0
  39. package/dist/commands/insights.js +127 -0
  40. package/dist/commands/integrations.d.ts +14 -0
  41. package/dist/commands/integrations.js +306 -0
  42. package/dist/commands/lint.d.ts +3 -0
  43. package/dist/commands/lint.js +74 -0
  44. package/dist/commands/mcp.d.ts +3 -0
  45. package/dist/commands/mcp.js +190 -0
  46. package/dist/commands/project.d.ts +3 -0
  47. package/dist/commands/project.js +133 -0
  48. package/dist/commands/proposals.d.ts +3 -0
  49. package/dist/commands/proposals.js +106 -0
  50. package/dist/commands/record.d.ts +3 -0
  51. package/dist/commands/record.js +174 -0
  52. package/dist/commands/report.d.ts +3 -0
  53. package/dist/commands/report.js +255 -0
  54. package/dist/commands/run.d.ts +41 -0
  55. package/dist/commands/run.js +369 -0
  56. package/dist/commands/schedule.d.ts +3 -0
  57. package/dist/commands/schedule.js +337 -0
  58. package/dist/commands/serve.d.ts +3 -0
  59. package/dist/commands/serve.js +38 -0
  60. package/dist/commands/steps.d.ts +8 -0
  61. package/dist/commands/steps.js +67 -0
  62. package/dist/commands/tokens.d.ts +3 -0
  63. package/dist/commands/tokens.js +110 -0
  64. package/dist/commands/trace.d.ts +10 -0
  65. package/dist/commands/trace.js +104 -0
  66. package/dist/commands/upgrade.d.ts +14 -0
  67. package/dist/commands/upgrade.js +115 -0
  68. package/dist/commands/users.d.ts +3 -0
  69. package/dist/commands/users.js +183 -0
  70. package/dist/commands/watch.d.ts +7 -0
  71. package/dist/commands/watch.js +116 -0
  72. package/dist/commands/workspace.d.ts +8 -0
  73. package/dist/commands/workspace.js +83 -0
  74. package/dist/context.d.ts +17 -0
  75. package/dist/context.js +22 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.js +3 -0
  78. package/dist/program.d.ts +7 -0
  79. package/dist/program.js +90 -0
  80. package/dist/templates/project.d.ts +10 -0
  81. package/dist/templates/project.js +115 -0
  82. package/dist/ui.d.ts +16 -0
  83. package/dist/ui.js +67 -0
  84. package/package.json +61 -0
  85. package/templates/.claude/skills/sdods-record/SKILL.md +75 -0
  86. package/templates/.claude/skills/sdods-run/SKILL.md +54 -0
  87. package/templates/.env.example +62 -0
  88. package/templates/docker-compose.yml +40 -0
  89. package/templates/projects/demo-shop/.env.example +4 -0
  90. package/templates/projects/demo-shop/data/common/posts.yaml +11 -0
  91. package/templates/projects/demo-shop/data/common/products.json +8 -0
  92. package/templates/projects/demo-shop/data/common/users.csv +7 -0
  93. package/templates/projects/demo-shop/data/factories.ts +16 -0
  94. package/templates/projects/demo-shop/data/staging/users.csv +6 -0
  95. package/templates/projects/demo-shop/envs/local.yaml +17 -0
  96. package/templates/projects/demo-shop/envs/staging.yaml +23 -0
  97. package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--chromium/darwin/inventory.png +0 -0
  98. package/templates/projects/demo-shop/features/api/posts.feature +90 -0
  99. package/templates/projects/demo-shop/features/auth/data-driven-login.feature +17 -0
  100. package/templates/projects/demo-shop/features/auth/login.feature +32 -0
  101. package/templates/projects/demo-shop/features/auth/user-pool.feature +20 -0
  102. package/templates/projects/demo-shop/features/cart/cart.feature +33 -0
  103. package/templates/projects/demo-shop/features/hybrid/seed-then-verify.feature +28 -0
  104. package/templates/projects/demo-shop/features/inventory/inventory.feature +38 -0
  105. package/templates/projects/demo-shop/features/inventory/network-intercept.feature +29 -0
  106. package/templates/projects/demo-shop/features/inventory/visual.feature +10 -0
  107. package/templates/projects/demo-shop/har/staging/cart.har +1 -0
  108. package/templates/projects/demo-shop/har/staging/cart.har.json +11 -0
  109. package/templates/projects/demo-shop/har/staging/login.har +1 -0
  110. package/templates/projects/demo-shop/har/staging/login.har.json +11 -0
  111. package/templates/projects/demo-shop/har/staging/posts.api.har +2061 -0
  112. package/templates/projects/demo-shop/har/staging/posts.har.json +8 -0
  113. package/templates/projects/demo-shop/har/staging/products.har +1 -0
  114. package/templates/projects/demo-shop/har/staging/products.har.json +11 -0
  115. package/templates/projects/demo-shop/har/staging/site.api.har +148 -0
  116. package/templates/projects/demo-shop/har/staging/site.har +1 -0
  117. package/templates/projects/demo-shop/har/staging/site.har.json +11 -0
  118. package/templates/projects/demo-shop/pages/CartPage.ts +20 -0
  119. package/templates/projects/demo-shop/pages/InventoryPage.ts +103 -0
  120. package/templates/projects/demo-shop/pages/LoginPage.ts +42 -0
  121. package/templates/projects/demo-shop/schemas/post.schema.json +13 -0
  122. package/templates/projects/demo-shop/sdods.project.yaml +185 -0
  123. package/templates/projects/demo-shop/steps/auth.ts +7 -0
  124. package/templates/projects/demo-shop/steps/fixtures.ts +39 -0
@@ -0,0 +1,103 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { execa } from 'execa';
4
+ import pc from 'picocolors';
5
+ import { BrowserSchema } from '@sdods/contracts';
6
+ import { SdodsError } from '@sdods/core';
7
+ import { createContext } from '../context.js';
8
+ import { collect, json, ok, table } from '../ui.js';
9
+ const ENGINE_OF = {
10
+ chromium: 'chromium',
11
+ firefox: 'firefox',
12
+ webkit: 'webkit',
13
+ 'mobile-chrome': 'chromium',
14
+ 'mobile-safari': 'webkit',
15
+ };
16
+ export async function browserStatuses(names = Object.keys(ENGINE_OF)) {
17
+ const require = createRequire(import.meta.url);
18
+ let pw;
19
+ let version = 'unknown';
20
+ try {
21
+ pw = await import('playwright-core');
22
+ version = require('playwright-core/package.json').version;
23
+ }
24
+ catch {
25
+ return names.map((name) => ({
26
+ name,
27
+ engine: ENGINE_OF[name] ?? 'chromium',
28
+ installed: false,
29
+ executable: null,
30
+ playwrightVersion: version,
31
+ }));
32
+ }
33
+ return names.map((name) => {
34
+ const engine = ENGINE_OF[name] ?? 'chromium';
35
+ let executable = null;
36
+ try {
37
+ executable = pw[engine].executablePath();
38
+ }
39
+ catch {
40
+ executable = null;
41
+ }
42
+ return {
43
+ name,
44
+ engine,
45
+ installed: Boolean(executable && existsSync(executable)),
46
+ executable,
47
+ playwrightVersion: version,
48
+ };
49
+ });
50
+ }
51
+ /** Install browser engines (used by `browsers install`, `doctor --fix`, `init`). */
52
+ export async function installBrowsers(opts = {}) {
53
+ const engines = [
54
+ ...new Set((opts.browsers?.length ? opts.browsers : Object.keys(ENGINE_OF)).map((b) => ENGINE_OF[b] ?? b)),
55
+ ];
56
+ const args = ['playwright', 'install', ...(opts.withDeps ? ['--with-deps'] : []), ...engines];
57
+ await execa('npx', args, { stdio: 'inherit', cwd: opts.cwd });
58
+ }
59
+ export function register(program) {
60
+ const browsers = program.command('browsers').description('Install and list browser engines');
61
+ browsers
62
+ .command('install')
63
+ .description('Install browser engines (default: chromium, firefox, webkit)')
64
+ .option('-b, --browser <name>', 'browser to install (repeatable)', collect, [])
65
+ .option('--with-deps', 'also install OS dependencies (Linux CI)')
66
+ .option('-p, --project <slug>', 'install the browsers declared by a project')
67
+ .action(async (opts, cmd) => {
68
+ const ctx = createContext(cmd);
69
+ let list = opts.browser;
70
+ if (opts.project)
71
+ list = ctx.registry.get(opts.project).browsers;
72
+ for (const b of list)
73
+ BrowserSchema.parse(b);
74
+ await installBrowsers({ browsers: list, withDeps: opts.withDeps, cwd: ctx.rootDir });
75
+ ok(`Installed ${list.length ? list.join(', ') : 'chromium, firefox, webkit'}`);
76
+ });
77
+ browsers
78
+ .command('list')
79
+ .description('Show which browsers are installed and where')
80
+ .option('-p, --project <slug>', 'limit to the browsers declared by a project')
81
+ .action(async (opts, cmd) => {
82
+ const ctx = createContext(cmd);
83
+ const names = opts.project ? ctx.registry.get(opts.project).browsers : Object.keys(ENGINE_OF);
84
+ const statuses = await browserStatuses(names);
85
+ if (ctx.opts.json)
86
+ return json(statuses);
87
+ table(statuses.map((s) => ({
88
+ browser: s.name,
89
+ engine: s.engine,
90
+ installed: s.installed ? pc.green('yes') : pc.red('no'),
91
+ playwright: s.playwrightVersion,
92
+ executable: s.executable ?? '',
93
+ })));
94
+ if (statuses.some((s) => !s.installed)) {
95
+ process.exitCode = 1;
96
+ throw new SdodsError('NOT_SUPPORTED', 'Some browsers are not installed.', {
97
+ hint: 'Run `sdods browsers install --with-deps`.',
98
+ exitCode: 1,
99
+ });
100
+ }
101
+ });
102
+ }
103
+ //# sourceMappingURL=browsers.js.map
@@ -0,0 +1,17 @@
1
+ import type { Command } from 'commander';
2
+ type Shell = 'bash' | 'zsh' | 'fish';
3
+ interface Node {
4
+ name: string;
5
+ description: string;
6
+ options: Array<{
7
+ flags: string[];
8
+ description: string;
9
+ }>;
10
+ children: Node[];
11
+ }
12
+ /** Walk commander metadata into a plain tree (no actions executed). */
13
+ export declare function commandTree(cmd: Command): Node;
14
+ export declare function renderCompletion(shell: Shell, root: Node): string;
15
+ export declare function register(program: Command): void;
16
+ export {};
17
+ //# sourceMappingURL=completion.d.ts.map
@@ -0,0 +1,143 @@
1
+ import { SdodsError } from '@sdods/core';
2
+ /** Walk commander metadata into a plain tree (no actions executed). */
3
+ export function commandTree(cmd) {
4
+ return {
5
+ name: cmd.name(),
6
+ description: cmd.description(),
7
+ options: cmd.options.map((o) => ({
8
+ flags: [o.short, o.long].filter((f) => Boolean(f)),
9
+ description: o.description,
10
+ })),
11
+ children: cmd.commands
12
+ .filter((c) => !c.name().startsWith('_'))
13
+ .map((c) => commandTree(c))
14
+ .sort((a, b) => a.name.localeCompare(b.name)),
15
+ };
16
+ }
17
+ export function renderCompletion(shell, root) {
18
+ switch (shell) {
19
+ case 'bash':
20
+ return bash(root);
21
+ case 'zsh':
22
+ return zsh(root);
23
+ case 'fish':
24
+ return fish(root);
25
+ }
26
+ }
27
+ function q(s) {
28
+ return s.replace(/'/g, "'\\''");
29
+ }
30
+ function words(n) {
31
+ return [...n.children.map((c) => c.name), ...n.options.flatMap((o) => o.flags)].join(' ');
32
+ }
33
+ function bash(root) {
34
+ const cases = root.children
35
+ .map((c) => {
36
+ const sub = c.children.length
37
+ ? c.children
38
+ .map((s) => ` ${s.name}) COMPREPLY=( $(compgen -W '${q(words(s))} ${q(root.options.flatMap((o) => o.flags).join(' '))}' -- "$cur") ); return 0 ;;`)
39
+ .join('\n')
40
+ : '';
41
+ return ` ${c.name})
42
+ case "\${COMP_WORDS[2]}" in
43
+ ${sub}
44
+ *) COMPREPLY=( $(compgen -W '${q(words(c))}' -- "$cur") ); return 0 ;;
45
+ esac ;;`;
46
+ })
47
+ .join('\n');
48
+ return `# bash completion for sdods — generated by \`sdods completion bash\`
49
+ # Install: sdods completion bash > ~/.sdods-completion.bash && echo 'source ~/.sdods-completion.bash' >> ~/.bashrc
50
+ _sdods() {
51
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
52
+ if [[ $COMP_CWORD -eq 1 ]]; then
53
+ COMPREPLY=( $(compgen -W '${q(words(root))}' -- "$cur") ); return 0
54
+ fi
55
+ case "\${COMP_WORDS[1]}" in
56
+ ${cases}
57
+ *) COMPREPLY=( $(compgen -W '${q(root.options.flatMap((o) => o.flags).join(' '))}' -- "$cur") ) ;;
58
+ esac
59
+ }
60
+ complete -F _sdods sdods
61
+ `;
62
+ }
63
+ function zsh(root) {
64
+ const describe = (n, indent) => n.children.map((c) => `${indent}'${q(c.name)}:${q(c.description || c.name)}'`).join('\n');
65
+ const optSpecs = (n) => n.options.flatMap((o) => o.flags.map((f) => `'${q(f)}[${q(o.description || f)}]'`)).join(' ');
66
+ const level2 = root.children
67
+ .map((c) => ` ${c.name})
68
+ if (( CURRENT == 3 )) && [[ -n "${c.children.length ? 'x' : ''}" ]]; then
69
+ local -a subcmds; subcmds=(
70
+ ${describe(c, ' ')}
71
+ )
72
+ _describe 'subcommand' subcmds
73
+ else
74
+ _arguments -s ${optSpecs(c)} ${optSpecs(root)} '*:file:_files'
75
+ fi ;;`)
76
+ .join('\n');
77
+ return `#compdef sdods
78
+ # zsh completion for sdods — generated by \`sdods completion zsh\`
79
+ # Install: sdods completion zsh > "\${fpath[1]}/_sdods" && autoload -Uz compinit && compinit
80
+ _sdods() {
81
+ local -a cmds
82
+ cmds=(
83
+ ${describe(root, ' ')}
84
+ )
85
+ if (( CURRENT == 2 )); then
86
+ _describe 'command' cmds
87
+ _arguments -s ${optSpecs(root)}
88
+ return
89
+ fi
90
+ case "\${words[2]}" in
91
+ ${level2}
92
+ *) _arguments -s ${optSpecs(root)} ;;
93
+ esac
94
+ }
95
+ _sdods "$@"
96
+ `;
97
+ }
98
+ function fish(root) {
99
+ const lines = [
100
+ '# fish completion for sdods — generated by `sdods completion fish`',
101
+ '# Install: sdods completion fish > ~/.config/fish/completions/sdods.fish',
102
+ `complete -c sdods -f`,
103
+ ];
104
+ for (const o of root.options) {
105
+ for (const f of o.flags) {
106
+ lines.push(`complete -c sdods ${f.startsWith('--') ? `-l ${f.slice(2)}` : `-s ${f.slice(1)}`} -d '${q(o.description)}'`);
107
+ }
108
+ }
109
+ for (const c of root.children) {
110
+ lines.push(`complete -c sdods -n '__fish_use_subcommand' -a ${c.name} -d '${q(c.description)}'`);
111
+ for (const s of c.children) {
112
+ lines.push(`complete -c sdods -n '__fish_seen_subcommand_from ${c.name}' -a ${s.name} -d '${q(s.description)}'`);
113
+ for (const o of s.options) {
114
+ for (const f of o.flags) {
115
+ lines.push(`complete -c sdods -n '__fish_seen_subcommand_from ${s.name}' ${f.startsWith('--') ? `-l ${f.slice(2)}` : `-s ${f.slice(1)}`} -d '${q(o.description)}'`);
116
+ }
117
+ }
118
+ }
119
+ for (const o of c.options) {
120
+ for (const f of o.flags) {
121
+ lines.push(`complete -c sdods -n '__fish_seen_subcommand_from ${c.name}' ${f.startsWith('--') ? `-l ${f.slice(2)}` : `-s ${f.slice(1)}`} -d '${q(o.description)}'`);
122
+ }
123
+ }
124
+ }
125
+ return lines.join('\n') + '\n';
126
+ }
127
+ export function register(program) {
128
+ program
129
+ .command('completion <shell>')
130
+ .description('Print a shell completion script (bash | zsh | fish)')
131
+ .action((shell) => {
132
+ if (!['bash', 'zsh', 'fish'].includes(shell)) {
133
+ throw new SdodsError('CONFIG_INVALID', `Unknown shell "${shell}". Use bash, zsh or fish.`, {
134
+ exitCode: 2,
135
+ });
136
+ }
137
+ let root = program;
138
+ while (root.parent)
139
+ root = root.parent;
140
+ process.stdout.write(renderCompletion(shell, commandTree(root)));
141
+ });
142
+ }
143
+ //# sourceMappingURL=completion.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerConfigCommands(program: Command): void;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,79 @@
1
+ import pc from 'picocolors';
2
+ import { explainConfig, redact } from '@sdods/core';
3
+ import { createContext } from '../context.js';
4
+ import { json, out, table } from '../ui.js';
5
+ export function registerConfigCommands(program) {
6
+ const config = program.command('config').description('Inspect resolved configuration');
7
+ config
8
+ .command('show')
9
+ .description('Print the merged config for a project/env with the winning layer per key')
10
+ .requiredOption('-p, --project <slug>', 'project slug')
11
+ .option('-e, --env <name>', 'environment name (default: project envs.default)')
12
+ .option('--explain', 'show provenance table instead of the tree')
13
+ .option('--show-secrets', 'do not redact secret-looking values')
14
+ .action((opts, cmd) => {
15
+ const ctx = createContext(cmd);
16
+ const cfg = ctx.registry.resolve(opts.project, opts.env);
17
+ const view = opts.showSecrets ? cfg : redact(cfg);
18
+ if (opts.explain) {
19
+ const rows = explainConfig(cfg).map((r) => ({
20
+ path: r.path,
21
+ layer: r.layer,
22
+ value: typeof r.value === 'object'
23
+ ? JSON.stringify(r.value)
24
+ : String(opts.showSecrets ? r.value : redactLeaf(r.path, r.value)),
25
+ }));
26
+ if (ctx.opts.json)
27
+ return json(rows);
28
+ return table(rows, ['path', 'layer', 'value']);
29
+ }
30
+ if (ctx.opts.json)
31
+ return json({
32
+ project: view.project,
33
+ env: view.env,
34
+ runtime: view.runtime,
35
+ sources: view.sources,
36
+ });
37
+ out(pc.bold(`Project ${cfg.project.slug} · env ${cfg.env.name}`));
38
+ out(pc.dim(`dotenv files: ${cfg.sources.dotenvFiles.length ? cfg.sources.dotenvFiles.join(', ') : '(none)'}`));
39
+ out(JSON.stringify({ project: view.project, env: view.env, runtime: view.runtime }, null, 2));
40
+ });
41
+ config
42
+ .command('validate')
43
+ .description('Validate every project and environment file without running anything')
44
+ .action((_opts, cmd) => {
45
+ const ctx = createContext(cmd);
46
+ const results = [];
47
+ for (const e of ctx.registry.entriesList()) {
48
+ for (const env of e.config.envs.available) {
49
+ try {
50
+ ctx.registry.resolve(e.slug, env, {}, { ...process.env });
51
+ results.push({ project: e.slug, env, ok: true });
52
+ }
53
+ catch (err) {
54
+ results.push({
55
+ project: e.slug,
56
+ env,
57
+ ok: false,
58
+ error: err.message.split('\n')[0],
59
+ });
60
+ }
61
+ }
62
+ }
63
+ if (ctx.opts.json)
64
+ return json(results);
65
+ table(results.map((r) => ({
66
+ ...r,
67
+ ok: r.ok ? pc.green('ok') : pc.red('FAIL'),
68
+ error: r.error ?? '',
69
+ })));
70
+ if (results.some((r) => !r.ok))
71
+ process.exitCode = 2;
72
+ });
73
+ }
74
+ function redactLeaf(path, value) {
75
+ return /(password|secret|token|apikey|api_key)/i.test(path) && typeof value === 'string' && value
76
+ ? '***'
77
+ : value;
78
+ }
79
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function register(program: Command): void;
3
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1,65 @@
1
+ import pc from 'picocolors';
2
+ import { createContext } from '../context.js';
3
+ import { heading, json, out, table } from '../ui.js';
4
+ export function register(program) {
5
+ program
6
+ .command('coverage')
7
+ .description('Route, endpoint and role coverage by scenarios, split by suite tag')
8
+ .requiredOption('-p, --project <slug>', 'project slug')
9
+ .option('-e, --env <name>', 'environment (for the OpenAPI spec and API base URL)')
10
+ .option('--openapi [file]', 'include endpoints from the OpenAPI spec (env api.openapi or a file)')
11
+ .option('--routes', 'only routes')
12
+ .option('--endpoints', 'only endpoints')
13
+ .option('--roles', 'only roles')
14
+ .option('--uncovered', 'only rows without scenarios')
15
+ .action(async (opts, cmd) => {
16
+ const ctx = createContext(cmd);
17
+ const { computeCoverage } = await import('@sdods/core/analyze');
18
+ const report = computeCoverage(ctx.registry, opts.project, {
19
+ env: opts.env,
20
+ openapi: opts.openapi === true ? true : (opts.openapi ?? false),
21
+ });
22
+ if (ctx.opts.json)
23
+ return json(report);
24
+ printCoverage(report, opts);
25
+ if (report.summary.uncoveredModules.length)
26
+ process.exitCode = 1;
27
+ });
28
+ }
29
+ function rows(list, suites, onlyUncovered) {
30
+ return list
31
+ .filter((r) => !onlyUncovered || !r.covered)
32
+ .map((r) => ({
33
+ name: r.name,
34
+ target: r.target,
35
+ module: r.module ?? '',
36
+ covered: r.covered ? pc.green('yes') : pc.red('no'),
37
+ ...Object.fromEntries(suites.map((s) => [s, r.bySuite[s] ?? 0])),
38
+ scenarios: r.scenarios.length,
39
+ }));
40
+ }
41
+ function printCoverage(report, opts) {
42
+ const all = !opts.routes && !opts.endpoints && !opts.roles;
43
+ const s = report.summary;
44
+ heading(`Coverage for ${report.project}`);
45
+ out(`${pc.bold('routes')} ${s.routes.covered}/${s.routes.total} ${pc.bold('endpoints')} ${s.endpoints.covered}/${s.endpoints.total} ${pc.bold('roles')} ${s.roles.covered}/${s.roles.total} ${pc.bold('scenarios')} ${s.scenarios} ${pc.dim(Object.entries(s.bySuite)
46
+ .map(([k, v]) => `${k}=${v}`)
47
+ .join(' '))}`);
48
+ const cols = ['name', 'target', 'module', 'covered', ...report.suites, 'scenarios'];
49
+ if (all || opts.routes) {
50
+ heading('Routes');
51
+ table(rows(report.routes, report.suites, Boolean(opts.uncovered)), cols);
52
+ }
53
+ if (all || opts.endpoints) {
54
+ heading('Endpoints');
55
+ table(rows(report.endpoints, report.suites, Boolean(opts.uncovered)), cols);
56
+ }
57
+ if (all || opts.roles) {
58
+ heading('Roles');
59
+ table(rows(report.roles, report.suites, Boolean(opts.uncovered)), cols);
60
+ }
61
+ if (s.uncoveredModules.length) {
62
+ out(`\n${pc.yellow('⚠')} Modules without any covered target: ${s.uncoveredModules.join(', ')}`);
63
+ }
64
+ }
65
+ //# sourceMappingURL=coverage.js.map
@@ -0,0 +1,7 @@
1
+ import type { Command } from 'commander';
2
+ type Row = Record<string, unknown>;
3
+ /** CSV/JSON/YAML → rows (CSV via csv-parse, loaded lazily). */
4
+ export declare function readRows(file: string): Promise<Row[]>;
5
+ export declare function register(program: Command): void;
6
+ export {};
7
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1,188 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { extname, resolve } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { SdodsError } from '@sdods/core';
6
+ import { createContext } from '../context.js';
7
+ import { json, ok, out, table } from '../ui.js';
8
+ /** CSV/JSON/YAML → rows (CSV via csv-parse, loaded lazily). */
9
+ export async function readRows(file) {
10
+ const abs = resolve(file);
11
+ const text = readFileSync(abs, 'utf8');
12
+ const ext = extname(abs).toLowerCase();
13
+ if (ext === '.json') {
14
+ const data = JSON.parse(text);
15
+ return Array.isArray(data) ? data : Array.isArray(data?.rows) ? data.rows : [data];
16
+ }
17
+ if (ext === '.yaml' || ext === '.yml') {
18
+ const data = parseYaml(text);
19
+ return Array.isArray(data) ? data : Array.isArray(data?.rows) ? data.rows : [data];
20
+ }
21
+ const { parse } = await import('csv-parse/sync');
22
+ return parse(text, {
23
+ columns: true,
24
+ skip_empty_lines: true,
25
+ trim: true,
26
+ bom: true,
27
+ cast: true,
28
+ cast_date: false,
29
+ });
30
+ }
31
+ export function register(program) {
32
+ const data = program
33
+ .command('data')
34
+ .description('Import, preview and seed test data (CSV, JSON, YAML)');
35
+ data
36
+ .command('preview <file>')
37
+ .description('Show the first rows and inferred columns of a data file')
38
+ .option('--limit <n>', 'rows to show', '10')
39
+ .action(async (file, opts, cmd) => {
40
+ const ctx = createContext(cmd);
41
+ const rows = await readRows(file);
42
+ const columns = [...new Set(rows.flatMap((r) => Object.keys(r)))];
43
+ const head = rows.slice(0, Number(opts.limit));
44
+ if (ctx.opts.json)
45
+ return json({ file, rows: rows.length, columns, sample: head });
46
+ out(pc.dim(`${rows.length} row(s), columns: ${columns.join(', ')}`));
47
+ table(head.map((r) => Object.fromEntries(columns.map((c) => [c, String(r[c] ?? '')]))));
48
+ });
49
+ data
50
+ .command('import <dataset> <file>')
51
+ .description('Import a data file into the database (datasets table or a td_<project>_<dataset> table)')
52
+ .requiredOption('-p, --project <slug>', 'project slug')
53
+ .option('-e, --env <name>', 'environment key (default: * = every environment)')
54
+ .option('--to <target>', 'db (datasets/dataset_rows) or table (td_* table)', 'db')
55
+ .option('--truncate', 'replace existing rows for this env when --to table')
56
+ .action(async (dataset, file, opts, cmd) => {
57
+ const ctx = createContext(cmd);
58
+ const entry = ctx.registry.entry(opts.project);
59
+ const rows = await readRows(file);
60
+ const m = await import('@sdods/db');
61
+ const adb = m.createDb();
62
+ try {
63
+ await m.migrateToLatest(adb);
64
+ const projectId = await m.ensureProject(adb.db, adb.driver, entry.slug, entry.config.name);
65
+ if (opts.to === 'table') {
66
+ const res = await m.importRowsToTable(adb.db, adb.driver, {
67
+ projectSlug: entry.slug,
68
+ dataset,
69
+ env: opts.env ?? '*',
70
+ rows,
71
+ truncate: Boolean(opts.truncate),
72
+ });
73
+ if (ctx.opts.json)
74
+ return json(res);
75
+ ok(`Imported ${res.inserted} row(s) into ${res.table} (${res.columns.join(', ')})`);
76
+ return;
77
+ }
78
+ if (opts.to !== 'db')
79
+ throw new SdodsError('NOT_SUPPORTED', `--to must be db or table (got ${opts.to}).`, {
80
+ exitCode: 2,
81
+ });
82
+ const res = await m.upsertDataset(adb.db, adb.driver, {
83
+ projectId,
84
+ envKey: opts.env ?? '*',
85
+ name: dataset,
86
+ kind: extname(file).slice(1) || 'json',
87
+ storage: 'db',
88
+ sourcePath: resolve(file),
89
+ rows,
90
+ });
91
+ if (ctx.opts.json)
92
+ return json(res);
93
+ ok(`Imported dataset ${dataset} (${res.rowCount} row(s)) for ${entry.slug} [${opts.env ?? '*'}]`);
94
+ }
95
+ finally {
96
+ await adb.close();
97
+ }
98
+ });
99
+ data
100
+ .command('list')
101
+ .description('List datasets stored in the database for a project')
102
+ .requiredOption('-p, --project <slug>', 'project slug')
103
+ .action(async (opts, cmd) => {
104
+ const ctx = createContext(cmd);
105
+ const m = await import('@sdods/db');
106
+ const adb = m.createDb();
107
+ try {
108
+ await m.migrateToLatest(adb);
109
+ const project = await m.getProjectBySlug(adb.db, opts.project);
110
+ const rows = project ? await m.listDatasets(adb.db, project.id) : [];
111
+ const tables = (await m.listTdTables(adb.db, adb.driver)).filter((t) => t.startsWith(`td_${opts.project.replace(/[^a-z0-9]+/g, '_')}_`));
112
+ if (ctx.opts.json)
113
+ return json({ datasets: rows, tables });
114
+ table(rows.map((d) => ({
115
+ name: d.name,
116
+ env: d.envKey,
117
+ kind: d.kind,
118
+ rows: d.rowCount,
119
+ columns: d.columns.join(','),
120
+ })));
121
+ if (tables.length)
122
+ out(pc.dim(`td tables: ${tables.join(', ')}`));
123
+ }
124
+ finally {
125
+ await adb.close();
126
+ }
127
+ });
128
+ data
129
+ .command('seed')
130
+ .description('Import every file under projects/<slug>/data/<env>/ and data/common/ into the database')
131
+ .requiredOption('-p, --project <slug>', 'project slug')
132
+ .option('-e, --env <name>', 'environment (default: project default)')
133
+ .option('--to <target>', 'db or table', 'db')
134
+ .action(async (opts, cmd) => {
135
+ const ctx = createContext(cmd);
136
+ const entry = ctx.registry.entry(opts.project);
137
+ const env = opts.env ?? entry.config.envs.default;
138
+ const { existsSync, readdirSync } = await import('node:fs');
139
+ const { join } = await import('node:path');
140
+ const m = await import('@sdods/db');
141
+ const adb = m.createDb();
142
+ const imported = [];
143
+ try {
144
+ await m.migrateToLatest(adb);
145
+ const projectId = await m.ensureProject(adb.db, adb.driver, entry.slug, entry.config.name);
146
+ for (const [envKey, dir] of [
147
+ ['*', join(entry.root, 'data', 'common')],
148
+ [env, join(entry.root, 'data', env)],
149
+ ]) {
150
+ if (!existsSync(dir))
151
+ continue;
152
+ for (const f of readdirSync(dir)) {
153
+ if (!/\.(csv|json|ya?ml)$/i.test(f))
154
+ continue;
155
+ const dataset = f.replace(/\.(csv|json|ya?ml)$/i, '');
156
+ const rows = await readRows(join(dir, f));
157
+ if (opts.to === 'table')
158
+ await m.importRowsToTable(adb.db, adb.driver, {
159
+ projectSlug: entry.slug,
160
+ dataset,
161
+ env: envKey,
162
+ rows,
163
+ truncate: true,
164
+ });
165
+ else
166
+ await m.upsertDataset(adb.db, adb.driver, {
167
+ projectId,
168
+ envKey,
169
+ name: dataset,
170
+ kind: f.split('.').pop(),
171
+ storage: 'db',
172
+ sourcePath: join(dir, f),
173
+ rows,
174
+ });
175
+ imported.push({ dataset, env: envKey, rows: rows.length });
176
+ }
177
+ }
178
+ if (ctx.opts.json)
179
+ return json(imported);
180
+ table(imported);
181
+ ok(`Seeded ${imported.length} dataset(s) for ${entry.slug}`);
182
+ }
183
+ finally {
184
+ await adb.close();
185
+ }
186
+ });
187
+ }
188
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function register(program: Command): void;
3
+ //# sourceMappingURL=db.d.ts.map