@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,363 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerResource = exports.resource = void 0;
4
+ const const_1 = require("../const");
5
+ const notify_1 = require("../notify");
6
+ const chalk_1 = require("chalk");
7
+ const utils_1 = require("../core/utils");
8
+ class Resource {
9
+ builtinTemplates = Object.create(null);
10
+ userTemplates = Object.create(null);
11
+ resolve() {
12
+ const system = Object.values(this.builtinTemplates);
13
+ const user = Object.values(this.userTemplates);
14
+ return [...system, ...user];
15
+ }
16
+ get(type) {
17
+ const template = this.resolve().find(t => t.type === type);
18
+ if (!template) {
19
+ notify_1.notify.push({
20
+ type: 'error',
21
+ title: `Unknown resource type: "${type}"`,
22
+ detail: [
23
+ `No template is registered for resource type "${type}".`,
24
+ `Available types: ${this.resolve().map(t => t.type).join(', ') || '(none)'}`,
25
+ ],
26
+ close: true,
27
+ });
28
+ }
29
+ return template;
30
+ }
31
+ registry(template, type = 'system') {
32
+ if (type === 'system') {
33
+ this.builtinTemplates[template.type] = template;
34
+ }
35
+ if (type === 'user') {
36
+ this.userTemplates[template.type] = template;
37
+ }
38
+ }
39
+ evaluate(type, scenario, context) {
40
+ const template = this.get(type);
41
+ const result = {};
42
+ for (const field of template.fields) {
43
+ if (field.knownAfterApply)
44
+ continue;
45
+ result[field.name] = typeof field.value === 'function' ? field.value(scenario, context) : field.value;
46
+ }
47
+ return result;
48
+ }
49
+ getSymbol(type) {
50
+ const symbol = {
51
+ add: (0, chalk_1.green)('+'),
52
+ change: (0, chalk_1.yellow)('~'),
53
+ destroy: (0, chalk_1.red)('-'),
54
+ replace: `${(0, chalk_1.red)('-')}/${(0, chalk_1.green)('+')}`,
55
+ }[type];
56
+ return symbol;
57
+ }
58
+ getSymbols(changes) {
59
+ const actions = new Set(changes.map((c) => c.action));
60
+ const symbols = {
61
+ add: `${(0, chalk_1.green)('+')} create`,
62
+ change: `${(0, chalk_1.yellow)('~')} update in-place`,
63
+ destroy: `${(0, chalk_1.red)('-')} destroy`,
64
+ replace: `${(0, chalk_1.red)('-')}/${(0, chalk_1.green)('+')} destroy and then create replacement`,
65
+ };
66
+ const result = [];
67
+ if (actions.has('add'))
68
+ result.push(symbols.add);
69
+ if (actions.has('change'))
70
+ result.push(symbols.change);
71
+ if (actions.has('destroy'))
72
+ result.push(symbols.destroy);
73
+ if (actions.has('replace'))
74
+ result.push(symbols.replace);
75
+ return result.map(s => ` ${s}`).join('\n');
76
+ }
77
+ format(change, context) {
78
+ const { action, scenario, resourceType, identity, oldAttributes } = change;
79
+ const lines = [];
80
+ const template = this.get(resourceType);
81
+ const sym = this.getSymbol(action);
82
+ const pad = (name) => name.padEnd(16);
83
+ lines.push(` ${sym} resource "${resourceType}" "${(0, utils_1.formatIdentityDisplay)(identity)}" {`);
84
+ for (const field of template.fields) {
85
+ const raw = typeof field.value === 'function' ? field.value(scenario, context) : field.value;
86
+ const oldRaw = oldAttributes ? oldAttributes[field.name] : undefined;
87
+ let valueStr = '';
88
+ let fieldChanged = false;
89
+ if (field.knownAfterApply) {
90
+ valueStr = '(known after apply)';
91
+ fieldChanged = true;
92
+ }
93
+ else if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
94
+ let safeOldRaw = oldRaw;
95
+ if (!safeOldRaw || typeof safeOldRaw !== 'object' || Array.isArray(safeOldRaw)) {
96
+ safeOldRaw = action === 'destroy' ? oldRaw : {};
97
+ }
98
+ const isDiffAction = action === 'change' || action === 'add' || action === 'replace';
99
+ if (isDiffAction && safeOldRaw && typeof safeOldRaw === 'object' && !Array.isArray(safeOldRaw)) {
100
+ const allKeys = Array.from(new Set([...Object.keys(safeOldRaw), ...Object.keys(raw)])).sort();
101
+ const diffLines = [];
102
+ diffLines.push('{');
103
+ for (const k of allKeys) {
104
+ const oldVal = safeOldRaw[k];
105
+ const newVal = raw[k];
106
+ const kStr = `"${k}"`;
107
+ if (action === 'add') {
108
+ const formattedNewVal = (0, utils_1.formatHclValue)(newVal, 2).trimStart();
109
+ diffLines.push(` ${(0, chalk_1.green)('+')} ${kStr}: ${formattedNewVal}`);
110
+ fieldChanged = true;
111
+ }
112
+ else if (oldVal === undefined) {
113
+ const formattedNewVal = (0, utils_1.formatHclValue)(newVal, 2).trimStart();
114
+ diffLines.push(` ${(0, chalk_1.green)('+')} ${kStr}: ${formattedNewVal}`);
115
+ fieldChanged = true;
116
+ }
117
+ else if (newVal === undefined) {
118
+ const formattedOldVal = (0, utils_1.formatHclValue)(oldVal, 2).trimStart();
119
+ diffLines.push(` ${(0, chalk_1.red)('-')} ${kStr}: ${formattedOldVal}`);
120
+ fieldChanged = true;
121
+ }
122
+ else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
123
+ const formattedOldVal = (0, utils_1.formatHclValue)(oldVal, 2).trimStart();
124
+ const formattedNewVal = (0, utils_1.formatHclValue)(newVal, 2).trimStart();
125
+ diffLines.push(` ${(0, chalk_1.yellow)('~')} ${kStr}: ${formattedOldVal} -> ${formattedNewVal}`);
126
+ fieldChanged = true;
127
+ }
128
+ else {
129
+ const formattedOldVal = (0, utils_1.formatHclValue)(oldVal, 2).trimStart();
130
+ diffLines.push(` ${kStr}: ${formattedOldVal}`);
131
+ }
132
+ }
133
+ diffLines.push(' }');
134
+ valueStr = diffLines.join('\n');
135
+ }
136
+ else {
137
+ const formattedLines = (0, utils_1.formatHclValue)(raw, 2).split('\n');
138
+ valueStr = formattedLines.map((line, idx) => idx === 0 ? line.trimStart() : line).join('\n');
139
+ fieldChanged = true;
140
+ }
141
+ }
142
+ else if (Array.isArray(raw)) {
143
+ const formattedLines = (0, utils_1.formatHclValue)(raw, 2).split('\n');
144
+ valueStr = formattedLines.map((line, idx) => idx === 0 ? line.trimStart() : line).join('\n');
145
+ if (action === 'change') {
146
+ fieldChanged = JSON.stringify(raw) !== JSON.stringify(oldRaw);
147
+ }
148
+ else {
149
+ fieldChanged = true;
150
+ }
151
+ }
152
+ else if (typeof raw === 'string' && raw.includes('\n')) {
153
+ const formattedLines = (0, utils_1.formatHclValue)(raw, 2).split('\n');
154
+ valueStr = formattedLines.map((line, idx) => idx === 0 ? line.trimStart() : line).join('\n');
155
+ if (action === 'change') {
156
+ fieldChanged = raw !== oldRaw;
157
+ }
158
+ else {
159
+ fieldChanged = true;
160
+ }
161
+ }
162
+ else {
163
+ if (action === 'change' && raw !== oldRaw && oldRaw !== undefined) {
164
+ valueStr = `"${oldRaw}" -> "${raw}"`;
165
+ fieldChanged = true;
166
+ }
167
+ else {
168
+ valueStr = `"${raw}"`;
169
+ if (action === 'change') {
170
+ fieldChanged = raw !== oldRaw;
171
+ }
172
+ else {
173
+ fieldChanged = true;
174
+ }
175
+ }
176
+ }
177
+ if (action === 'add' || action === 'replace') {
178
+ lines.push(` ${sym} ${pad(field.name)}= ${valueStr}`);
179
+ }
180
+ else if (action === 'change') {
181
+ if (field.knownAfterApply) {
182
+ lines.push(` ${pad(field.name)}= ${valueStr}`);
183
+ }
184
+ else if (fieldChanged) {
185
+ lines.push(` ${sym} ${pad(field.name)}= ${valueStr}`);
186
+ }
187
+ else {
188
+ lines.push(` ${pad(field.name)}= ${valueStr}`);
189
+ }
190
+ }
191
+ else if (action === 'destroy') {
192
+ lines.push(` ${sym} ${pad(field.name)}= ${valueStr} -> null`);
193
+ }
194
+ }
195
+ lines.push(' }');
196
+ return lines.join('\n');
197
+ }
198
+ summary(changes, isPlanOnly = true, context) {
199
+ const output = [];
200
+ if (changes.length === 0) {
201
+ output.push('No changes. Your test matches the configuration.');
202
+ if (!isPlanOnly) {
203
+ output.push('');
204
+ output.push(`${const_1.TITLE_APP} has compared your real test against your configuration and found no differences, so no`);
205
+ output.push('changes are needed.');
206
+ output.push('');
207
+ output.push((0, chalk_1.green)('Apply complete! Resources: 0 added, 0 changed, 0 destroyed.'));
208
+ }
209
+ console.log(output.join('\n'));
210
+ return;
211
+ }
212
+ // Header
213
+ output.push(`${const_1.TITLE_APP} used the selected providers to generate the following execution plan. Resource actions are indicated with the`);
214
+ output.push('following symbols:');
215
+ output.push(this.getSymbols(changes));
216
+ output.push('');
217
+ // Changes grouped by type
218
+ output.push(`${const_1.TITLE_APP} will perform the following actions:`);
219
+ output.push('');
220
+ for (const change of changes) {
221
+ const shortIdentity = (0, utils_1.formatIdentityDisplay)(change.identity);
222
+ const actionText = change.action === 'add'
223
+ ? (0, chalk_1.bold)((0, chalk_1.green)(`# [${change.scenario.feature?.name || 'Unknown'}].${change.resourceType}.${shortIdentity} will be created`))
224
+ : change.action === 'change'
225
+ ? (0, chalk_1.bold)((0, chalk_1.yellow)(`# ${change.resourceType}.${shortIdentity} will be updated in-place`))
226
+ : change.action === 'replace'
227
+ ? (0, chalk_1.bold)((0, chalk_1.red)(`# ${change.resourceType}.${shortIdentity} must be replaced`))
228
+ : (0, chalk_1.bold)((0, chalk_1.red)(`# ${change.resourceType}.${shortIdentity} will be destroyed`));
229
+ output.push(` ${actionText}`);
230
+ // For destroy, add explanation comment
231
+ if (change.action === 'destroy') {
232
+ output.push(` # (because ${change.resourceType}.${shortIdentity} is not in configuration)`);
233
+ }
234
+ output.push(this.format(change, context));
235
+ output.push('');
236
+ }
237
+ // Summary
238
+ const toAdd = changes.filter((c) => c.action === 'add').length;
239
+ const toChange = changes.filter((c) => c.action === 'change').length;
240
+ const toDestroy = changes.filter((c) => c.action === 'destroy').length;
241
+ const toReplace = changes.filter((c) => c.action === 'replace').length;
242
+ output.push(`${(0, chalk_1.bold)('Plan:')} ${toAdd + toReplace} to add, ${toChange} to change, ${toDestroy + toReplace} to destroy.`);
243
+ output.push('');
244
+ // Note
245
+ if (isPlanOnly) {
246
+ // Separator
247
+ output.push((0, chalk_1.dim)('─'.repeat(108)));
248
+ output.push('');
249
+ if (!context?.outPath) {
250
+ output.push(`Note: You didn't use the -out option to save this plan, so ${const_1.TITLE_APP} can't guarantee to take exactly these`);
251
+ output.push(`actions if you run "${const_1.TITLE_CLI} apply" now.`);
252
+ }
253
+ }
254
+ console.log(output.join('\n'));
255
+ }
256
+ }
257
+ exports.resource = new Resource();
258
+ const registerResource = (template) => exports.resource.registry(template, 'user');
259
+ exports.registerResource = registerResource;
260
+ exports.resource.registry({
261
+ type: 'github_testcase',
262
+ fields: [
263
+ { name: 'title', value: (s) => s.name },
264
+ { name: 'body', value: (s) => '```gherkin\n' + s.steps.map((sp) => `${sp.keyword}${sp.text}`).join('\n') + '\n```' },
265
+ { name: 'labels', value: (s) => [...new Set([...(s.tags || [])].map(String).map(t => t.startsWith('@') ? t.substring(1) : t))] },
266
+ { name: 'assignees', value: (s) => (s.custom?.fields?.assignees || '').split(',').map((a) => a.trim()).filter(Boolean) },
267
+ { name: 'milestone', value: (s) => s.custom?.fields?.milestone || '' },
268
+ { name: 'custom_fields', value: (s) => {
269
+ const fields = { ...(s.custom?.fields || {}) };
270
+ delete fields['labels'];
271
+ delete fields['assignees'];
272
+ delete fields['milestone'];
273
+ return fields;
274
+ } }
275
+ ]
276
+ });
277
+ exports.resource.registry({
278
+ type: 'github_testrun',
279
+ fields: [
280
+ { name: 'title', value: (s) => s.feature?.name || '' },
281
+ { name: 'body', value: (s, context) => {
282
+ let body = s.feature.description || '';
283
+ const testcases = s.custom?.testcases || [];
284
+ if (testcases.length > 0 && context?.state) {
285
+ body += '\n\n### Test Cases\n';
286
+ const state = context.state;
287
+ for (const tc of testcases) {
288
+ const parts = tc.split('::');
289
+ const scenarioName = parts.pop();
290
+ const ruleName = parts.pop() || '';
291
+ const baseRule = ruleName.replace('.case.feature', '').replace('.feature', '');
292
+ const tcResources = state.getResources('github_testcase').filter((r) => r.identity.includes(baseRule) && (scenarioName === '*' || r.identity.endsWith(`::${scenarioName}`)));
293
+ if (tcResources.length > 0) {
294
+ for (const tcResource of tcResources) {
295
+ if (tcResource?.attributes?.issueNumber) {
296
+ body += `- [ ] #${tcResource.attributes.issueNumber}\n`;
297
+ }
298
+ else {
299
+ body += `- [ ] (known after apply) - ${tcResource.identity}\n`;
300
+ }
301
+ }
302
+ }
303
+ else {
304
+ body += `- [ ] (not found in state) - ${tc}\n`;
305
+ }
306
+ }
307
+ }
308
+ return body.trim();
309
+ } },
310
+ { name: 'labels', value: (s) => [...new Set([...(s.tags || [])].map(String).map(t => t.startsWith('@') ? t.substring(1) : t))] },
311
+ { name: 'assignees', value: (s) => (s.custom?.fields?.assignees || '').split(',').map((a) => a.trim()).filter(Boolean) },
312
+ { name: 'milestone', value: (s) => s.custom?.fields?.milestone || '' },
313
+ { name: 'custom_fields', value: (s) => {
314
+ const fields = { ...(s.custom?.fields || {}) };
315
+ delete fields['labels'];
316
+ delete fields['assignees'];
317
+ delete fields['milestone'];
318
+ return fields;
319
+ } }
320
+ ]
321
+ });
322
+ exports.resource.registry({
323
+ type: 'github_testplan',
324
+ fields: [
325
+ { name: 'title', value: (s) => s.feature?.name || '' },
326
+ { name: 'body', value: (s, context) => {
327
+ let body = s.feature.description || '';
328
+ const testruns = s.custom?.testruns || [];
329
+ if (testruns.length > 0 && context?.state) {
330
+ body += '\n\n### Test Runs\n';
331
+ const state = context.state;
332
+ for (const tr of testruns) {
333
+ const runResources = state.getResources('github_testrun').filter((r) => r.identity.endsWith(tr));
334
+ if (runResources.length > 0) {
335
+ for (const runResource of runResources) {
336
+ if (runResource?.attributes?.issueNumber) {
337
+ body += `- [ ] #${runResource.attributes.issueNumber} - ${runResource.attributes.title}\n`;
338
+ }
339
+ else {
340
+ body += `- [ ] (known after apply) - ${runResource.identity}\n`;
341
+ }
342
+ }
343
+ }
344
+ else {
345
+ body += `- [ ] (not found in state) - ${tr}\n`;
346
+ }
347
+ }
348
+ }
349
+ return body.trim();
350
+ } },
351
+ { name: 'labels', value: (s) => [...new Set([...(s.tags || [])].map(String).map(t => t.startsWith('@') ? t.substring(1) : t))] },
352
+ { name: 'assignees', value: (s) => (s.custom?.fields?.assignees || '').split(',').map((a) => a.trim()).filter(Boolean) },
353
+ { name: 'milestone', value: (s) => s.custom?.fields?.milestone || '' },
354
+ { name: 'custom_fields', value: (s) => {
355
+ const fields = { ...(s.custom?.fields || {}) };
356
+ delete fields['labels'];
357
+ delete fields['assignees'];
358
+ delete fields['milestone'];
359
+ return fields;
360
+ } },
361
+ { name: 'testruns', value: (s) => s.custom?.testruns || [] }
362
+ ]
363
+ });