@fromeroc9/testform 1.0.3 → 1.0.4

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 (50) hide show
  1. package/dist/action/index.js +1 -1
  2. package/dist/action.js +60 -0
  3. package/dist/adapters/github.js +467 -0
  4. package/dist/adapters/resources.js +363 -0
  5. package/dist/cli/index.js +3 -3
  6. package/dist/commands/apply.js +390 -0
  7. package/dist/commands/destroy.js +85 -0
  8. package/dist/commands/diff.js +131 -0
  9. package/dist/commands/fmt.js +166 -0
  10. package/dist/commands/force-unlock.js +55 -0
  11. package/dist/commands/generate.js +143 -0
  12. package/dist/commands/graph.js +159 -0
  13. package/dist/commands/import.js +222 -0
  14. package/dist/commands/init.js +167 -0
  15. package/dist/commands/login.js +71 -0
  16. package/dist/commands/logout.js +20 -0
  17. package/dist/commands/plan.js +250 -0
  18. package/dist/commands/refresh.js +165 -0
  19. package/dist/commands/report.js +724 -0
  20. package/dist/commands/show.js +61 -0
  21. package/dist/commands/state.js +197 -0
  22. package/dist/commands/taint.js +49 -0
  23. package/dist/commands/validate.js +128 -0
  24. package/dist/commands/workspace.js +102 -0
  25. package/dist/const.js +105 -0
  26. package/dist/core/backends/azurerm.js +201 -0
  27. package/dist/core/backends/backend.js +2 -0
  28. package/dist/core/backends/gcs.js +200 -0
  29. package/dist/core/backends/local.js +162 -0
  30. package/dist/core/backends/s3.js +224 -0
  31. package/dist/core/command-context.js +59 -0
  32. package/dist/core/config.js +131 -0
  33. package/dist/core/credentials.js +53 -0
  34. package/dist/core/parser.js +62 -0
  35. package/dist/core/parsers/base-parser.js +215 -0
  36. package/dist/core/parsers/testcase-parser.js +115 -0
  37. package/dist/core/parsers/testplan-parser.js +41 -0
  38. package/dist/core/parsers/testrun-parser.js +43 -0
  39. package/dist/core/policy.js +341 -0
  40. package/dist/core/prompt.js +109 -0
  41. package/dist/core/state.js +185 -0
  42. package/dist/core/utils.js +94 -0
  43. package/dist/core/variables.js +108 -0
  44. package/dist/core/workspace.js +56 -0
  45. package/dist/help.js +797 -0
  46. package/dist/index.js +650 -0
  47. package/dist/logger.js +134 -0
  48. package/dist/notify.js +36 -0
  49. package/dist/types.js +2 -0
  50. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fmtCmd = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const logger_1 = require("../logger");
7
+ const fmtCmd = async (options) => {
8
+ const { dir = '.', check = false, list = true, write = true, recursive = false } = options;
9
+ const files = findFeatureFiles(dir, recursive);
10
+ if (files.length === 0) {
11
+ logger_1.logger.warn('No .feature files found to format.');
12
+ return;
13
+ }
14
+ let unformattedCount = 0;
15
+ for (const file of files) {
16
+ const content = (0, fs_1.readFileSync)(file, 'utf8');
17
+ const formatted = formatGherkin(content);
18
+ if (content !== formatted) {
19
+ unformattedCount++;
20
+ if (!check && write) {
21
+ (0, fs_1.writeFileSync)(file, formatted, 'utf8');
22
+ }
23
+ if (list) {
24
+ if (check) {
25
+ logger_1.logger.warn(file);
26
+ }
27
+ else {
28
+ logger_1.logger.success(file);
29
+ }
30
+ }
31
+ }
32
+ }
33
+ if (check) {
34
+ if (unformattedCount > 0) {
35
+ logger_1.logger.warn(`\n${unformattedCount} file(s) would be reformatted.`);
36
+ process.exit(3); // fmt -check returns 3 if unformatted
37
+ }
38
+ else {
39
+ logger_1.logger.success('All files are formatted correctly.');
40
+ }
41
+ }
42
+ };
43
+ exports.fmtCmd = fmtCmd;
44
+ function findFeatureFiles(dir, recursive) {
45
+ try {
46
+ const stat = (0, fs_1.statSync)(dir);
47
+ if (stat.isFile() && dir.endsWith('.feature')) {
48
+ return [dir];
49
+ }
50
+ if (stat.isDirectory()) {
51
+ // Ignore node_modules and .git
52
+ if (dir.includes('node_modules') || dir.includes('.git'))
53
+ return [];
54
+ const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
55
+ const result = [];
56
+ for (const entry of entries) {
57
+ const filePath = (0, path_1.join)(dir, entry.name);
58
+ if (entry.isDirectory()) {
59
+ if (recursive) {
60
+ result.push(...findFeatureFiles(filePath, recursive));
61
+ }
62
+ }
63
+ else if (entry.name.endsWith('.feature')) {
64
+ result.push(filePath);
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ return [];
70
+ }
71
+ catch (e) {
72
+ return [];
73
+ }
74
+ }
75
+ function formatGherkin(content) {
76
+ const lines = content.split('\n');
77
+ const output = [];
78
+ let expectedIndent = 0;
79
+ let inDocString = false;
80
+ let docStringIndent = 0;
81
+ for (let i = 0; i < lines.length; i++) {
82
+ let line = lines[i];
83
+ // Handle docstrings
84
+ if (line.trim().startsWith('"""') || line.trim().startsWith('```')) {
85
+ if (!inDocString) {
86
+ inDocString = true;
87
+ docStringIndent = 6;
88
+ output.push(' '.repeat(docStringIndent) + line.trim());
89
+ }
90
+ else {
91
+ inDocString = false;
92
+ output.push(' '.repeat(docStringIndent) + line.trim());
93
+ }
94
+ continue;
95
+ }
96
+ if (inDocString) {
97
+ output.push(line); // Keep docstring content as is
98
+ continue;
99
+ }
100
+ let trimmed = line.trim();
101
+ if (trimmed === '') {
102
+ // Only add empty line if the previous line wasn't empty
103
+ if (output.length > 0 && output[output.length - 1] !== '') {
104
+ output.push('');
105
+ }
106
+ continue;
107
+ }
108
+ // Determine indentation based on keyword
109
+ if (trimmed.startsWith('Feature:')) {
110
+ expectedIndent = 0;
111
+ }
112
+ else if (trimmed.startsWith('Rule:') || trimmed.startsWith('Background:') || trimmed.startsWith('Scenario:') || trimmed.startsWith('Scenario Outline:') || trimmed.startsWith('Example:')) {
113
+ expectedIndent = 2;
114
+ // Add blank line before these blocks if needed
115
+ if (output.length > 0 && output[output.length - 1] !== '' && !output[output.length - 1].trim().startsWith('@')) {
116
+ output.push('');
117
+ }
118
+ }
119
+ else if (trimmed.startsWith('Given ') || trimmed.startsWith('When ') || trimmed.startsWith('Then ') || trimmed.startsWith('And ') || trimmed.startsWith('But ') || trimmed.startsWith('* ')) {
120
+ expectedIndent = 4;
121
+ }
122
+ else if (trimmed.startsWith('Examples:')) {
123
+ expectedIndent = 4;
124
+ if (output.length > 0 && output[output.length - 1] !== '') {
125
+ output.push('');
126
+ }
127
+ }
128
+ else if (trimmed.startsWith('|')) {
129
+ expectedIndent = 6;
130
+ }
131
+ else if (trimmed.startsWith('@')) {
132
+ // Tags go with the next element, so we peek ahead to see what it is
133
+ let nextIndent = 0;
134
+ for (let j = i + 1; j < lines.length; j++) {
135
+ let nextTrimmed = lines[j].trim();
136
+ if (nextTrimmed === '' || nextTrimmed.startsWith('@'))
137
+ continue;
138
+ if (nextTrimmed.startsWith('Feature:'))
139
+ nextIndent = 0;
140
+ else if (nextTrimmed.startsWith('Rule:') || nextTrimmed.startsWith('Background:') || nextTrimmed.startsWith('Scenario:') || nextTrimmed.startsWith('Scenario Outline:'))
141
+ nextIndent = 2;
142
+ else if (nextTrimmed.startsWith('Examples:'))
143
+ nextIndent = 4;
144
+ break;
145
+ }
146
+ expectedIndent = nextIndent;
147
+ if (output.length > 0 && output[output.length - 1] !== '') {
148
+ output.push('');
149
+ }
150
+ }
151
+ else if (trimmed.startsWith('#')) {
152
+ // Comments preserve expected indent
153
+ }
154
+ else {
155
+ // Continuation of text (e.g. feature description)
156
+ if (expectedIndent === 0)
157
+ expectedIndent = 2; // Feature description
158
+ }
159
+ output.push(' '.repeat(expectedIndent) + trimmed);
160
+ }
161
+ // Ensure trailing newline
162
+ if (output.length > 0 && output[output.length - 1] !== '') {
163
+ output.push('');
164
+ }
165
+ return output.join('\n');
166
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.forceUnlockCmd = void 0;
4
+ const chalk_1 = require("chalk");
5
+ const state_1 = require("../core/state");
6
+ const const_1 = require("../const");
7
+ const forceUnlockCmd = async (options) => {
8
+ const { dir = '.', lockId, force = false, statePath } = options;
9
+ const stateObj = new state_1.State(dir, statePath);
10
+ await stateObj.init();
11
+ const executeUnlock = async () => {
12
+ const result = await stateObj.forceUnlock(lockId);
13
+ if (result.success) {
14
+ console.log((0, chalk_1.green)(`\n${const_1.TITLE_APP} state has been successfully unlocked!\n`));
15
+ if (!force) {
16
+ console.log(`The state has been unlocked, and ${const_1.TITLE_APP} commands should now be able to`);
17
+ console.log(`obtain a new lock on the remote state.`);
18
+ }
19
+ }
20
+ else {
21
+ if (result.currentLockId) {
22
+ console.error((0, chalk_1.red)(`Error: Lock ID does not match.\n\nExpected: ${lockId}\nActual: ${result.currentLockId}\n`));
23
+ }
24
+ else if (result.error) {
25
+ console.error((0, chalk_1.red)(`${result.error}\n`));
26
+ }
27
+ process.exit(1);
28
+ }
29
+ };
30
+ if (force) {
31
+ await executeUnlock();
32
+ return;
33
+ }
34
+ console.log(`Do you really want to force-unlock?`);
35
+ console.log(` ${const_1.TITLE_APP} will remove the lock on the remote state.`);
36
+ console.log(` This will allow local ${const_1.TITLE_APP} commands to modify this state, even though it`);
37
+ console.log(` may be still be in use. Only 'yes' will be accepted to confirm.`);
38
+ console.log('');
39
+ const { createInterface } = require('readline');
40
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
41
+ return new Promise((resolveReject) => {
42
+ rl.question(' Enter a value: ', async (answer) => {
43
+ rl.close();
44
+ if (answer.trim().toLowerCase() === 'yes') {
45
+ await executeUnlock();
46
+ }
47
+ else {
48
+ console.error((0, chalk_1.red)(`\nUnlock cancelled.\n`));
49
+ process.exitCode = 1;
50
+ }
51
+ resolveReject();
52
+ });
53
+ });
54
+ };
55
+ exports.forceUnlockCmd = forceUnlockCmd;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateCmd = void 0;
4
+ const path_1 = require("path");
5
+ const fs_1 = require("fs");
6
+ const config_1 = require("../core/config");
7
+ const logger_1 = require("../logger");
8
+ function findFeatures(dirPath) {
9
+ const result = [];
10
+ try {
11
+ const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
12
+ for (const entry of entries) {
13
+ const fullPath = (0, path_1.join)(dirPath, entry.name);
14
+ if (entry.isDirectory()) {
15
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
16
+ result.push(...findFeatures(fullPath));
17
+ }
18
+ }
19
+ else if (entry.name.endsWith('.feature')) {
20
+ result.push(fullPath);
21
+ }
22
+ }
23
+ }
24
+ catch (e) { }
25
+ return result;
26
+ }
27
+ function findNextIdentity(dirPath, pattern) {
28
+ if (!pattern.includes('*'))
29
+ return null;
30
+ const prefix = pattern.split('*')[0].replace(/^@/, '');
31
+ const suffix = pattern.split('*')[1] || '';
32
+ const regex = new RegExp(`@${prefix}(\\d+)${suffix}\\b`, 'g');
33
+ let max = 0;
34
+ const allFeatures = findFeatures(dirPath);
35
+ for (const file of allFeatures) {
36
+ const content = require('fs').readFileSync(file, 'utf-8');
37
+ let match;
38
+ while ((match = regex.exec(content)) !== null) {
39
+ const num = parseInt(match[1], 10);
40
+ if (num > max)
41
+ max = num;
42
+ }
43
+ }
44
+ return `@${prefix}${max + 1}${suffix}`;
45
+ }
46
+ const generateCmd = async (options) => {
47
+ const { dir, scope, title } = options;
48
+ const config = new config_1.Config(dir);
49
+ const convention = config.getConvention(scope);
50
+ const identityPattern = config.getIdentity(scope);
51
+ const scopeExt = scope.replace('test', ''); // e.g., 'run', 'case', 'plan'
52
+ // Default directory is the scope itself (e.g. 'testrun') if not specified in convention
53
+ const outDir = convention?.directory || scope;
54
+ const fileTpl = convention?.filename || `{YYYYMMDD}_{HHmmss}.${scopeExt}.feature`;
55
+ const now = new Date();
56
+ const YYYYMMDD = now.toISOString().split('T')[0].replace(/-/g, '');
57
+ const HHmmss = now.toTimeString().split(' ')[0].replace(/:/g, '');
58
+ const timestamp = now.getTime().toString();
59
+ let generatedTitle = title;
60
+ let slug = '';
61
+ if (generatedTitle) {
62
+ slug = generatedTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
63
+ }
64
+ let filename = fileTpl
65
+ .replace(/{YYYYMMDD}/g, YYYYMMDD)
66
+ .replace(/{HHmmss}/g, HHmmss)
67
+ .replace(/{timestamp}/g, timestamp)
68
+ .replace(/{slug}/g, slug);
69
+ // Clean up any stray underscores or hyphens left by an empty slug (e.g. "_.run.feature" -> ".run.feature")
70
+ // or "_2026.run.feature" -> "2026.run.feature"
71
+ filename = filename.replace(/^[_\-]+/, '').replace(/[_\-]+(\.)/g, '$1');
72
+ // Generate a short 6-character random hash for unique identity
73
+ const shortHash = Math.random().toString(16).substring(2, 8);
74
+ // Always resolve the extension to .feature and inject the hash before the scope extension
75
+ if (filename.endsWith('.feature')) {
76
+ // If they already included .feature in the template, we inject the hash before the extension
77
+ if (filename.endsWith(`.${scopeExt}.feature`)) {
78
+ filename = filename.replace(`.${scopeExt}.feature`, `_${shortHash}.${scopeExt}.feature`);
79
+ }
80
+ else {
81
+ filename = filename.replace(`.feature`, `_${shortHash}.feature`);
82
+ }
83
+ }
84
+ else {
85
+ // If it doesn't end with .feature, append hash then extension
86
+ if (filename.endsWith(`.${scopeExt}`)) {
87
+ filename = filename.replace(`.${scopeExt}`, `_${shortHash}.${scopeExt}.feature`);
88
+ }
89
+ else {
90
+ filename += `_${shortHash}.${scopeExt}.feature`;
91
+ }
92
+ }
93
+ // If title was not provided, use the generated filename EXACTLY without .feature
94
+ if (!generatedTitle) {
95
+ generatedTitle = filename.replace(/\.feature$/, '');
96
+ }
97
+ const fullPath = (0, path_1.join)(dir, outDir, filename);
98
+ if ((0, fs_1.existsSync)(fullPath)) {
99
+ logger_1.logger.error(`File already exists: ${fullPath}`);
100
+ process.exit(1);
101
+ }
102
+ if (options.rules && options.rules.length > 0) {
103
+ const allFeatures = findFeatures(dir);
104
+ for (const rule of options.rules) {
105
+ const ruleFile = rule.includes('::') ? rule.split('::')[0] : rule;
106
+ const exists = allFeatures.some(f => f.endsWith(ruleFile) || f.includes(ruleFile));
107
+ if (!exists) {
108
+ logger_1.logger.error([
109
+ `Not Found`,
110
+ `The feature file for Rule '${ruleFile}' does not exist in the workspace.`
111
+ ]);
112
+ process.exit(1);
113
+ }
114
+ }
115
+ }
116
+ const parentDir = (0, path_1.dirname)(fullPath);
117
+ if (!(0, fs_1.existsSync)(parentDir)) {
118
+ (0, fs_1.mkdirSync)(parentDir, { recursive: true });
119
+ }
120
+ const nextIdentityTag = (scope === 'testrun' || scope === 'testplan') && identityPattern
121
+ ? findNextIdentity(dir, identityPattern)
122
+ : null;
123
+ let content = `@${scope}`;
124
+ if (nextIdentityTag) {
125
+ content += ` ${nextIdentityTag}`;
126
+ }
127
+ content += `\nFeature: ${generatedTitle}\n`;
128
+ if (options.rules && options.rules.length > 0) {
129
+ content += `\n`;
130
+ for (const rule of options.rules) {
131
+ content += ` Rule: ${rule}\n`;
132
+ }
133
+ }
134
+ try {
135
+ (0, fs_1.writeFileSync)(fullPath, content, 'utf-8');
136
+ logger_1.logger.success(`Generated ${scope} file: ${fullPath}`, { bold: true });
137
+ }
138
+ catch (e) {
139
+ logger_1.logger.error(`Failed to generate file: ${e.message}`);
140
+ process.exit(1);
141
+ }
142
+ };
143
+ exports.generateCmd = generateCmd;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.graphCmd = void 0;
4
+ const chalk_1 = require("chalk");
5
+ const parser_1 = require("../core/parser");
6
+ const graphCmd = async (options = {}) => {
7
+ const { dir = '.', scope = 'testcase', drawCycles = false } = options;
8
+ const parser = new parser_1.Parser(dir);
9
+ const scenarios = parser.content();
10
+ const testcases = scenarios.filter(s => !s.uri.endsWith('.run.feature') && !s.uri.endsWith('.plan.feature'));
11
+ const testrunsAll = scenarios.filter(s => s.uri.endsWith('.run.feature'));
12
+ const testplansAll = scenarios.filter(s => s.uri.endsWith('.plan.feature'));
13
+ // Group testplans by URI
14
+ const testplansMap = new Map();
15
+ for (const p of testplansAll) {
16
+ if (!testplansMap.has(p.uri)) {
17
+ testplansMap.set(p.uri, { name: p.feature?.name || 'Unnamed Plan', testruns: [] });
18
+ }
19
+ if (p.rule?.name)
20
+ testplansMap.get(p.uri).testruns.push(p.rule.name);
21
+ }
22
+ const testplans = Array.from(testplansMap.values());
23
+ // Group testruns by URI
24
+ const testrunsMap = new Map();
25
+ for (const r of testrunsAll) {
26
+ if (!testrunsMap.has(r.uri)) {
27
+ testrunsMap.set(r.uri, { name: r.feature?.name || 'Unnamed Run', testcases: [], identity: r.custom?.identity || r.feature?.name || '', uri: r.uri });
28
+ }
29
+ if (r.rule?.name)
30
+ testrunsMap.get(r.uri).testcases.push(r.rule.name);
31
+ else if (r.name)
32
+ testrunsMap.get(r.uri).testcases.push(`${r.feature?.name}::${r.name}`);
33
+ }
34
+ const testruns = Array.from(testrunsMap.values());
35
+ if (testplans.length === 0 && testruns.length === 0 && testcases.length === 0) {
36
+ console.log('No test configurations found.');
37
+ return;
38
+ }
39
+ console.log((0, chalk_1.bold)('Test Infrastructure Graph\n'));
40
+ const link = (str) => drawCycles ? (0, chalk_1.magenta)(str) : str;
41
+ function findTcs(tcId) {
42
+ let scenarioName = '*';
43
+ let ruleName = tcId;
44
+ if (tcId.includes('::')) {
45
+ const parts = tcId.split('::');
46
+ scenarioName = parts.pop() || '*';
47
+ ruleName = parts.join('::');
48
+ }
49
+ const matches = testcases.filter(r => {
50
+ const matchesFile = r.uri.endsWith(ruleName) || r.uri.includes(ruleName);
51
+ const matchesScenario = scenarioName === '*' || r.name === scenarioName || r.name.includes(scenarioName);
52
+ return matchesFile && matchesScenario;
53
+ });
54
+ // Detect duplicates
55
+ const uris = Array.from(new Set(matches.map(tc => tc.uri)));
56
+ if (uris.length > 1) {
57
+ if (scenarioName !== '*') {
58
+ const { logger } = require('../logger');
59
+ logger.error(`Ambiguous reference for Scenario '${scenarioName}' under Rule '${ruleName}'. It matches multiple files:\n` + uris.map(u => ` - ${u}`).join('\n') + `\nPlease specify the full file path in your Rule to disambiguate.`);
60
+ }
61
+ else {
62
+ const { logger } = require('../logger');
63
+ logger.warn(`Rule '${ruleName}' matches multiple feature files. Processing all of them:\n` + uris.map(u => ` - ${u}`).join('\n') + `\nIf this was unintentional, specify the full file path.`);
64
+ }
65
+ }
66
+ return matches;
67
+ }
68
+ if (scope === 'testplan' || scope === 'testcase') {
69
+ // Print Test Plans at root
70
+ for (const plan of testplans) {
71
+ const planName = plan.name;
72
+ console.log(`📦 ${(0, chalk_1.cyan)((0, chalk_1.bold)(planName))} ${(0, chalk_1.dim)(`(testplan)`)}`);
73
+ const trIds = plan.testruns;
74
+ for (const [i, trId] of trIds.entries()) {
75
+ const isLastRun = i === trIds.length - 1;
76
+ const runPrefix = isLastRun ? '└── ' : '├── ';
77
+ const run = testruns.find(r => r.identity === trId || r.name === trId || r.name.endsWith(trId) || trId.endsWith(r.name) || r.uri.endsWith(trId));
78
+ if (run) {
79
+ const runName = run.name;
80
+ console.log(` ${link(runPrefix)}📂 ${(0, chalk_1.green)(runName)} ${(0, chalk_1.dim)(`(testrun)`)}`);
81
+ if (scope === 'testcase') {
82
+ const tcIds = run.testcases;
83
+ for (let j = 0; j < tcIds.length; j++) {
84
+ const tcId = tcIds.at(j);
85
+ const isLastTc = j === tcIds.length - 1;
86
+ const tcPrefix = isLastRun ? ' ' : '│ ';
87
+ const tcConnector = isLastTc ? '└── ' : '├── ';
88
+ const tcs = findTcs(tcId);
89
+ if (tcs.length > 0) {
90
+ for (let k = 0; k < tcs.length; k++) {
91
+ const tc = tcs.at(k);
92
+ const tcIsLast = isLastTc && k === tcs.length - 1;
93
+ const tcFinalConnector = tcIsLast ? '└── ' : '├── ';
94
+ console.log(` ${tcPrefix}${link(tcFinalConnector)}📄 ${tc.name} ${(0, chalk_1.dim)(`(testcase)`)}`);
95
+ }
96
+ }
97
+ else {
98
+ console.log(` ${tcPrefix}${link(tcConnector)}📄 ${(0, chalk_1.dim)(tcId + ' (Not found)')}`);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ else {
104
+ console.log(` ${link(runPrefix)}📂 ${(0, chalk_1.dim)(trId + ' (Not found)')}`);
105
+ }
106
+ }
107
+ console.log('');
108
+ }
109
+ if (scope === 'testcase') {
110
+ // Identify orphaned testruns
111
+ const referencedRuns = new Set(testplans.flatMap(p => p.testruns || []));
112
+ const orphanedRuns = testruns.filter(r => {
113
+ return !Array.from(referencedRuns).some(ref => r.identity === ref || r.name === ref || r.uri.endsWith(ref) || ref.endsWith(r.name));
114
+ });
115
+ if (orphanedRuns.length > 0) {
116
+ console.log((0, chalk_1.bold)('Orphaned Test Runs (Not linked to any testplan)\n'));
117
+ for (const run of orphanedRuns) {
118
+ const runName = run.name;
119
+ console.log(`📂 ${(0, chalk_1.green)(runName)} ${(0, chalk_1.dim)(`(testrun)`)}`);
120
+ const tcIds = run.testcases;
121
+ for (let j = 0; j < tcIds.length; j++) {
122
+ const tcId = tcIds.at(j);
123
+ const isLastTc = j === tcIds.length - 1;
124
+ const tcConnector = isLastTc ? '└── ' : '├── ';
125
+ console.log(` ${link(tcConnector)}📄 ${tcId}`);
126
+ }
127
+ console.log('');
128
+ }
129
+ }
130
+ }
131
+ }
132
+ else if (scope === 'testrun') {
133
+ // Print Test Runs at root
134
+ for (const run of testruns) {
135
+ const runName = run.name;
136
+ console.log(`📂 ${(0, chalk_1.green)((0, chalk_1.bold)(runName))} ${(0, chalk_1.dim)(`(testrun)`)}`);
137
+ const tcIds = run.testcases;
138
+ for (let j = 0; j < tcIds.length; j++) {
139
+ const tcId = tcIds.at(j);
140
+ const isLastTc = j === tcIds.length - 1;
141
+ const tcConnector = isLastTc ? '└── ' : '├── ';
142
+ const tcs = findTcs(tcId);
143
+ if (tcs.length > 0) {
144
+ for (let k = 0; k < tcs.length; k++) {
145
+ const tc = tcs.at(k);
146
+ const tcIsLast = isLastTc && k === tcs.length - 1;
147
+ const tcFinalConnector = tcIsLast ? '└── ' : '├── ';
148
+ console.log(` ${link(tcFinalConnector)}📄 ${tc.name} ${(0, chalk_1.dim)(`(testcase)`)}`);
149
+ }
150
+ }
151
+ else {
152
+ console.log(` ${link(tcConnector)}📄 ${(0, chalk_1.dim)(tcId + ' (Not found)')}`);
153
+ }
154
+ }
155
+ console.log('');
156
+ }
157
+ }
158
+ };
159
+ exports.graphCmd = graphCmd;