@freshworks/shiftleft-tools 1.1.8
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/README.md +351 -0
- package/bin/shiftleft.js +95 -0
- package/package.json +57 -0
- package/src/commands/doctor.js +208 -0
- package/src/commands/init-postman.js +298 -0
- package/src/commands/init-rules.js +78 -0
- package/src/commands/link.js +172 -0
- package/src/commands/protect.js +61 -0
- package/src/commands/run-tests.js +182 -0
- package/src/commands/setup-pipeline.js +209 -0
- package/src/commands/update.js +203 -0
- package/src/index.js +4 -0
- package/src/utils/copy-tree.js +98 -0
- package/src/utils/gitignore.js +26 -0
- package/src/utils/logger.js +9 -0
- package/src/utils/manifest.js +145 -0
- package/src/utils/stack.js +80 -0
- package/src/utils/template.js +135 -0
- package/templates/AGENTS.md +109 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
- package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
- package/templates/postman/.husky/pre-commit +19 -0
- package/templates/postman/.prettierrc.json +5 -0
- package/templates/postman/README.md.ejs +147 -0
- package/templates/postman/collections/01-core.json.ejs +91 -0
- package/templates/postman/config/local.json.ejs +12 -0
- package/templates/postman/config/staging.json.ejs +26 -0
- package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
- package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
- package/templates/postman/gitignore +16 -0
- package/templates/postman/npmrc +31 -0
- package/templates/postman/package.json.ejs +66 -0
- package/templates/postman/run-all-shim.sh +16 -0
- package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
- package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
- package/templates/postman/scripts/infra/start-mocks.sh +138 -0
- package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
- package/templates/postman/scripts/lib/api_coverage.py +1122 -0
- package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
- package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
- package/templates/postman/scripts/lib/report_combined.py +527 -0
- package/templates/postman/scripts/lib/report_consolidated.py +363 -0
- package/templates/postman/scripts/lib/report_generator.py +121 -0
- package/templates/postman/scripts/lib/report_migration.py +156 -0
- package/templates/postman/scripts/lib/report_mutation.py +110 -0
- package/templates/postman/scripts/lib/report_unit.py +353 -0
- package/templates/postman/scripts/lib/report_utils.py +973 -0
- package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
- package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
- package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
- package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
- package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
- package/templates/postman/scripts/run-all.sh +452 -0
- package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
- package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
- package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
- package/templates/postman-node/README.md.ejs +26 -0
- package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
- package/templates/postman-node/config/local.json.ejs +46 -0
- package/templates/postman-node/config/staging.json.ejs +31 -0
- package/templates/postman-node/local.test.env.ejs +3 -0
- package/templates/postman-node/mocks/external.js +14 -0
- package/templates/postman-node/package.json.ejs +39 -0
- package/templates/postman-node/requirements.txt +1 -0
- package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
- package/templates/postman-node/scripts/database/run-migrations.js +29 -0
- package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
- package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
- package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
- package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
- package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
- package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
- package/templates/postman-node/scripts/lib/start-app.sh +48 -0
- package/templates/postman-node/scripts/lib/utils.sh +114 -0
- package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
- package/templates/postman-node/scripts/run-all.sh +303 -0
- package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
- package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
- package/templates/postman-node/stryker.config.js.ejs +51 -0
- package/templates/rules/local-test-setup.mdc +420 -0
- package/templates/rules/testing-node.mdc +66 -0
- package/templates/rules/testing.mdc +248 -0
- package/templates/skills/_shared/postman-standards.md +380 -0
- package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
- package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
- package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
- package/templates/skills/review-test-suite/SKILL-java.md +137 -0
- package/templates/skills/review-test-suite/SKILL-node.md +78 -0
- package/templates/skills/review-test-suite/SKILL.md +9 -0
- package/templates/skills/run-test-suite/SKILL-java.md +186 -0
- package/templates/skills/run-test-suite/SKILL-node.md +191 -0
- package/templates/skills/run-test-suite/SKILL.md +9 -0
- package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
- package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
- package/templates/skills/setup-api-tests/SKILL.md +9 -0
- package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
- package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
- package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
- package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
- package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
- package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
- package/templates/skills/write-api-tests/SKILL-java.md +115 -0
- package/templates/skills/write-api-tests/SKILL-node.md +83 -0
- package/templates/skills/write-api-tests/SKILL.md +9 -0
- package/templates/stryker.config.js +50 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { templateContent } from '../utils/template.js';
|
|
5
|
+
import { loadManifest, fileState, getToolVersion, getProtectedPaths } from '../utils/manifest.js';
|
|
6
|
+
import { detectStack, inferServiceName, buildContext } from '../utils/stack.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inspect a repo against its .shiftleft.json: is the tool version behind, are
|
|
11
|
+
* any managed files out of date / locally modified / missing, are symlinks
|
|
12
|
+
* (Phase C) intact. Read-only.
|
|
13
|
+
*
|
|
14
|
+
* --check exit 1 if the repo is stale, drifted, or has broken symlinks
|
|
15
|
+
* --json emit a machine-readable report instead of the formatted one
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Pure inspection: returns the drift report for a repo without printing.
|
|
19
|
+
*/
|
|
20
|
+
export function inspect(cwd) {
|
|
21
|
+
const manifest = loadManifest(cwd);
|
|
22
|
+
const installed = getToolVersion();
|
|
23
|
+
|
|
24
|
+
if (!manifest) {
|
|
25
|
+
return { initialized: false, installed, ok: true, files: [], symlinks: [] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const stack = manifest.stack || detectStack(cwd);
|
|
29
|
+
const context = buildContext(inferServiceName(cwd), stack);
|
|
30
|
+
|
|
31
|
+
const behind = manifest.toolVersion !== installed;
|
|
32
|
+
const files = classifyFiles(cwd, manifest, context);
|
|
33
|
+
const symlinks = classifySymlinks(cwd, manifest);
|
|
34
|
+
// A committed .claude/skills/ copy predates the plugin model (Phase C).
|
|
35
|
+
const claudeSkillsStale = existsSync(join(cwd, '.claude/skills'));
|
|
36
|
+
|
|
37
|
+
// A managed (non-scaffold) file that is out of date, drifted, or missing
|
|
38
|
+
// is a failure; scaffold files are repo-owned so their drift is expected.
|
|
39
|
+
const fileIssues = files.filter(
|
|
40
|
+
(f) => !f.scaffold && ['update-available', 'modified', 'missing'].includes(f.status)
|
|
41
|
+
);
|
|
42
|
+
const brokenLinks = symlinks.filter((s) => s.status === 'broken');
|
|
43
|
+
const ok = !behind && fileIssues.length === 0 && brokenLinks.length === 0;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
initialized: true,
|
|
47
|
+
installed,
|
|
48
|
+
repoVersion: manifest.toolVersion,
|
|
49
|
+
behind,
|
|
50
|
+
stack,
|
|
51
|
+
files,
|
|
52
|
+
symlinks,
|
|
53
|
+
protected: getProtectedPaths(manifest),
|
|
54
|
+
claudeSkillsStale,
|
|
55
|
+
fileIssues,
|
|
56
|
+
brokenLinks,
|
|
57
|
+
ok,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function doctor(options = {}) {
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const report = inspect(cwd);
|
|
64
|
+
|
|
65
|
+
if (!report.initialized) {
|
|
66
|
+
if (options.json) {
|
|
67
|
+
console.log(JSON.stringify({ initialized: false, installed: report.installed }, null, 2));
|
|
68
|
+
} else {
|
|
69
|
+
logger.info('This project is not managed by shiftleft-tools (no .shiftleft.json).');
|
|
70
|
+
logger.info('Run "shiftleft init-rules" or "shiftleft init-postman" to set it up.');
|
|
71
|
+
}
|
|
72
|
+
return; // not managed -> nothing to be stale; --check passes
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (options.json) {
|
|
76
|
+
const { fileIssues, brokenLinks, ...json } = report;
|
|
77
|
+
console.log(JSON.stringify(json, null, 2));
|
|
78
|
+
} else {
|
|
79
|
+
printReport(report);
|
|
80
|
+
if (report.claudeSkillsStale) {
|
|
81
|
+
logger.info('Found a committed .claude/skills/ — Claude Code skills now come from the shiftleft-tools plugin. You can delete it.');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.check && !report.ok) {
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function classifyFiles(cwd, manifest, context) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const [key, entry] of Object.entries(manifest.managed)) {
|
|
93
|
+
const scaffold = entry.scaffold === true;
|
|
94
|
+
|
|
95
|
+
if (!entry.template) {
|
|
96
|
+
// Pre-Phase-B entry: can only judge presence, not upstream changes.
|
|
97
|
+
out.push({ path: key, scaffold, status: existsSync(join(cwd, key)) ? 'tracked' : 'missing' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let incoming;
|
|
102
|
+
try {
|
|
103
|
+
incoming = templateContent(entry.template, context, entry.template.endsWith('.ejs'));
|
|
104
|
+
} catch {
|
|
105
|
+
// Template no longer ships in this package version.
|
|
106
|
+
out.push({ path: key, scaffold, status: 'orphaned' });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const state = fileState(cwd, key, manifest, incoming);
|
|
111
|
+
out.push({ path: key, scaffold, status: stateToStatus(state) });
|
|
112
|
+
}
|
|
113
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function stateToStatus(state) {
|
|
117
|
+
switch (state) {
|
|
118
|
+
case 'uptodate': return 'current';
|
|
119
|
+
case 'clean': return 'update-available';
|
|
120
|
+
case 'local-edit': return 'modified';
|
|
121
|
+
case 'missing': return 'missing';
|
|
122
|
+
default: return 'current'; // 'new' shouldn't occur for a recorded file
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function classifySymlinks(cwd, manifest) {
|
|
127
|
+
return (manifest.symlinks || []).map((link) => {
|
|
128
|
+
const abs = join(cwd, link);
|
|
129
|
+
let stat;
|
|
130
|
+
try {
|
|
131
|
+
stat = lstatSync(abs);
|
|
132
|
+
} catch {
|
|
133
|
+
// Absent entirely — symlinks are gitignored, so this is normal on a
|
|
134
|
+
// fresh clone. Not a failure; the fix is to run `shiftleft link`.
|
|
135
|
+
return { path: link, status: 'unlinked' };
|
|
136
|
+
}
|
|
137
|
+
if (!stat.isSymbolicLink()) return { path: link, status: 'broken' };
|
|
138
|
+
// existsSync follows the link; false => dangling target.
|
|
139
|
+
return { path: link, status: existsSync(abs) ? 'ok' : 'broken' };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const LABEL = {
|
|
144
|
+
current: chalk.green('current'),
|
|
145
|
+
'update-available': chalk.yellow('update available'),
|
|
146
|
+
modified: chalk.yellow('modified locally'),
|
|
147
|
+
missing: chalk.red('missing'),
|
|
148
|
+
orphaned: chalk.gray('not in this version'),
|
|
149
|
+
tracked: chalk.gray('tracked'),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function mark(status) {
|
|
153
|
+
if (status === 'current' || status === 'ok') return chalk.green('✓');
|
|
154
|
+
if (status === 'missing' || status === 'broken') return chalk.red('✗');
|
|
155
|
+
return chalk.yellow('⚠');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function printReport({ installed, repoVersion, stack, behind, files, symlinks, protected: protectedPaths = [], fileIssues, brokenLinks, ok }) {
|
|
159
|
+
console.log();
|
|
160
|
+
const versionLine = behind
|
|
161
|
+
? `${chalk.yellow('⚠ behind')} (installed ${installed}, repo ${repoVersion}) — run: shiftleft update`
|
|
162
|
+
: `${chalk.green('✓')} ${installed}`;
|
|
163
|
+
console.log(` ${chalk.bold('Tool:')} ${versionLine}`);
|
|
164
|
+
console.log(` ${chalk.bold('Stack:')} ${stack || 'unknown'}`);
|
|
165
|
+
|
|
166
|
+
console.log(`\n ${chalk.bold('Managed files:')}`);
|
|
167
|
+
for (const f of files) {
|
|
168
|
+
const label = f.scaffold && (f.status === 'update-available' || f.status === 'modified')
|
|
169
|
+
? chalk.gray(`${f.status === 'modified' ? 'modified' : 'differs'} (repo-owned)`)
|
|
170
|
+
: LABEL[f.status] || f.status;
|
|
171
|
+
const symbol = f.scaffold && (f.status === 'update-available' || f.status === 'modified')
|
|
172
|
+
? chalk.green('✓')
|
|
173
|
+
: mark(f.status);
|
|
174
|
+
console.log(` ${symbol} ${f.path} ${label}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (symlinks.length) {
|
|
178
|
+
console.log(`\n ${chalk.bold('Symlinks:')}`);
|
|
179
|
+
for (const s of symlinks) {
|
|
180
|
+
let label;
|
|
181
|
+
if (s.status === 'ok') label = chalk.green('ok');
|
|
182
|
+
else if (s.status === 'unlinked') label = chalk.yellow('not linked (run: shiftleft link)');
|
|
183
|
+
else label = chalk.red(s.status);
|
|
184
|
+
console.log(` ${mark(s.status)} ${s.path} ${label}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (protectedPaths.length) {
|
|
189
|
+
console.log(`\n ${chalk.bold('Protected (repo-owned, not staged):')}`);
|
|
190
|
+
for (const p of protectedPaths) console.log(` ${chalk.cyan('●')} postman/scripts/${p}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log();
|
|
194
|
+
if (ok) {
|
|
195
|
+
logger.success('Up to date.');
|
|
196
|
+
} else {
|
|
197
|
+
const parts = [];
|
|
198
|
+
if (behind) parts.push('tool version behind');
|
|
199
|
+
const upd = fileIssues.filter((f) => f.status === 'update-available').length;
|
|
200
|
+
const mod = fileIssues.filter((f) => f.status === 'modified').length;
|
|
201
|
+
const mis = fileIssues.filter((f) => f.status === 'missing').length;
|
|
202
|
+
if (upd) parts.push(`${upd} update(s) available`);
|
|
203
|
+
if (mod) parts.push(`${mod} modified`);
|
|
204
|
+
if (mis) parts.push(`${mis} missing`);
|
|
205
|
+
if (brokenLinks.length) parts.push(`${brokenLinks.length} broken symlink(s)`);
|
|
206
|
+
logger.warn(`${parts.join(', ')}. Run "shiftleft update".`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { copyTemplate, ensureDir } from '../utils/template.js';
|
|
6
|
+
import { copyTemplateTree } from '../utils/copy-tree.js';
|
|
7
|
+
import { detectStack, inferServiceName, buildContext } from '../utils/stack.js';
|
|
8
|
+
import { loadManifest, createManifest, saveManifest, recordFile } from '../utils/manifest.js';
|
|
9
|
+
import { stageScripts } from './run-tests.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize Postman/Newman test infrastructure
|
|
14
|
+
*/
|
|
15
|
+
export async function initPostman(options) {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
let stack = detectStack(cwd, options.stack);
|
|
18
|
+
|
|
19
|
+
if (!stack) {
|
|
20
|
+
const { chosen } = await inquirer.prompt([
|
|
21
|
+
{
|
|
22
|
+
type: 'list',
|
|
23
|
+
name: 'chosen',
|
|
24
|
+
message: 'Could not detect project type. Which stack is this?',
|
|
25
|
+
choices: [
|
|
26
|
+
{ name: 'Java / Spring Boot (pom.xml)', value: 'java' },
|
|
27
|
+
{ name: 'Node.js / Express (package.json)', value: 'node' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
stack = chosen;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let serviceName = options.name;
|
|
35
|
+
if (!serviceName) {
|
|
36
|
+
serviceName = inferServiceName(cwd);
|
|
37
|
+
if (!serviceName) {
|
|
38
|
+
const answers = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'input',
|
|
41
|
+
name: 'serviceName',
|
|
42
|
+
message: 'Enter service name:',
|
|
43
|
+
default: basename(cwd),
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
serviceName = answers.serviceName;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const includeStaging = options.withStaging || false;
|
|
51
|
+
const context = buildContext(serviceName, stack, includeStaging);
|
|
52
|
+
|
|
53
|
+
const spinner = ora(`Initializing Postman test infrastructure (${stack})...`).start();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const postmanDir = join(cwd, 'postman');
|
|
57
|
+
|
|
58
|
+
if (existsSync(postmanDir) && !options.force) {
|
|
59
|
+
spinner.warn('postman/ directory already exists. Use --force to overwrite.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
createDirectoryStructure(cwd, stack);
|
|
64
|
+
spinner.succeed('Created directory structure');
|
|
65
|
+
|
|
66
|
+
const manifest = loadManifest(cwd) || createManifest(stack);
|
|
67
|
+
manifest.stack = stack;
|
|
68
|
+
|
|
69
|
+
const copySpinner = ora('Copying template files...').start();
|
|
70
|
+
let copied = 0;
|
|
71
|
+
|
|
72
|
+
copied += copySharedPostmanFiles(cwd, stack, context, options.force, manifest);
|
|
73
|
+
copied += copyRunAllShim(cwd, context, options.force, manifest);
|
|
74
|
+
|
|
75
|
+
if (stack === 'node') {
|
|
76
|
+
copied += copyNodeScaffold(cwd, context, options.force, manifest);
|
|
77
|
+
copyRootNodeFiles(cwd, context, options.force, manifest);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
saveManifest(cwd, manifest);
|
|
81
|
+
|
|
82
|
+
copySpinner.succeed(`Copied ${copied} files`);
|
|
83
|
+
|
|
84
|
+
const stageSpinner = ora('Staging library scripts (gitignored cache)...').start();
|
|
85
|
+
const { staged } = stageScripts(cwd, { stack });
|
|
86
|
+
stageSpinner.succeed(`Staged ${staged} library scripts (refreshed automatically by shiftleft test)`);
|
|
87
|
+
|
|
88
|
+
await makeScriptsExecutable(cwd);
|
|
89
|
+
|
|
90
|
+
printNextSteps(stack, includeStaging);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
spinner.fail('Failed to initialize Postman infrastructure');
|
|
93
|
+
logger.error(error.message);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createDirectoryStructure(cwd, stack) {
|
|
99
|
+
const dirs = [
|
|
100
|
+
'postman',
|
|
101
|
+
'postman/.husky',
|
|
102
|
+
'postman/collections',
|
|
103
|
+
'postman/config',
|
|
104
|
+
'postman/environments',
|
|
105
|
+
'postman/reports',
|
|
106
|
+
'postman/scripts',
|
|
107
|
+
'postman/scripts/lib',
|
|
108
|
+
'postman/scripts/runners',
|
|
109
|
+
'postman/scripts/report-generators',
|
|
110
|
+
'postman/scripts/auth',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
if (stack === 'java') {
|
|
114
|
+
dirs.push('postman/scripts/infra', 'postman/scripts/wiremock', 'postman/scripts/wiremock/mappings');
|
|
115
|
+
} else {
|
|
116
|
+
dirs.push(
|
|
117
|
+
'postman/scripts/database',
|
|
118
|
+
'postman/mocks',
|
|
119
|
+
'postman/collections/crud',
|
|
120
|
+
'postman/seeders/global',
|
|
121
|
+
'postman/test_data',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const dir of dirs) {
|
|
126
|
+
ensureDir(join(cwd, dir));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function copySharedPostmanFiles(cwd, stack, context, force, manifest) {
|
|
131
|
+
let copied = 0;
|
|
132
|
+
const files = [
|
|
133
|
+
// Shipped under non-dot names: npm pack always strips .npmrc/.gitignore,
|
|
134
|
+
// and a nested .gitignore would also exclude the staged-script templates.
|
|
135
|
+
{ src: 'postman/npmrc', dest: 'postman/.npmrc' },
|
|
136
|
+
{ src: 'postman/gitignore', dest: 'postman/.gitignore' },
|
|
137
|
+
{ src: 'postman/.prettierrc.json', dest: 'postman/.prettierrc.json' },
|
|
138
|
+
{ src: 'postman/.husky/pre-commit', dest: 'postman/.husky/pre-commit' },
|
|
139
|
+
{
|
|
140
|
+
src: stack === 'node' ? 'postman-node/package.json.ejs' : 'postman/package.json.ejs',
|
|
141
|
+
dest: 'postman/package.json',
|
|
142
|
+
render: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
src: stack === 'node' ? 'postman-node/README.md.ejs' : 'postman/README.md.ejs',
|
|
146
|
+
dest: 'postman/README.md',
|
|
147
|
+
render: true,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
src: 'postman/environments/local.postman_environment.json.ejs',
|
|
151
|
+
dest: 'postman/environments/local.postman_environment.json',
|
|
152
|
+
render: true,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
src: stack === 'node' ? 'postman-node/config/local.json.ejs' : 'postman/config/local.json.ejs',
|
|
156
|
+
dest: 'postman/config/local.json',
|
|
157
|
+
render: true,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
if (context.includeStaging) {
|
|
162
|
+
files.push(
|
|
163
|
+
{
|
|
164
|
+
src: 'postman/environments/staging.postman_environment.json.ejs',
|
|
165
|
+
dest: 'postman/environments/staging.postman_environment.json',
|
|
166
|
+
render: true,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
src: stack === 'node' ? 'postman-node/config/staging.json.ejs' : 'postman/config/staging.json.ejs',
|
|
170
|
+
dest: 'postman/config/staging.json',
|
|
171
|
+
render: true,
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (stack === 'java') {
|
|
177
|
+
files.push({
|
|
178
|
+
src: 'postman/collections/01-core.json.ejs',
|
|
179
|
+
dest: 'postman/collections/01-core.json',
|
|
180
|
+
render: true,
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
files.push({
|
|
184
|
+
src: 'postman-node/collections/crud/01-bootstrap.json.ejs',
|
|
185
|
+
dest: 'postman/collections/crud/01-bootstrap.json',
|
|
186
|
+
render: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const destPath = join(cwd, file.dest);
|
|
192
|
+
try {
|
|
193
|
+
const result = copyTemplate(file.src, destPath, context, { render: file.render || false, force });
|
|
194
|
+
if (!result.skipped) {
|
|
195
|
+
recordFile(manifest, cwd, destPath, result.content, { scaffold: true, template: file.src });
|
|
196
|
+
}
|
|
197
|
+
copied++;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
logger.warn(`Could not copy ${file.src}: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return copied;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function copyRunAllShim(cwd, context, force, manifest) {
|
|
207
|
+
const dest = join(cwd, 'postman/scripts/run-all.sh');
|
|
208
|
+
const result = copyTemplate('postman/run-all-shim.sh', dest, context, { force });
|
|
209
|
+
if (!result.skipped) {
|
|
210
|
+
recordFile(manifest, cwd, dest, result.content, { template: 'postman/run-all-shim.sh' });
|
|
211
|
+
}
|
|
212
|
+
return result.skipped ? 0 : 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function copyNodeScaffold(cwd, context, force, manifest) {
|
|
216
|
+
let copied = 0;
|
|
217
|
+
|
|
218
|
+
// App-runtime mock orchestrator, customized per service — repo-owned.
|
|
219
|
+
const mocksEntry = copyTemplate('postman-node/scripts/setup-mocks.js.ejs',
|
|
220
|
+
join(cwd, 'postman/scripts/setup-mocks.js'), context, { render: true, force });
|
|
221
|
+
if (!mocksEntry.skipped) {
|
|
222
|
+
recordFile(manifest, cwd, join(cwd, 'postman/scripts/setup-mocks.js'), mocksEntry.content, {
|
|
223
|
+
scaffold: true,
|
|
224
|
+
template: 'postman-node/scripts/setup-mocks.js.ejs',
|
|
225
|
+
});
|
|
226
|
+
copied++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Mocks are a starter the repo customizes — record as scaffold (repo-owned).
|
|
230
|
+
copied += copyTemplateTree('postman-node/mocks', join(cwd, 'postman/mocks'), context, {
|
|
231
|
+
force,
|
|
232
|
+
manifest,
|
|
233
|
+
cwd,
|
|
234
|
+
scaffold: true,
|
|
235
|
+
}).copied;
|
|
236
|
+
|
|
237
|
+
return copied;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function copyRootNodeFiles(cwd, context, force, manifest) {
|
|
241
|
+
const reqs = copyTemplate('postman-node/requirements.txt', join(cwd, 'requirements.txt'), context, { force });
|
|
242
|
+
if (!reqs.skipped) recordFile(manifest, cwd, join(cwd, 'requirements.txt'), reqs.content, { scaffold: true, template: 'postman-node/requirements.txt' });
|
|
243
|
+
|
|
244
|
+
const env = copyTemplate('postman-node/local.test.env.ejs', join(cwd, 'postman/local.test.env'), context, {
|
|
245
|
+
render: true,
|
|
246
|
+
force,
|
|
247
|
+
});
|
|
248
|
+
if (!env.skipped) recordFile(manifest, cwd, join(cwd, 'postman/local.test.env'), env.content, { scaffold: true, template: 'postman-node/local.test.env.ejs' });
|
|
249
|
+
|
|
250
|
+
const strykerDest = join(cwd, 'stryker.config.js');
|
|
251
|
+
if (!existsSync(strykerDest) || force) {
|
|
252
|
+
const stryker = copyTemplate('postman-node/stryker.config.js.ejs', strykerDest, context, { render: true, force });
|
|
253
|
+
if (!stryker.skipped) recordFile(manifest, cwd, strykerDest, stryker.content, { scaffold: true, template: 'postman-node/stryker.config.js.ejs' });
|
|
254
|
+
logger.success('Created stryker.config.js (customize mutate paths for your src/)');
|
|
255
|
+
} else {
|
|
256
|
+
logger.skip('Skipped stryker.config.js (already exists)');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function makeScriptsExecutable(cwd) {
|
|
261
|
+
const makeExecutable = ora('Making scripts executable...').start();
|
|
262
|
+
const { execSync } = await import('child_process');
|
|
263
|
+
try {
|
|
264
|
+
execSync(`find ${join(cwd, 'postman/scripts')} -name "*.sh" -exec chmod +x {} \\;`, { stdio: 'ignore' });
|
|
265
|
+
execSync(`chmod +x ${join(cwd, 'postman/.husky/pre-commit')}`, { stdio: 'ignore' });
|
|
266
|
+
makeExecutable.succeed('Scripts are executable');
|
|
267
|
+
} catch {
|
|
268
|
+
makeExecutable.warn('Could not make scripts executable (run chmod +x manually)');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function printNextSteps(stack, includeStaging) {
|
|
273
|
+
console.log();
|
|
274
|
+
logger.success('Postman test infrastructure initialized!');
|
|
275
|
+
console.log();
|
|
276
|
+
logger.info('Next steps:');
|
|
277
|
+
console.log(' 1. cd postman && npm install');
|
|
278
|
+
console.log(' 2. Update postman/config/local.json with your API URL, port, and auth settings');
|
|
279
|
+
if (stack === 'node') {
|
|
280
|
+
console.log(' 3. Wire setup-mocks.js into your app entry when NODE_ENV=integration-tests');
|
|
281
|
+
console.log(' 4. Customize stryker.config.js mutate paths for your src/ layout');
|
|
282
|
+
console.log(' 5. Add collections under postman/collections/crud/');
|
|
283
|
+
console.log(' 6. Run: shiftleft test (or: postman/scripts/run-all.sh)');
|
|
284
|
+
} else {
|
|
285
|
+
console.log(' 3. Add your first test collection in postman/collections/');
|
|
286
|
+
console.log(' 4. Run: shiftleft test (or: postman/scripts/run-all.sh)');
|
|
287
|
+
}
|
|
288
|
+
console.log();
|
|
289
|
+
logger.info('Library scripts are staged from the shiftleft-tools package (gitignored) and refresh on every run.');
|
|
290
|
+
if (!includeStaging) {
|
|
291
|
+
console.log();
|
|
292
|
+
logger.info('Tip: Run "shiftleft init-postman --with-staging" to add staging support later.');
|
|
293
|
+
}
|
|
294
|
+
if (stack === 'node') {
|
|
295
|
+
console.log();
|
|
296
|
+
logger.info('Invoke /setup-api-tests in Cursor or Claude Code for guided Node Postman setup.');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { copyTemplate } from '../utils/template.js';
|
|
3
|
+
import { detectStack } from '../utils/stack.js';
|
|
4
|
+
import { loadManifest, createManifest, saveManifest, recordFile } from '../utils/manifest.js';
|
|
5
|
+
import { linkAssets } from './link.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initialize Cursor rules/skills and the agent guide files in the project.
|
|
10
|
+
*
|
|
11
|
+
* Cursor skills and rules are linked to the installed shiftleft-tools package
|
|
12
|
+
* (symlinks, gitignored) so they update with a single global install. Claude
|
|
13
|
+
* Code skills are provided by the shiftleft-tools plugin, not copied per repo.
|
|
14
|
+
* CLAUDE.md / AGENTS.md remain real, manifest-managed files.
|
|
15
|
+
*/
|
|
16
|
+
export async function initRules(options) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const stack = detectStack(cwd, options.stack);
|
|
19
|
+
const manifest = loadManifest(cwd) || createManifest(stack);
|
|
20
|
+
manifest.stack = manifest.stack || stack;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { linked, copied, symlinkMode } = linkAssets(cwd, {
|
|
24
|
+
stack,
|
|
25
|
+
skills: !options.skipSkills,
|
|
26
|
+
rules: true,
|
|
27
|
+
copy: options.copy,
|
|
28
|
+
manifest,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (symlinkMode) {
|
|
32
|
+
logger.success(`Linked ${linked.length} Cursor asset(s) to the shiftleft-tools package (symlinks, gitignored)`);
|
|
33
|
+
} else {
|
|
34
|
+
logger.success(`Copied ${copied.length} Cursor asset(s) (symlinks unavailable)`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
copyClaude(cwd, options, manifest);
|
|
38
|
+
copyAgents(cwd, options, manifest);
|
|
39
|
+
|
|
40
|
+
saveManifest(cwd, manifest);
|
|
41
|
+
|
|
42
|
+
console.log();
|
|
43
|
+
logger.info('Cursor rules and skills initialized.');
|
|
44
|
+
logger.info('Claude Code skills come from the shiftleft-tools plugin — install it once (see README), not per repo.');
|
|
45
|
+
logger.info('Run "shiftleft update --rules" to refresh links later.');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error(`Failed to initialize rules: ${error.message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Record a copyTemplate result in the manifest when it actually wrote a file. */
|
|
53
|
+
function record(manifest, cwd, templatePath, result) {
|
|
54
|
+
if (result && !result.skipped) {
|
|
55
|
+
recordFile(manifest, cwd, result.path, result.content, { template: templatePath });
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function copyClaude(cwd, options, manifest) {
|
|
61
|
+
const claudeMdDest = join(cwd, 'CLAUDE.md');
|
|
62
|
+
const result = record(manifest, cwd, 'CLAUDE.md', copyTemplate('CLAUDE.md', claudeMdDest, {}, { force: options.force }));
|
|
63
|
+
if (result.skipped) {
|
|
64
|
+
logger.skip('Skipped CLAUDE.md (already exists, use --force to overwrite)');
|
|
65
|
+
} else {
|
|
66
|
+
logger.success('Created CLAUDE.md');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function copyAgents(cwd, options, manifest) {
|
|
71
|
+
const agentsMdDest = join(cwd, 'AGENTS.md');
|
|
72
|
+
const result = record(manifest, cwd, 'AGENTS.md', copyTemplate('AGENTS.md', agentsMdDest, {}, { force: options.force }));
|
|
73
|
+
if (result.skipped) {
|
|
74
|
+
logger.skip('Skipped AGENTS.md (already exists, use --force to overwrite)');
|
|
75
|
+
} else {
|
|
76
|
+
logger.success('Created AGENTS.md');
|
|
77
|
+
}
|
|
78
|
+
}
|