@fromeroc9/testform 1.0.3 → 1.0.5
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/action/index.js +1 -1
- package/dist/action.js +60 -0
- package/dist/adapters/github.js +467 -0
- package/dist/adapters/resources.js +363 -0
- package/dist/cli/index.js +3 -3
- package/dist/commands/apply.js +390 -0
- package/dist/commands/destroy.js +85 -0
- package/dist/commands/diff.js +131 -0
- package/dist/commands/fmt.js +166 -0
- package/dist/commands/force-unlock.js +55 -0
- package/dist/commands/generate.js +143 -0
- package/dist/commands/graph.js +159 -0
- package/dist/commands/import.js +222 -0
- package/dist/commands/init.js +167 -0
- package/dist/commands/login.js +71 -0
- package/dist/commands/logout.js +20 -0
- package/dist/commands/plan.js +250 -0
- package/dist/commands/refresh.js +165 -0
- package/dist/commands/report.js +724 -0
- package/dist/commands/show.js +61 -0
- package/dist/commands/state.js +197 -0
- package/dist/commands/taint.js +49 -0
- package/dist/commands/validate.js +128 -0
- package/dist/commands/workspace.js +102 -0
- package/dist/const.js +105 -0
- package/dist/core/backends/azurerm.js +201 -0
- package/dist/core/backends/backend.js +2 -0
- package/dist/core/backends/gcs.js +200 -0
- package/dist/core/backends/local.js +162 -0
- package/dist/core/backends/s3.js +224 -0
- package/dist/core/command-context.js +59 -0
- package/dist/core/config.js +131 -0
- package/dist/core/credentials.js +53 -0
- package/dist/core/parser.js +62 -0
- package/dist/core/parsers/base-parser.js +215 -0
- package/dist/core/parsers/testcase-parser.js +115 -0
- package/dist/core/parsers/testplan-parser.js +41 -0
- package/dist/core/parsers/testrun-parser.js +43 -0
- package/dist/core/policy.js +341 -0
- package/dist/core/prompt.js +109 -0
- package/dist/core/state.js +185 -0
- package/dist/core/utils.js +94 -0
- package/dist/core/variables.js +108 -0
- package/dist/core/workspace.js +56 -0
- package/dist/help.js +797 -0
- package/dist/index.js +650 -0
- package/dist/logger.js +134 -0
- package/dist/notify.js +36 -0
- package/dist/types.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.showCmd = void 0;
|
|
7
|
+
const chalk_1 = require("chalk");
|
|
8
|
+
const state_1 = require("../core/state");
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const const_1 = require("../const");
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const showCmd = async (options = {}) => {
|
|
13
|
+
const { path, isJson = false, verbose = false, dir = '.', statePath, backupPath } = options;
|
|
14
|
+
const logger = new logger_1.Logger(verbose);
|
|
15
|
+
// If path is "state" or "plan" (legacy support)
|
|
16
|
+
let actualPath = path;
|
|
17
|
+
if (path === 'state')
|
|
18
|
+
actualPath = undefined;
|
|
19
|
+
if (path === 'plan') {
|
|
20
|
+
logger.info(`Use "${const_1.TITLE_CLI} plan" to generate and view an execution plan.`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const state = new state_1.State(dir, actualPath || statePath, backupPath);
|
|
24
|
+
await state.init();
|
|
25
|
+
const current = state.getState();
|
|
26
|
+
const resources = current.resources;
|
|
27
|
+
if (isJson) {
|
|
28
|
+
console.log(JSON.stringify(current, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log((0, chalk_1.bold)(`# ${const_1.TITLE_APP} State`));
|
|
33
|
+
console.log(` Version: ${current.version}`);
|
|
34
|
+
console.log(` Serial: ${current.serial}`);
|
|
35
|
+
console.log(` Lineage: ${current.lineage}`);
|
|
36
|
+
console.log(` Last Sync: ${current.lastSync || '(never)'}`);
|
|
37
|
+
console.log(` Resources: ${resources.length}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
if (resources.length > 0) {
|
|
40
|
+
for (const res of resources) {
|
|
41
|
+
const remoteId = res.attributes.remoteId ?? '(unknown)';
|
|
42
|
+
const status = res.attributes.issueNumber ? (0, chalk_1.green)('synced') : (0, chalk_1.yellow)('pending');
|
|
43
|
+
let formattedIdentity = res.identity;
|
|
44
|
+
if (formattedIdentity.includes('::')) {
|
|
45
|
+
const parts = formattedIdentity.split('::');
|
|
46
|
+
const base = path_1.default.basename(parts[0], '.feature');
|
|
47
|
+
formattedIdentity = `${base}::${parts.slice(1).join('::')}`;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
formattedIdentity = path_1.default.basename(formattedIdentity, '.feature');
|
|
51
|
+
}
|
|
52
|
+
console.log(` ${(0, chalk_1.cyan)(res.type)}.${(0, chalk_1.bold)(formattedIdentity)} [id=${remoteId}] ${status}`);
|
|
53
|
+
console.log(` title: ${res.attributes.title}`);
|
|
54
|
+
console.log(` issueNumber: ${res.attributes.issueNumber ?? '(none)'}`);
|
|
55
|
+
console.log(` localHash: ${(0, chalk_1.dim)(res.attributes.localHash.substring(0, 12))}...`);
|
|
56
|
+
console.log(` lastApplied: ${res.lastApplied}`);
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
exports.showCmd = showCmd;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stateCmd = void 0;
|
|
4
|
+
const chalk_1 = require("chalk");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const state_1 = require("../core/state");
|
|
8
|
+
const const_1 = require("../const");
|
|
9
|
+
const utils_1 = require("../core/utils");
|
|
10
|
+
const ERROR_NO_STATE = `No state file was found!
|
|
11
|
+
|
|
12
|
+
State management commands require a state file. Run this command
|
|
13
|
+
in a directory where ${const_1.TITLE_CLI} has been run or use the -state flag
|
|
14
|
+
to point the command to a specific state location.`;
|
|
15
|
+
const stateCmd = async (options) => {
|
|
16
|
+
const { dir = '.', action, args, statePath, backupPath, isJson, id, dryRun, force } = options;
|
|
17
|
+
const state = new state_1.State(dir, statePath, backupPath);
|
|
18
|
+
if (action !== 'push' && !(await state.hasState())) {
|
|
19
|
+
console.error((0, chalk_1.red)(ERROR_NO_STATE));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
await state.init();
|
|
23
|
+
// Commands that don't need a lock
|
|
24
|
+
if (action === 'pull') {
|
|
25
|
+
const current = state.getState();
|
|
26
|
+
console.log(JSON.stringify(current, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (action === 'push') {
|
|
30
|
+
if (args.length !== 1) {
|
|
31
|
+
console.error((0, chalk_1.red)(`Usage: ${const_1.TITLE_CLI} state push [path]`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const localPath = (0, path_1.resolve)(process.cwd(), args[0]);
|
|
35
|
+
if (!(0, fs_1.existsSync)(localPath)) {
|
|
36
|
+
console.error((0, chalk_1.red)(`Error: File ${localPath} not found.`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const raw = (0, fs_1.readFileSync)(localPath, 'utf-8');
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
await state.acquireLock(true, '0s');
|
|
43
|
+
const current = state.getState();
|
|
44
|
+
// Handle lineage validation if not forced
|
|
45
|
+
if (!force && current.lineage && parsed.lineage && current.lineage !== parsed.lineage) {
|
|
46
|
+
console.error((0, chalk_1.red)(`Error: Cannot push state with different lineage. Use -force to override.`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const mutCurrent = current;
|
|
50
|
+
mutCurrent.resources = parsed.resources || [];
|
|
51
|
+
mutCurrent.serial += 1;
|
|
52
|
+
await state.save();
|
|
53
|
+
await state.releaseLock();
|
|
54
|
+
console.log((0, chalk_1.green)(`Successfully pushed state from ${args[0]}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error((0, chalk_1.red)(`Error parsing or pushing state: ${e.message}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await state.acquireLock(true, '0s');
|
|
63
|
+
try {
|
|
64
|
+
const rawResources = state.getResources();
|
|
65
|
+
const filterResources = (resources) => {
|
|
66
|
+
let filtered = resources;
|
|
67
|
+
if (id) {
|
|
68
|
+
filtered = filtered.filter(r => r.attributes.issueNumber?.toString() === id || r.attributes.remoteId === id);
|
|
69
|
+
}
|
|
70
|
+
if (args.length > 0) {
|
|
71
|
+
filtered = filtered.filter(r => {
|
|
72
|
+
const fullAddress = `${r.type}.${r.identity}`;
|
|
73
|
+
return args.some(addr => fullAddress.startsWith(addr) || r.identity.startsWith(addr));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return filtered;
|
|
77
|
+
};
|
|
78
|
+
if (action === 'list') {
|
|
79
|
+
const resources = filterResources(rawResources);
|
|
80
|
+
if (resources.length === 0 && args.length > 0) {
|
|
81
|
+
console.error((0, chalk_1.red)(`No instance found for the given address!`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
else if (resources.length === 0) {
|
|
85
|
+
console.log('No resources found in state.');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
for (const res of resources) {
|
|
89
|
+
const prefix = res.tainted ? '[tainted] ' : '';
|
|
90
|
+
console.log(`${prefix}${res.type}.${res.identity}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (action === 'identities') {
|
|
95
|
+
if (!isJson) {
|
|
96
|
+
console.error((0, chalk_1.red)(`The \`${const_1.TITLE_CLI} state identities\` command requires the \`-json\` flag.`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const resources = filterResources(rawResources);
|
|
100
|
+
const identities = resources.map(r => r.identity);
|
|
101
|
+
console.log(JSON.stringify(identities, null, 2));
|
|
102
|
+
}
|
|
103
|
+
else if (action === 'show') {
|
|
104
|
+
if (args.length !== 1) {
|
|
105
|
+
console.error((0, chalk_1.red)(`Exactly one argument expected.\nUsage: ${const_1.TITLE_CLI} [global options] state show [options] ADDRESS`));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const address = args[0];
|
|
109
|
+
const identity = address.includes('.') ? address.split('.').slice(1).join('.') : address;
|
|
110
|
+
const res = rawResources.find(r => r.identity === identity || `${r.type}.${r.identity}` === identity);
|
|
111
|
+
if (!res) {
|
|
112
|
+
console.error((0, chalk_1.red)(`No instance found for the given address!`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
console.log((0, chalk_1.bold)(`# ${res.type}.${res.identity}:`));
|
|
116
|
+
console.log(`resource "${res.type}" "${res.identity}" {`);
|
|
117
|
+
const keys = Object.keys(res.attributes);
|
|
118
|
+
let maxKeyLen = 0;
|
|
119
|
+
for (const k of keys) {
|
|
120
|
+
if (k.length > maxKeyLen)
|
|
121
|
+
maxKeyLen = k.length;
|
|
122
|
+
}
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
const padding = ' '.repeat(maxKeyLen - key.length);
|
|
125
|
+
const attrVal = Object.prototype.hasOwnProperty.call(res.attributes, key) ? res.attributes[key] : undefined;
|
|
126
|
+
console.log(` ${key}${padding} = ${(0, utils_1.formatHclValue)(attrVal, 1)}`);
|
|
127
|
+
}
|
|
128
|
+
console.log(`}`);
|
|
129
|
+
if (res.tainted) {
|
|
130
|
+
console.log((0, chalk_1.red)(`\nThis resource is marked as tainted.`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (action === 'rm') {
|
|
134
|
+
if (args.length === 0) {
|
|
135
|
+
console.error((0, chalk_1.red)(`At least one address is required.\n\nUsage: ${const_1.TITLE_CLI} [global options] state rm [options] ADDRESS...`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
let removedCount = 0;
|
|
139
|
+
for (const arg of args) {
|
|
140
|
+
const identity = arg.includes('.') ? arg.split('.').slice(1).join('.') : arg;
|
|
141
|
+
if (rawResources.find(r => r.identity === identity || `${r.type}.${r.identity}` === identity)) {
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
console.log(`Would remove ${arg}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
state.removeResource(identity);
|
|
147
|
+
console.log(`Removed ${arg}`);
|
|
148
|
+
}
|
|
149
|
+
removedCount++;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.error((0, chalk_1.red)(`Error: Resource ${arg} not found in state.`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (removedCount > 0 && !dryRun) {
|
|
156
|
+
await state.save();
|
|
157
|
+
console.log((0, chalk_1.green)(`\nSuccessfully removed ${removedCount} resource instance(s).`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (action === 'mv') {
|
|
161
|
+
if (args.length !== 2) {
|
|
162
|
+
console.error((0, chalk_1.red)(`Exactly two arguments expected.\n\nUsage: ${const_1.TITLE_CLI} [global options] state mv [options] SOURCE DESTINATION`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const source = args[0];
|
|
166
|
+
const dest = args[1];
|
|
167
|
+
const sourceIdentity = source.includes('.') ? source.split('.').slice(1).join('.') : source;
|
|
168
|
+
const destIdentity = dest.includes('.') ? dest.split('.').slice(1).join('.') : dest;
|
|
169
|
+
const res = rawResources.find(r => r.identity === sourceIdentity || `${r.type}.${r.identity}` === sourceIdentity);
|
|
170
|
+
if (!res) {
|
|
171
|
+
console.error((0, chalk_1.red)(`Error: Source resource ${source} not found in state.`));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
console.log(`Would move ${source} to ${dest}`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
state.removeResource(res.identity);
|
|
179
|
+
res.identity = destIdentity;
|
|
180
|
+
if (dest.includes('.')) {
|
|
181
|
+
res.type = dest.split('.')[0];
|
|
182
|
+
}
|
|
183
|
+
state.upsertResource(res);
|
|
184
|
+
await state.save();
|
|
185
|
+
console.log((0, chalk_1.green)(`Move ${source} to ${dest} successfully executed!`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.error((0, chalk_1.red)(`Usage: ${const_1.TITLE_CLI} state <identities|list|mv|pull|push|rm|show> [options] [args]`));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await state.releaseLock();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
exports.stateCmd = stateCmd;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.taintCmd = void 0;
|
|
4
|
+
const chalk_1 = require("chalk");
|
|
5
|
+
const state_1 = require("../core/state");
|
|
6
|
+
const taintCmd = async (options) => {
|
|
7
|
+
const { dir = '.', action, identityRaw, statePath, backupPath, allowMissing = false, lock = true, lockTimeout = '0s' } = options;
|
|
8
|
+
const state = new state_1.State(dir, statePath, backupPath);
|
|
9
|
+
await state.init();
|
|
10
|
+
await state.acquireLock(lock, lockTimeout);
|
|
11
|
+
try {
|
|
12
|
+
const identity = identityRaw.replace(/^github_testcase\./, '');
|
|
13
|
+
const res = state.getResources('github_testcase').find(r => r.identity === identity);
|
|
14
|
+
if (!res) {
|
|
15
|
+
if (allowMissing) {
|
|
16
|
+
console.log((0, chalk_1.green)(`Resource not found in state, but allow-missing is set. Exiting successfully.`));
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
console.error((0, chalk_1.red)(`Error: Resource not found in state.`));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (action === 'taint') {
|
|
23
|
+
if (res.tainted) {
|
|
24
|
+
console.log(`Resource instance ${identityRaw} is already tainted`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
res.tainted = true;
|
|
28
|
+
state.upsertResource(res);
|
|
29
|
+
await state.save();
|
|
30
|
+
console.log((0, chalk_1.green)(`Resource instance ${identityRaw} has been marked as tainted.`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!res.tainted) {
|
|
35
|
+
console.log(`Resource instance ${identityRaw} is not tainted`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
delete res.tainted;
|
|
39
|
+
state.upsertResource(res);
|
|
40
|
+
await state.save();
|
|
41
|
+
console.log((0, chalk_1.green)(`Resource instance ${identityRaw} has been successfully untainted.`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await state.releaseLock();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.taintCmd = taintCmd;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCmd = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const config_1 = require("../core/config");
|
|
7
|
+
const parser_1 = require("../core/parser");
|
|
8
|
+
const policy_1 = require("../core/policy");
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const validateCmd = async (options) => {
|
|
11
|
+
const { targetPath = '.', verbose = false, scope, variables, isJson = false, testDirectory, noTests = false, query } = options;
|
|
12
|
+
const logger = new logger_1.Logger(verbose);
|
|
13
|
+
if (isJson) {
|
|
14
|
+
// Suppress logger output when JSON is active
|
|
15
|
+
// But we just won't call it for the important parts
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
logger.info('Validating configuration...', { bold: true });
|
|
19
|
+
}
|
|
20
|
+
let configDir = targetPath;
|
|
21
|
+
let parseDir = testDirectory ? (0, path_1.join)(targetPath, testDirectory) : targetPath;
|
|
22
|
+
try {
|
|
23
|
+
if ((0, fs_1.statSync)(parseDir).isFile()) {
|
|
24
|
+
configDir = (0, path_1.dirname)(parseDir);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
// Ignore, handled by next steps
|
|
29
|
+
}
|
|
30
|
+
// Parse feature files
|
|
31
|
+
const parser = new parser_1.Parser(parseDir, variables);
|
|
32
|
+
const documents = parser.content();
|
|
33
|
+
if (documents.length === 0) {
|
|
34
|
+
if (isJson) {
|
|
35
|
+
console.log(JSON.stringify({
|
|
36
|
+
valid: false,
|
|
37
|
+
error_count: 1,
|
|
38
|
+
warning_count: 0,
|
|
39
|
+
diagnostics: [{
|
|
40
|
+
severity: 'error',
|
|
41
|
+
summary: 'Failed to read module directory',
|
|
42
|
+
detail: `Module directory ${targetPath} does not exist or cannot be read.`
|
|
43
|
+
}]
|
|
44
|
+
}, null, 2));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
logger.error(`Failed to read module directory\n\nModule directory ${parseDir} does not exist or cannot be read.`);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Load and validate config (exits on error via notify)
|
|
52
|
+
const config = new config_1.Config(configDir);
|
|
53
|
+
logger.debug(`Configuration loaded: v${config.getConfig().version}`);
|
|
54
|
+
logger.debug(`Found ${documents.length} scenarios across feature files`);
|
|
55
|
+
// Filter by scope (only process files matching the requested scope)
|
|
56
|
+
const SCOPE_CONFIG = {
|
|
57
|
+
testcase: { tag: '@testcase', ext: '.case.feature' },
|
|
58
|
+
testrun: { tag: '@testrun', ext: '.run.feature' },
|
|
59
|
+
testplan: { tag: '@testplan', ext: '.plan.feature' },
|
|
60
|
+
};
|
|
61
|
+
const matchesScope = (s, scopeName) => {
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(SCOPE_CONFIG, scopeName))
|
|
63
|
+
return false;
|
|
64
|
+
const cfg = SCOPE_CONFIG[scopeName];
|
|
65
|
+
return s.feature?.tags?.includes(cfg.tag) || s.uri.endsWith(cfg.ext);
|
|
66
|
+
};
|
|
67
|
+
const rawScenarios = documents.filter(s => matchesScope(s, scope));
|
|
68
|
+
if (rawScenarios.length === 0) {
|
|
69
|
+
if (!isJson)
|
|
70
|
+
logger.warn(`No scenarios found for scope "${scope}".`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!isJson)
|
|
74
|
+
logger.debug(`Found ${rawScenarios.length} scenarios matching scope "${scope}"`);
|
|
75
|
+
// Filter with DSL
|
|
76
|
+
const data = {
|
|
77
|
+
identity: config.getIdentity(scope),
|
|
78
|
+
fields: config.getFields(scope),
|
|
79
|
+
};
|
|
80
|
+
let filtered = parser.filter(rawScenarios, data, scope);
|
|
81
|
+
if (query) {
|
|
82
|
+
const lowerQuery = query.toLowerCase();
|
|
83
|
+
filtered = filtered.filter(s => (s.name && s.name.toLowerCase().includes(lowerQuery)) ||
|
|
84
|
+
(s.custom?.identity && s.custom.identity.toLowerCase().includes(lowerQuery)) ||
|
|
85
|
+
(s.tags && s.tags.some(t => t.toLowerCase().includes(lowerQuery))) ||
|
|
86
|
+
(s.uri && s.uri.toLowerCase().includes(lowerQuery)));
|
|
87
|
+
if (!isJson) {
|
|
88
|
+
logger.info(`--- Query Results for "${query}" ---`, { bold: true });
|
|
89
|
+
if (filtered.length === 0) {
|
|
90
|
+
logger.info(`No scenarios matched the query.`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
filtered.forEach(s => {
|
|
94
|
+
logger.info(`- [${scope}] ${s.custom?.identity || s.name} (File: ${s.uri}:${s.location})`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
logger.blank();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Run policy validation (will print violations via notify or JSON)
|
|
101
|
+
if (!noTests) {
|
|
102
|
+
const hasViolations = policy_1.policy.scanner(filtered, scope, isJson);
|
|
103
|
+
if (hasViolations) {
|
|
104
|
+
const err = new Error("Please fix them before continuing.");
|
|
105
|
+
err.name = "Policy violations found";
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (!isJson) {
|
|
110
|
+
logger.info(`Skipping policy validation (-no-tests).`);
|
|
111
|
+
}
|
|
112
|
+
// If we reach here, all validations passed
|
|
113
|
+
if (isJson) {
|
|
114
|
+
console.log(JSON.stringify({
|
|
115
|
+
valid: true,
|
|
116
|
+
error_count: 0,
|
|
117
|
+
warning_count: 0,
|
|
118
|
+
diagnostics: []
|
|
119
|
+
}, null, 2));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
logger.success('Success! The configuration is valid.', { bold: true });
|
|
123
|
+
logger.blank();
|
|
124
|
+
logger.info(` Scenarios: ${filtered.length}`);
|
|
125
|
+
logger.info(` Scope: ${scope}`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
exports.validateCmd = validateCmd;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.workspaceCmd = void 0;
|
|
4
|
+
const chalk_1 = require("chalk");
|
|
5
|
+
const logger_1 = require("../logger");
|
|
6
|
+
const state_1 = require("../core/state");
|
|
7
|
+
const const_1 = require("../const");
|
|
8
|
+
const workspaceCmd = async (options) => {
|
|
9
|
+
const { dir, verbose, args } = options;
|
|
10
|
+
const logger = new logger_1.Logger(verbose);
|
|
11
|
+
const stateObj = new state_1.State(dir);
|
|
12
|
+
const subcommand = args[0];
|
|
13
|
+
const name = args[1];
|
|
14
|
+
if (!subcommand) {
|
|
15
|
+
logger.error(`Usage: ${const_1.TITLE_CLI} workspace [subcommand] [options] [args]`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
await stateObj.init();
|
|
19
|
+
const currentWorkspace = stateObj.getCurrentWorkspace();
|
|
20
|
+
switch (subcommand) {
|
|
21
|
+
case 'show':
|
|
22
|
+
console.log(currentWorkspace);
|
|
23
|
+
break;
|
|
24
|
+
case 'list':
|
|
25
|
+
const workspaces = await stateObj.listWorkspaces();
|
|
26
|
+
for (const ws of workspaces) {
|
|
27
|
+
if (ws === currentWorkspace) {
|
|
28
|
+
console.log((0, chalk_1.green)(`* ${ws}`));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(` ${ws}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'new':
|
|
36
|
+
if (!name) {
|
|
37
|
+
logger.error('Expected a workspace name');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const existingForNew = await stateObj.listWorkspaces();
|
|
41
|
+
if (existingForNew.includes(name)) {
|
|
42
|
+
logger.error(`Workspace "${name}" already exists`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
stateObj.setCurrentWorkspace(name);
|
|
46
|
+
// Re-instantiate state to bind backend to the new workspace
|
|
47
|
+
const newStateObj = new state_1.State(dir);
|
|
48
|
+
await newStateObj.init();
|
|
49
|
+
newStateObj.clearResources();
|
|
50
|
+
await newStateObj.save();
|
|
51
|
+
console.log((0, chalk_1.green)(`Created and switched to workspace "${name}"!`));
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(`You're now on a new, empty workspace. Workspaces isolate their state,`);
|
|
54
|
+
console.log(`so if you run "${const_1.TITLE_CLI} plan" ${const_1.TITLE_CLI} will not see any existing state`);
|
|
55
|
+
console.log(`for this configuration.`);
|
|
56
|
+
break;
|
|
57
|
+
case 'select':
|
|
58
|
+
if (!name) {
|
|
59
|
+
logger.error('Expected a workspace name');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const existingForSelect = await stateObj.listWorkspaces();
|
|
63
|
+
if (!existingForSelect.includes(name)) {
|
|
64
|
+
logger.error(`Workspace "${name}" doesn't exist.\n\nYou can create this workspace with the "new" subcommand.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
stateObj.setCurrentWorkspace(name);
|
|
68
|
+
console.log((0, chalk_1.green)(`Switched to workspace "${name}".`));
|
|
69
|
+
break;
|
|
70
|
+
case 'delete':
|
|
71
|
+
if (!name) {
|
|
72
|
+
logger.error('Expected a workspace name');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (name === 'default') {
|
|
76
|
+
logger.error(`Workspace "default" cannot be deleted.`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (name === currentWorkspace) {
|
|
80
|
+
logger.error(`Workspace "${name}" is your active workspace.\n\nYou cannot delete the currently active workspace. Please switch\nto another workspace and try again.`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const existingForDelete = await stateObj.listWorkspaces();
|
|
84
|
+
if (!existingForDelete.includes(name)) {
|
|
85
|
+
logger.error(`Workspace "${name}" doesn't exist.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const deleted = await stateObj.deleteWorkspace(name);
|
|
89
|
+
if (deleted) {
|
|
90
|
+
console.log((0, chalk_1.green)(`Deleted workspace "${name}"!`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
logger.error(`Failed to delete workspace "${name}".`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
logger.error(`Invalid workspace subcommand: ${subcommand}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
exports.workspaceCmd = workspaceCmd;
|
package/dist/const.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Core constants for the testform CLI.
|
|
4
|
+
*
|
|
5
|
+
* Centralizes all string literals, maps, and enumerations used across
|
|
6
|
+
* the application to avoid duplication and provide a single source of truth.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.MSG_APPROVE_ONLY_YES = exports.MSG_ACQUIRING_LOCK = exports.ERR_NO_INPUT_ALLOWED = exports.ERR_GITHUB_CONFIG_NOT_FOUND = exports.SCOPE_CONFIG = exports.SCOPE_RESOURCE_MAP = exports.FILE_STATE = exports.FILE_CONFIG = exports.VERSION_STATE = exports.VERSION_CONFIG = exports.VERSION_CLI = exports.TITLE_CLI = exports.TITLE_APP = void 0;
|
|
10
|
+
// ─── App Identity ─────────────────────────────────────────────────────────────
|
|
11
|
+
/** Human-readable application name used in output messages. */
|
|
12
|
+
exports.TITLE_APP = "Testform";
|
|
13
|
+
/** CLI binary name used in usage strings. */
|
|
14
|
+
exports.TITLE_CLI = "testform";
|
|
15
|
+
/** Current CLI semver version. Keep in sync with package.json. */
|
|
16
|
+
exports.VERSION_CLI = "1.0.0-beta";
|
|
17
|
+
/** Version token written into testform.json config files. */
|
|
18
|
+
exports.VERSION_CONFIG = "1.0";
|
|
19
|
+
/** Version token written into testform.state files. */
|
|
20
|
+
exports.VERSION_STATE = "1.0";
|
|
21
|
+
// ─── File Names ───────────────────────────────────────────────────────────────
|
|
22
|
+
/** Default configuration file name. */
|
|
23
|
+
exports.FILE_CONFIG = "testform.json";
|
|
24
|
+
/** Default state file name. */
|
|
25
|
+
exports.FILE_STATE = "testform.state";
|
|
26
|
+
// ─── Scope Maps ───────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Maps each scope name to its corresponding GitHub resource type string.
|
|
29
|
+
* Used to eliminate hardcoded resource type strings across commands.
|
|
30
|
+
*/
|
|
31
|
+
exports.SCOPE_RESOURCE_MAP = {
|
|
32
|
+
testcase: 'github_testcase',
|
|
33
|
+
testrun: 'github_testrun',
|
|
34
|
+
testplan: 'github_testplan',
|
|
35
|
+
testreport: 'github_testreport',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Maps each scope to the Gherkin tag used to identify feature files.
|
|
39
|
+
* A feature file with this tag (or the matching extension) belongs to the scope.
|
|
40
|
+
*/
|
|
41
|
+
const SCOPE_TAG_MAP = {
|
|
42
|
+
testcase: '@testcase',
|
|
43
|
+
testrun: '@testrun',
|
|
44
|
+
testplan: '@testplan',
|
|
45
|
+
testreport: '@testreport',
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Maps each scope to the file extension convention used to identify feature files.
|
|
49
|
+
* A file ending in this extension automatically belongs to the scope.
|
|
50
|
+
*/
|
|
51
|
+
const SCOPE_EXT_MAP = {
|
|
52
|
+
testcase: '.case.feature',
|
|
53
|
+
testrun: '.run.feature',
|
|
54
|
+
testplan: '.plan.feature',
|
|
55
|
+
testreport: '.report.feature',
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Full scope configuration map combining resource type, tag and extension.
|
|
59
|
+
* Exported for use in plan, parser and other modules that need all three values.
|
|
60
|
+
*/
|
|
61
|
+
exports.SCOPE_CONFIG = {
|
|
62
|
+
testcase: {
|
|
63
|
+
tag: SCOPE_TAG_MAP.testcase,
|
|
64
|
+
ext: SCOPE_EXT_MAP.testcase,
|
|
65
|
+
resource: exports.SCOPE_RESOURCE_MAP.testcase,
|
|
66
|
+
},
|
|
67
|
+
testrun: {
|
|
68
|
+
tag: SCOPE_TAG_MAP.testrun,
|
|
69
|
+
ext: SCOPE_EXT_MAP.testrun,
|
|
70
|
+
resource: exports.SCOPE_RESOURCE_MAP.testrun,
|
|
71
|
+
},
|
|
72
|
+
testplan: {
|
|
73
|
+
tag: SCOPE_TAG_MAP.testplan,
|
|
74
|
+
ext: SCOPE_EXT_MAP.testplan,
|
|
75
|
+
resource: exports.SCOPE_RESOURCE_MAP.testplan,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
// ─── Testcase Execution Statuses ──────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* All valid execution statuses for a test case result comment.
|
|
81
|
+
* Aligned with Cucumber's built-in statuses plus common QA states.
|
|
82
|
+
*/
|
|
83
|
+
const TESTCASE_STATUSES = ['pending', 'passed', 'failed', 'blocked', 'skipped', 'undefined'];
|
|
84
|
+
/**
|
|
85
|
+
* Emoji representation for each testcase execution status.
|
|
86
|
+
* Used when rendering the status table inside testrun issue comments.
|
|
87
|
+
*/
|
|
88
|
+
const TESTCASE_STATUS_EMOJI = {
|
|
89
|
+
pending: '⏳',
|
|
90
|
+
passed: '✅',
|
|
91
|
+
failed: '❌',
|
|
92
|
+
blocked: '⚠️',
|
|
93
|
+
skipped: '⏭️',
|
|
94
|
+
undefined: '❓',
|
|
95
|
+
};
|
|
96
|
+
// ─── Error Messages ───────────────────────────────────────────────────────────
|
|
97
|
+
/** Error shown when testform.json has no "github" section. */
|
|
98
|
+
exports.ERR_GITHUB_CONFIG_NOT_FOUND = `GitHub configuration not found. Add a "github" section to your ${exports.FILE_CONFIG} with owner, repository, and tokenEnv.`;
|
|
99
|
+
/** Error shown when a command requires input but input is disabled. */
|
|
100
|
+
exports.ERR_NO_INPUT_ALLOWED = `This command requires manual approval, but input is disabled. Use the -auto-approve flag to bypass approval.`;
|
|
101
|
+
// ─── UI Messages ──────────────────────────────────────────────────────────────
|
|
102
|
+
/** Message displayed while acquiring the state lock before remote operations. */
|
|
103
|
+
exports.MSG_ACQUIRING_LOCK = 'Acquiring state lock. This may take a few moments...';
|
|
104
|
+
/** Approval prompt instructions shown to the user before applying changes. */
|
|
105
|
+
exports.MSG_APPROVE_ONLY_YES = `Only 'yes' will be accepted to approve.`;
|