@fromeroc9/testform 1.0.2 → 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 +2 -2
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.S3Backend = void 0;
4
+ const client_s3_1 = require("@aws-sdk/client-s3");
5
+ const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
6
+ const crypto_1 = require("crypto");
7
+ const const_1 = require("../../const");
8
+ class S3Backend {
9
+ config;
10
+ workspace;
11
+ s3;
12
+ dynamo;
13
+ originalKey;
14
+ constructor(config, workspace = 'default') {
15
+ this.config = config;
16
+ this.workspace = workspace;
17
+ const clientConfig = { region: config.region || process.env.AWS_REGION || 'us-east-1' };
18
+ if (config.profile === 'local') {
19
+ clientConfig.endpoint = config.region && config.region.startsWith('http') ? config.region : 'http://localhost:4566';
20
+ clientConfig.region = process.env.AWS_REGION || 'us-east-1';
21
+ clientConfig.forcePathStyle = true;
22
+ clientConfig.credentials = {
23
+ accessKeyId: 'test',
24
+ secretAccessKey: 'test'
25
+ };
26
+ }
27
+ // Profiles are usually handled by the SDK automatically if AWS_PROFILE is set,
28
+ // or through credential providers, but for simplicity we rely on the default provider chain.
29
+ this.s3 = new client_s3_1.S3Client(clientConfig);
30
+ if (config.dynamodb_table) {
31
+ this.dynamo = new client_dynamodb_1.DynamoDBClient(clientConfig);
32
+ }
33
+ this.config.key = this.config.key || const_1.FILE_STATE;
34
+ this.originalKey = this.config.key;
35
+ if (this.workspace !== 'default') {
36
+ this.config.key = `env:/${this.workspace}/${this.originalKey}`;
37
+ }
38
+ }
39
+ async exists() {
40
+ try {
41
+ await this.s3.send(new client_s3_1.HeadObjectCommand({ Bucket: this.config.bucket, Key: this.config.key }));
42
+ return true;
43
+ }
44
+ catch (e) {
45
+ return false;
46
+ }
47
+ }
48
+ async read() {
49
+ try {
50
+ const command = new client_s3_1.GetObjectCommand({
51
+ Bucket: this.config.bucket,
52
+ Key: this.config.key,
53
+ });
54
+ const response = await this.s3.send(command);
55
+ const raw = await response.Body?.transformToString();
56
+ if (!raw) {
57
+ return this.emptyState();
58
+ }
59
+ return JSON.parse(raw);
60
+ }
61
+ catch (err) {
62
+ if (err instanceof client_s3_1.NoSuchKey || err.name === 'NoSuchKey' || err.Code === 'NoSuchKey') {
63
+ return this.emptyState();
64
+ }
65
+ throw err;
66
+ }
67
+ }
68
+ async write(state) {
69
+ const command = new client_s3_1.PutObjectCommand({
70
+ Bucket: this.config.bucket,
71
+ Key: this.config.key,
72
+ Body: JSON.stringify(state, null, 2),
73
+ ContentType: 'application/json',
74
+ });
75
+ await this.s3.send(command);
76
+ }
77
+ async lock(timeoutRaw) {
78
+ if (!this.dynamo || !this.config.dynamodb_table) {
79
+ // If no dynamodb table is configured, locking is bypassed (or purely optimistic).
80
+ return true;
81
+ }
82
+ const timeoutMatch = timeoutRaw.match(/^(\d+)s$/);
83
+ const timeoutMs = timeoutMatch ? parseInt(timeoutMatch[1], 10) * 1000 : 0;
84
+ const start = Date.now();
85
+ const lockId = (0, crypto_1.randomUUID)();
86
+ const item = {
87
+ LockID: { S: this.config.bucket + '/' + this.config.key },
88
+ Info: { S: JSON.stringify({
89
+ id: lockId,
90
+ operation: 'Operation',
91
+ who: process.env.USER || 'unknown',
92
+ created: new Date().toISOString()
93
+ }) }
94
+ };
95
+ while (true) {
96
+ try {
97
+ // Try to acquire lock
98
+ await this.dynamo.send(new client_dynamodb_1.PutItemCommand({
99
+ TableName: this.config.dynamodb_table,
100
+ Item: item,
101
+ ConditionExpression: 'attribute_not_exists(LockID)'
102
+ }));
103
+ return true;
104
+ }
105
+ catch (err) {
106
+ if (err instanceof client_dynamodb_1.ConditionalCheckFailedException || err.name === 'ConditionalCheckFailedException') {
107
+ if (Date.now() - start >= timeoutMs) {
108
+ return false;
109
+ }
110
+ await new Promise(r => setTimeout(r, 1000));
111
+ }
112
+ else {
113
+ throw err;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ async unlock() {
119
+ if (!this.dynamo || !this.config.dynamodb_table)
120
+ return true;
121
+ try {
122
+ await this.dynamo.send(new client_dynamodb_1.DeleteItemCommand({
123
+ TableName: this.config.dynamodb_table,
124
+ Key: { LockID: { S: this.config.bucket + '/' + this.config.key } }
125
+ }));
126
+ return true;
127
+ }
128
+ catch (e) {
129
+ return false;
130
+ }
131
+ }
132
+ async forceUnlock(lockId) {
133
+ if (!this.dynamo || !this.config.dynamodb_table) {
134
+ return { success: false, error: 'DynamoDB locking is not configured for this backend.' };
135
+ }
136
+ try {
137
+ const response = await this.dynamo.send(new client_dynamodb_1.GetItemCommand({
138
+ TableName: this.config.dynamodb_table,
139
+ Key: { LockID: { S: this.config.bucket + '/' + this.config.key } }
140
+ }));
141
+ if (!response.Item || !response.Item.Info) {
142
+ return { success: false, error: 'No lock exists for the given state.' };
143
+ }
144
+ const info = JSON.parse(response.Item.Info.S);
145
+ if (info.id !== lockId) {
146
+ return { success: false, currentLockId: info.id };
147
+ }
148
+ await this.dynamo.send(new client_dynamodb_1.DeleteItemCommand({
149
+ TableName: this.config.dynamodb_table,
150
+ Key: { LockID: { S: this.config.bucket + '/' + this.config.key } }
151
+ }));
152
+ return { success: true };
153
+ }
154
+ catch (e) {
155
+ return { success: false, error: `Failed to force unlock: ${e.message}` };
156
+ }
157
+ }
158
+ async isLocked() {
159
+ if (!this.dynamo || !this.config.dynamodb_table)
160
+ return false;
161
+ try {
162
+ const response = await this.dynamo.send(new client_dynamodb_1.GetItemCommand({
163
+ TableName: this.config.dynamodb_table,
164
+ Key: { LockID: { S: this.config.bucket + '/' + this.config.key } }
165
+ }));
166
+ return !!response.Item;
167
+ }
168
+ catch (e) {
169
+ return false;
170
+ }
171
+ }
172
+ emptyState() {
173
+ return {
174
+ version: const_1.VERSION_STATE,
175
+ serial: 0,
176
+ lineage: (0, crypto_1.randomUUID)(),
177
+ lastSync: '',
178
+ resources: [],
179
+ };
180
+ }
181
+ async listWorkspaces() {
182
+ const workspaces = new Set(['default']);
183
+ try {
184
+ const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
185
+ const command = new ListObjectsV2Command({
186
+ Bucket: this.config.bucket,
187
+ Prefix: 'env:/'
188
+ });
189
+ const response = await this.s3.send(command);
190
+ if (response.Contents) {
191
+ for (const item of response.Contents) {
192
+ if (item.Key) {
193
+ const match = item.Key.match(/^env:\/([^\/]+)\//);
194
+ if (match) {
195
+ workspaces.add(match[1]);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ catch (e) {
202
+ // Ignore error and just return what we have (fallback to local knowledge basically)
203
+ }
204
+ return Array.from(workspaces);
205
+ }
206
+ async deleteWorkspace(name) {
207
+ if (name === 'default')
208
+ return false;
209
+ try {
210
+ const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
211
+ const targetKey = `env:/${name}/${this.originalKey}`;
212
+ const command = new DeleteObjectCommand({
213
+ Bucket: this.config.bucket,
214
+ Key: targetKey
215
+ });
216
+ await this.s3.send(command);
217
+ return true;
218
+ }
219
+ catch (e) {
220
+ return false;
221
+ }
222
+ }
223
+ }
224
+ exports.S3Backend = S3Backend;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Shared command context factory.
4
+ *
5
+ * Centralizes the repetitive boilerplate of initializing State, acquiring
6
+ * the state lock, loading configuration, and constructing a GitHubAdapter.
7
+ * Every command that needs GitHub access should use `createCommandContext()`
8
+ * instead of repeating this pattern inline.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createCommandContext = createCommandContext;
12
+ const github_1 = require("../adapters/github");
13
+ const config_1 = require("./config");
14
+ const state_1 = require("./state");
15
+ const logger_1 = require("../logger");
16
+ const notify_1 = require("../notify");
17
+ const const_1 = require("../const");
18
+ /**
19
+ * Creates a fully initialized command context.
20
+ *
21
+ * Initializes the state file, acquires the lock, loads the testform.json
22
+ * configuration, and constructs an authenticated GitHubAdapter. Returns `null`
23
+ * (and emits an error notification) if GitHub configuration is missing unless
24
+ * `silent` is set.
25
+ *
26
+ * @param options - Context creation options.
27
+ * @returns Initialized context, or `null` if GitHub config is absent.
28
+ *
29
+ * @example
30
+ * const ctx = await createCommandContext({ dir, verbose, lock, lockTimeout });
31
+ * if (!ctx) return;
32
+ * try {
33
+ * await ctx.github.createIssue({ ... });
34
+ * } finally {
35
+ * await ctx.state.releaseLock();
36
+ * }
37
+ */
38
+ async function createCommandContext(options = {}) {
39
+ const { dir = '.', verbose = false, statePath, backupPath, lock = true, lockTimeout = '0s', silent = false, } = options;
40
+ const logger = new logger_1.Logger(verbose);
41
+ const config = new config_1.Config(dir);
42
+ const ghConfig = config.getGitHub();
43
+ if (!ghConfig) {
44
+ if (!silent) {
45
+ notify_1.notify.push({
46
+ type: 'error',
47
+ title: 'GitHub configuration not found',
48
+ detail: [const_1.ERR_GITHUB_CONFIG_NOT_FOUND],
49
+ close: true,
50
+ });
51
+ }
52
+ return null;
53
+ }
54
+ const state = new state_1.State(dir, statePath, backupPath);
55
+ await state.init();
56
+ await state.acquireLock(lock, lockTimeout);
57
+ const github = new github_1.GitHubAdapter(ghConfig);
58
+ return { state, github, config, logger };
59
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Config = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const const_1 = require("../const");
7
+ const notify_1 = require("../notify");
8
+ class Config {
9
+ dir;
10
+ config = {};
11
+ constructor(dir) {
12
+ this.dir = dir;
13
+ this.required();
14
+ }
15
+ resolve() {
16
+ let currentDir = (0, path_1.resolve)(this.dir);
17
+ const rootDir = (0, path_1.resolve)('/');
18
+ while (currentDir !== rootDir) {
19
+ const configPath = (0, path_1.resolve)(currentDir, const_1.FILE_CONFIG);
20
+ if ((0, fs_1.existsSync)(configPath)) {
21
+ return configPath;
22
+ }
23
+ const parentDir = (0, path_1.resolve)(currentDir, '..');
24
+ if (parentDir === currentDir)
25
+ break;
26
+ currentDir = parentDir;
27
+ }
28
+ const cwd = (0, path_1.resolve)(process.cwd(), const_1.FILE_CONFIG);
29
+ return (0, fs_1.existsSync)(cwd) ? cwd : null;
30
+ }
31
+ load(resolve) {
32
+ const raw = (0, fs_1.readFileSync)(resolve, "utf-8");
33
+ this.config = JSON.parse(raw);
34
+ return this.config;
35
+ }
36
+ getConfig() {
37
+ return this.config;
38
+ }
39
+ getGitHub() {
40
+ const github = this.config.github;
41
+ if (!github)
42
+ return undefined;
43
+ return {
44
+ owner: github.owner,
45
+ repository: github.repository,
46
+ projectId: github.projectId || github.project_id,
47
+ tokenEnv: github.tokenEnv || github.token_env || 'GITHUB_TOKEN'
48
+ };
49
+ }
50
+ getBackend() {
51
+ return this.config.backend;
52
+ }
53
+ getIdentity(scope) {
54
+ const scopeConf = this.config.scope && Object.prototype.hasOwnProperty.call(this.config.scope, scope) ? this.config.scope[scope] : undefined;
55
+ const id = scopeConf?.identity;
56
+ if (scope == "testcase")
57
+ return id ?? "tc-*";
58
+ if (scope == "testrun")
59
+ return id ?? "tr-*";
60
+ if (scope == "testplan")
61
+ return id ?? "tp-*";
62
+ }
63
+ getConvention(scope) {
64
+ const globalConf = this.config.scope && Object.prototype.hasOwnProperty.call(this.config.scope, 'global') ? this.config.scope['global'] : undefined;
65
+ const scopeConf = this.config.scope && Object.prototype.hasOwnProperty.call(this.config.scope, scope) ? this.config.scope[scope] : undefined;
66
+ // Merge convention: scope overrides global
67
+ if (!globalConf?.convention && !scopeConf?.convention)
68
+ return undefined;
69
+ return {
70
+ ...(globalConf?.convention || {}),
71
+ ...(scopeConf?.convention || {})
72
+ };
73
+ }
74
+ getReportMapping(key) {
75
+ return this.config.report_mapping ? this.config.report_mapping[key] : undefined;
76
+ }
77
+ getFields(scope) {
78
+ const globalConf = this.config.scope && Object.prototype.hasOwnProperty.call(this.config.scope, 'global') ? this.config.scope['global'] : undefined;
79
+ const globalFields = globalConf?.fields ?? [];
80
+ const scopeConf = this.config.scope && Object.prototype.hasOwnProperty.call(this.config.scope, scope) ? this.config.scope[scope] : undefined;
81
+ const scopeFields = scopeConf?.fields ?? [];
82
+ // Merge fields, giving precedence to scope-specific fields
83
+ const fieldMap = new Map();
84
+ for (const f of globalFields) {
85
+ if (f.name)
86
+ fieldMap.set(f.name.toLowerCase(), f);
87
+ }
88
+ for (const f of scopeFields) {
89
+ if (f.name)
90
+ fieldMap.set(f.name.toLowerCase(), f);
91
+ }
92
+ return Array.from(fieldMap.values());
93
+ }
94
+ required() {
95
+ const resolve = this.resolve();
96
+ if (!resolve) {
97
+ notify_1.notify.push({
98
+ type: 'error',
99
+ title: 'No configuration files',
100
+ detail: [
101
+ 'Plan requires configuration to be present. Planning without a configuration would mark everything for',
102
+ 'destruction, which is normally not what is desired. If you would like to destroy everything, run plan with the',
103
+ `-destroy option. Otherwise, create a ${const_1.TITLE_APP} configuration file (.json file) and try again.`
104
+ ],
105
+ close: true
106
+ });
107
+ }
108
+ const config = this.load(resolve);
109
+ const version = config.version ?? "";
110
+ if (version == null) {
111
+ notify_1.notify.push({
112
+ type: 'error',
113
+ title: `${const_1.FILE_CONFIG} is missing the "version" field`,
114
+ detail: [`Add a "version" field to your ${const_1.FILE_CONFIG} file and rerun init.`],
115
+ close: true
116
+ });
117
+ }
118
+ if (version !== const_1.VERSION_CONFIG) {
119
+ notify_1.notify.push({
120
+ type: 'error',
121
+ title: `configuration version mismatch`,
122
+ detail: [
123
+ `Found version "${this.config.version}", but expected "${const_1.VERSION_CONFIG}".`,
124
+ `Update ${const_1.FILE_CONFIG} to version ${const_1.VERSION_CONFIG} and rerun init.`,
125
+ ],
126
+ close: true
127
+ });
128
+ }
129
+ }
130
+ }
131
+ exports.Config = Config;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Credentials = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const os_1 = require("os");
7
+ class Credentials {
8
+ credsPath;
9
+ constructor() {
10
+ const home = (0, os_1.homedir)();
11
+ const configDir = (0, path_1.join)(home, '.testform.d');
12
+ if (!(0, fs_1.existsSync)(configDir)) {
13
+ (0, fs_1.mkdirSync)(configDir, { recursive: true });
14
+ }
15
+ this.credsPath = (0, path_1.join)(configDir, 'credentials.json');
16
+ }
17
+ load() {
18
+ if (!(0, fs_1.existsSync)(this.credsPath)) {
19
+ return { credentials: {} };
20
+ }
21
+ try {
22
+ const raw = (0, fs_1.readFileSync)(this.credsPath, 'utf8');
23
+ return JSON.parse(raw);
24
+ }
25
+ catch {
26
+ return { credentials: {} };
27
+ }
28
+ }
29
+ save(data) {
30
+ (0, fs_1.writeFileSync)(this.credsPath, JSON.stringify(data, null, 2), 'utf8');
31
+ }
32
+ getToken(hostname = 'github.com') {
33
+ const data = this.load();
34
+ return data.credentials?.[hostname]?.token;
35
+ }
36
+ setToken(hostname = 'github.com', token) {
37
+ const data = this.load();
38
+ if (!data.credentials)
39
+ data.credentials = {};
40
+ data.credentials[hostname] = { token };
41
+ this.save(data);
42
+ }
43
+ removeToken(hostname = 'github.com') {
44
+ const data = this.load();
45
+ if (data.credentials?.[hostname]) {
46
+ delete data.credentials[hostname];
47
+ this.save(data);
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ }
53
+ exports.Credentials = Credentials;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Parser facade.
4
+ *
5
+ * Provides a unified interface to the specialized Gherkin parsers.
6
+ * Delegates parsing and filtering logic based on the requested scope
7
+ * to maintain backward compatibility with existing commands.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.Parser = void 0;
11
+ const base_parser_1 = require("./parsers/base-parser");
12
+ const testcase_parser_1 = require("./parsers/testcase-parser");
13
+ const testrun_parser_1 = require("./parsers/testrun-parser");
14
+ const testplan_parser_1 = require("./parsers/testplan-parser");
15
+ /**
16
+ * Minimal concrete implementation of BaseParser to handle reading
17
+ * and formatting feature files before filtering.
18
+ */
19
+ class ReaderParser extends base_parser_1.BaseParser {
20
+ filter(scenarios) {
21
+ return scenarios;
22
+ }
23
+ }
24
+ class Parser {
25
+ dir;
26
+ variables;
27
+ reader;
28
+ constructor(dir, variables) {
29
+ this.dir = dir;
30
+ this.variables = variables;
31
+ this.reader = new ReaderParser(dir, variables);
32
+ }
33
+ /**
34
+ * Reads and parses all feature files in the directory.
35
+ * @returns Raw, un-filtered parser scenarios.
36
+ */
37
+ content() {
38
+ return this.reader.content();
39
+ }
40
+ /**
41
+ * Filters and enriches raw scenarios based on the given scope and DSL test config.
42
+ */
43
+ filter(scenarios, test, scope) {
44
+ let delegate;
45
+ switch (scope) {
46
+ case 'testcase':
47
+ delegate = new testcase_parser_1.TestcaseParser(this.dir, this.variables);
48
+ break;
49
+ case 'testrun':
50
+ delegate = new testrun_parser_1.TestrunParser(this.dir, this.variables);
51
+ break;
52
+ case 'testplan':
53
+ delegate = new testplan_parser_1.TestplanParser(this.dir, this.variables);
54
+ break;
55
+ default:
56
+ // Fallback to testcase parser logic for unknown scopes
57
+ delegate = new testcase_parser_1.TestcaseParser(this.dir, this.variables);
58
+ }
59
+ return delegate.filter(scenarios, test, scope);
60
+ }
61
+ }
62
+ exports.Parser = Parser;