@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,203 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { syncTemplate } from '../utils/template.js';
|
|
6
|
+
import {
|
|
7
|
+
loadManifest,
|
|
8
|
+
createManifest,
|
|
9
|
+
saveManifest,
|
|
10
|
+
getToolVersion,
|
|
11
|
+
} from '../utils/manifest.js';
|
|
12
|
+
import { detectStack, inferServiceName, buildContext } from '../utils/stack.js';
|
|
13
|
+
import { ensureGitignoreEntries } from '../utils/gitignore.js';
|
|
14
|
+
import { linkAssets } from './link.js';
|
|
15
|
+
import { stageScripts, STAGED_GITIGNORE_ENTRIES, STAGED_GITIGNORE_HEADER } from './run-tests.js';
|
|
16
|
+
import { logger } from '../utils/logger.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Update rules/skills/postman to latest version.
|
|
20
|
+
*
|
|
21
|
+
* Manifest-aware: branches on the three-way file state per file instead of
|
|
22
|
+
* blindly overwriting. Locally-edited files are never silently clobbered —
|
|
23
|
+
* a `<file>.shiftleft-new` is written beside them instead (unless --force).
|
|
24
|
+
*/
|
|
25
|
+
export async function update(options) {
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
|
|
28
|
+
// Determine what to update
|
|
29
|
+
const updateAll = !options.rules && !options.skills && !options.postman;
|
|
30
|
+
const updateRules = options.rules || updateAll;
|
|
31
|
+
const updateSkills = options.skills || updateAll;
|
|
32
|
+
const updatePostman = options.postman || updateAll;
|
|
33
|
+
|
|
34
|
+
// Confirm if not forcing
|
|
35
|
+
if (!options.force) {
|
|
36
|
+
const targets = [];
|
|
37
|
+
if (updateRules) targets.push('rules');
|
|
38
|
+
if (updateSkills) targets.push('skills');
|
|
39
|
+
if (updatePostman) targets.push('postman scripts');
|
|
40
|
+
|
|
41
|
+
const { confirm } = await inquirer.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: 'confirm',
|
|
44
|
+
name: 'confirm',
|
|
45
|
+
message: `Update ${targets.join(', ')} to the latest version? (locally-edited files are preserved; use --force to overwrite them.)`,
|
|
46
|
+
default: true,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
if (!confirm) {
|
|
51
|
+
logger.info('Update cancelled.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load (or adopt) the manifest. On a repo that has none, the first run
|
|
57
|
+
// seeds a baseline from current disk content so out-of-date files update
|
|
58
|
+
// cleanly rather than being flagged as conflicts.
|
|
59
|
+
let manifest = loadManifest(cwd);
|
|
60
|
+
const adopt = manifest === null;
|
|
61
|
+
if (adopt) {
|
|
62
|
+
manifest = createManifest(detectStack(cwd) || null);
|
|
63
|
+
logger.info('No .shiftleft.json found — adopting current files as the baseline.');
|
|
64
|
+
}
|
|
65
|
+
manifest.toolVersion = getToolVersion();
|
|
66
|
+
|
|
67
|
+
const totals = { copied: 0, skipped: 0, conflicts: 0 };
|
|
68
|
+
|
|
69
|
+
if (updateRules) {
|
|
70
|
+
accumulate(totals, await updateRulesFiles(cwd, manifest, options.force, adopt));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (updateSkills) {
|
|
74
|
+
accumulate(totals, await updateSkillsFiles(cwd, manifest));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (updatePostman) {
|
|
78
|
+
accumulate(totals, await updatePostmanScripts(cwd, manifest, options.force, adopt));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
saveManifest(cwd, manifest);
|
|
82
|
+
|
|
83
|
+
console.log();
|
|
84
|
+
logger.success(
|
|
85
|
+
`Update complete: ${totals.copied} updated, ${totals.skipped} unchanged, ${totals.conflicts} conflict(s).`
|
|
86
|
+
);
|
|
87
|
+
if (totals.conflicts > 0) {
|
|
88
|
+
logger.warn(
|
|
89
|
+
'Some files had local edits and were left untouched. Review the *.shiftleft-new files, '
|
|
90
|
+
+ 'then merge by hand or re-run with --force to overwrite.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function accumulate(totals, result) {
|
|
96
|
+
if (!result) return;
|
|
97
|
+
totals.copied += result.copied || 0;
|
|
98
|
+
totals.skipped += result.skipped || 0;
|
|
99
|
+
totals.conflicts += result.conflicts || 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function tally(result, status) {
|
|
103
|
+
if (status === 'conflict') result.conflicts++;
|
|
104
|
+
else if (status === 'uptodate' || status === 'skipped') result.skipped++;
|
|
105
|
+
else result.copied++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function updateRulesFiles(cwd, manifest, force, adopt) {
|
|
109
|
+
const spinner = ora('Updating rules...').start();
|
|
110
|
+
const result = { copied: 0, skipped: 0, conflicts: 0 };
|
|
111
|
+
|
|
112
|
+
// Cursor rules now resolve via symlinks to the installed package; refresh
|
|
113
|
+
// them rather than copying. Falls back to copies where symlinks are denied.
|
|
114
|
+
const { linked, copied } = linkAssets(cwd, {
|
|
115
|
+
stack: manifest.stack || detectStack(cwd),
|
|
116
|
+
skills: false,
|
|
117
|
+
rules: true,
|
|
118
|
+
manifest,
|
|
119
|
+
});
|
|
120
|
+
// Symlink refreshes are idempotent — count as unchanged, not updated.
|
|
121
|
+
result.skipped += linked.length;
|
|
122
|
+
result.copied += copied.length;
|
|
123
|
+
|
|
124
|
+
// CLAUDE.md and AGENTS.md remain real, manifest-managed files.
|
|
125
|
+
const claudeMdDest = join(cwd, 'CLAUDE.md');
|
|
126
|
+
if (existsSync(claudeMdDest)) {
|
|
127
|
+
tally(result, syncTemplate('CLAUDE.md', claudeMdDest, {}, { manifest, cwd, force, adopt }).status);
|
|
128
|
+
}
|
|
129
|
+
tally(result, syncTemplate('AGENTS.md', join(cwd, 'AGENTS.md'), {}, { manifest, cwd, force, adopt }).status);
|
|
130
|
+
|
|
131
|
+
spinner.succeed(`Rules: ${linked.length} linked, ${copied.length} copied, CLAUDE/AGENTS ${result.conflicts ? `${result.conflicts} conflict(s)` : 'in sync'}`);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function updateSkillsFiles(cwd, manifest) {
|
|
136
|
+
const spinner = ora('Updating skills...').start();
|
|
137
|
+
const result = { copied: 0, skipped: 0, conflicts: 0 };
|
|
138
|
+
|
|
139
|
+
// Cursor skills resolve via a symlink to the package; Claude Code skills
|
|
140
|
+
// come from the plugin. Refresh the Cursor symlink.
|
|
141
|
+
const { linked, copied } = linkAssets(cwd, {
|
|
142
|
+
stack: manifest.stack || detectStack(cwd),
|
|
143
|
+
skills: true,
|
|
144
|
+
rules: false,
|
|
145
|
+
manifest,
|
|
146
|
+
});
|
|
147
|
+
// Symlink refreshes are idempotent — count as unchanged, not updated.
|
|
148
|
+
result.skipped += linked.length;
|
|
149
|
+
result.copied += copied.length;
|
|
150
|
+
|
|
151
|
+
spinner.succeed(
|
|
152
|
+
linked.length
|
|
153
|
+
? 'Skills: Cursor symlink refreshed (Claude skills via plugin)'
|
|
154
|
+
: `Skills: ${copied.length} copied (symlinks unavailable; Claude skills via plugin)`
|
|
155
|
+
);
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function updatePostmanScripts(cwd, manifest, force, adopt) {
|
|
160
|
+
const spinner = ora('Updating postman scripts...').start();
|
|
161
|
+
const scriptsDir = join(cwd, 'postman/scripts');
|
|
162
|
+
const result = { copied: 0, skipped: 0, conflicts: 0 };
|
|
163
|
+
|
|
164
|
+
if (!existsSync(scriptsDir)) {
|
|
165
|
+
spinner.warn('No postman/scripts directory found. Run "shiftleft init-postman" first.');
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const stack = manifest.stack || detectStack(cwd);
|
|
170
|
+
const context = buildContext(inferServiceName(cwd), stack);
|
|
171
|
+
|
|
172
|
+
// The committed entrypoint becomes a thin shim; orchestration lives in the
|
|
173
|
+
// package. A locally-edited run-all.sh still gets conflict treatment.
|
|
174
|
+
tally(result, syncTemplate('postman/run-all-shim.sh', join(scriptsDir, 'run-all.sh'), context, {
|
|
175
|
+
manifest, cwd, force, adopt,
|
|
176
|
+
}).status);
|
|
177
|
+
|
|
178
|
+
// Library scripts are no longer vendored: drop their manifest entries.
|
|
179
|
+
// (The committed copies, if any, stay in git until the team removes them;
|
|
180
|
+
// staging refreshes the on-disk versions before every run.)
|
|
181
|
+
let pruned = 0;
|
|
182
|
+
for (const key of Object.keys(manifest.managed)) {
|
|
183
|
+
if (
|
|
184
|
+
key.startsWith('postman/scripts/') &&
|
|
185
|
+
key !== 'postman/scripts/run-all.sh' &&
|
|
186
|
+
key !== 'postman/scripts/setup-mocks.js'
|
|
187
|
+
) {
|
|
188
|
+
delete manifest.managed[key];
|
|
189
|
+
pruned++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ensureGitignoreEntries(join(cwd, 'postman/.gitignore'), STAGED_GITIGNORE_ENTRIES, STAGED_GITIGNORE_HEADER);
|
|
194
|
+
|
|
195
|
+
const { staged } = stageScripts(cwd, { stack });
|
|
196
|
+
|
|
197
|
+
spinner.succeed(
|
|
198
|
+
`Postman scripts: shim in sync, ${staged} library scripts staged from package`
|
|
199
|
+
+ (pruned ? `, ${pruned} legacy manifest entries pruned` : '')
|
|
200
|
+
+ (result.conflicts ? `, ${result.conflicts} conflict(s)` : '')
|
|
201
|
+
);
|
|
202
|
+
return result;
|
|
203
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { copyTemplate, syncTemplate, ensureDir, TEMPLATES_DIR } from './template.js';
|
|
4
|
+
import { recordFile } from './manifest.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recursively copy a template directory into a destination directory.
|
|
8
|
+
* Files ending in .ejs are rendered and written without the .ejs suffix.
|
|
9
|
+
*
|
|
10
|
+
* Modes:
|
|
11
|
+
* - default (no `manifest`): "create" semantics — skip existing unless `force`.
|
|
12
|
+
* - `sync: true` + `manifest`: manifest-aware three-way write per file.
|
|
13
|
+
* - `manifest` without `sync`: create semantics, but record each written file.
|
|
14
|
+
*
|
|
15
|
+
* Returns { copied, skipped, conflicts }.
|
|
16
|
+
*/
|
|
17
|
+
export function copyTemplateTree(relativeSrcDir, destBaseDir, context = {}, options = {}) {
|
|
18
|
+
const {
|
|
19
|
+
force = false,
|
|
20
|
+
excludeDirs = [],
|
|
21
|
+
excludeFiles = [],
|
|
22
|
+
excludePaths = [],
|
|
23
|
+
manifest = null,
|
|
24
|
+
cwd = null,
|
|
25
|
+
source,
|
|
26
|
+
scaffold = false,
|
|
27
|
+
sync = false,
|
|
28
|
+
adopt = false,
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
const srcRoot = join(TEMPLATES_DIR, relativeSrcDir);
|
|
32
|
+
if (!existsSync(srcRoot)) {
|
|
33
|
+
return { copied: 0, skipped: 0, conflicts: 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let copied = 0;
|
|
37
|
+
let skipped = 0;
|
|
38
|
+
let conflicts = 0;
|
|
39
|
+
|
|
40
|
+
function walk(currentSrc, currentDest) {
|
|
41
|
+
for (const entry of readdirSync(currentSrc)) {
|
|
42
|
+
if (excludeDirs.includes(entry)) continue;
|
|
43
|
+
|
|
44
|
+
const srcPath = join(currentSrc, entry);
|
|
45
|
+
const relPath = relative(srcRoot, srcPath).split('\\').join('/');
|
|
46
|
+
|
|
47
|
+
if (excludePaths.includes(relPath)) continue;
|
|
48
|
+
|
|
49
|
+
const isEjs = entry.endsWith('.ejs');
|
|
50
|
+
const destName = isEjs ? entry.replace(/\.ejs$/, '') : entry;
|
|
51
|
+
const destPath = join(currentDest, destName);
|
|
52
|
+
|
|
53
|
+
if (statSync(srcPath).isDirectory()) {
|
|
54
|
+
ensureDir(destPath);
|
|
55
|
+
walk(srcPath, destPath);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (excludeFiles.includes(entry)) continue;
|
|
60
|
+
|
|
61
|
+
const templateRel = join(relativeSrcDir, relPath).split('\\').join('/');
|
|
62
|
+
|
|
63
|
+
if (sync && manifest) {
|
|
64
|
+
const result = syncTemplate(templateRel, destPath, context, {
|
|
65
|
+
render: isEjs,
|
|
66
|
+
force,
|
|
67
|
+
manifest,
|
|
68
|
+
cwd,
|
|
69
|
+
source,
|
|
70
|
+
scaffold,
|
|
71
|
+
adopt,
|
|
72
|
+
});
|
|
73
|
+
if (result.status === 'conflict') conflicts++;
|
|
74
|
+
else if (result.status === 'uptodate' || result.status === 'skipped') skipped++;
|
|
75
|
+
else copied++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = copyTemplate(templateRel, destPath, context, {
|
|
80
|
+
force,
|
|
81
|
+
render: isEjs,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.skipped) {
|
|
85
|
+
skipped++;
|
|
86
|
+
} else {
|
|
87
|
+
copied++;
|
|
88
|
+
if (manifest) {
|
|
89
|
+
recordFile(manifest, cwd, destPath, result.content, { source, scaffold, template: templateRel });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ensureDir(destBaseDir);
|
|
96
|
+
walk(srcRoot, destBaseDir);
|
|
97
|
+
return { copied, skipped, conflicts };
|
|
98
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Append entries to a .gitignore under a labelled header, skipping any that
|
|
6
|
+
* are already present. Idempotent: the header is written at most once.
|
|
7
|
+
*/
|
|
8
|
+
export function ensureGitignoreEntries(gitignorePath, paths, header) {
|
|
9
|
+
if (!paths.length) return;
|
|
10
|
+
const content = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
11
|
+
const existing = new Set(content.split(/\r?\n/));
|
|
12
|
+
const toAdd = paths.filter((p) => !existing.has(p));
|
|
13
|
+
if (!toAdd.length) return;
|
|
14
|
+
|
|
15
|
+
let block = '';
|
|
16
|
+
if (header && !existing.has(header)) {
|
|
17
|
+
block += (content && !content.endsWith('\n') ? '\n' : '') + '\n' + header + '\n';
|
|
18
|
+
} else if (content && !content.endsWith('\n')) {
|
|
19
|
+
block += '\n';
|
|
20
|
+
}
|
|
21
|
+
block += toAdd.join('\n') + '\n';
|
|
22
|
+
|
|
23
|
+
const dir = dirname(gitignorePath);
|
|
24
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
25
|
+
appendFileSync(gitignorePath, block);
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg) => console.log(chalk.blue('info'), msg),
|
|
5
|
+
success: (msg) => console.log(chalk.green('✓'), msg),
|
|
6
|
+
warn: (msg) => console.log(chalk.yellow('⚠'), msg),
|
|
7
|
+
error: (msg) => console.log(chalk.red('✗'), msg),
|
|
8
|
+
skip: (msg) => console.log(chalk.gray('→'), chalk.gray(msg)),
|
|
9
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export const MANIFEST_NAME = '.shiftleft.json';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Version of the installed shiftleft-tools package.
|
|
13
|
+
*/
|
|
14
|
+
export function getToolVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
|
|
17
|
+
return pkg.version || '0.0.0';
|
|
18
|
+
} catch {
|
|
19
|
+
return '0.0.0';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function manifestPath(cwd) {
|
|
24
|
+
return join(cwd, MANIFEST_NAME);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a path to a repo-root-relative, forward-slash manifest key.
|
|
29
|
+
* Accepts either an absolute path or an already-repo-relative path.
|
|
30
|
+
*/
|
|
31
|
+
export function manifestKey(cwd, target) {
|
|
32
|
+
const rel = isAbsolute(target) ? relative(cwd, target) : target;
|
|
33
|
+
return (rel || target).split('\\').join('/').replace(/^\.\//, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hashContent(content) {
|
|
37
|
+
return 'sha256:' + createHash('sha256').update(content, 'utf8').digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadManifest(cwd) {
|
|
41
|
+
const p = manifestPath(cwd);
|
|
42
|
+
if (!existsSync(p)) return null;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
45
|
+
parsed.managed = parsed.managed || {};
|
|
46
|
+
parsed.symlinks = parsed.symlinks || [];
|
|
47
|
+
return parsed;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createManifest(stack) {
|
|
54
|
+
return {
|
|
55
|
+
toolVersion: getToolVersion(),
|
|
56
|
+
stack: stack || null,
|
|
57
|
+
managed: {},
|
|
58
|
+
symlinks: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function saveManifest(cwd, manifest) {
|
|
63
|
+
manifest.toolVersion = manifest.toolVersion || getToolVersion();
|
|
64
|
+
writeFileSync(manifestPath(cwd), JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Record the content the tool just wrote for a managed path.
|
|
69
|
+
* `relPath` may be absolute (resolved against cwd) or already repo-relative.
|
|
70
|
+
*/
|
|
71
|
+
export function recordFile(manifest, cwd, relPath, content, { source, scaffold = false, template } = {}) {
|
|
72
|
+
const key = manifestKey(cwd, relPath);
|
|
73
|
+
const entry = { hash: hashContent(content) };
|
|
74
|
+
entry.source = source || manifest.toolVersion || getToolVersion();
|
|
75
|
+
if (scaffold) entry.scaffold = true;
|
|
76
|
+
// Template source (relative to TEMPLATES_DIR) lets `doctor` recompute the
|
|
77
|
+
// upstream content for this file. Preserve a prior value if not supplied.
|
|
78
|
+
if (template) entry.template = template;
|
|
79
|
+
else if (manifest.managed[key]?.template) entry.template = manifest.managed[key].template;
|
|
80
|
+
manifest.managed[key] = entry;
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Three-way comparison of a managed file.
|
|
86
|
+
*
|
|
87
|
+
* current = hash of the file on disk (null if missing)
|
|
88
|
+
* recorded = hash the tool last wrote (from the manifest)
|
|
89
|
+
* incoming = hash of the new template output (null if not provided)
|
|
90
|
+
*
|
|
91
|
+
* Returns one of: 'missing' | 'uptodate' | 'new' | 'clean' | 'local-edit'.
|
|
92
|
+
*
|
|
93
|
+
* Note: a file that exists on disk, differs from `incoming`, and was never
|
|
94
|
+
* recorded is treated as 'local-edit' (a conflict) rather than 'new', so the
|
|
95
|
+
* tool never silently clobbers a file it did not write.
|
|
96
|
+
*/
|
|
97
|
+
export function fileState(cwd, relPath, manifest, incomingContent) {
|
|
98
|
+
const key = manifestKey(cwd, relPath);
|
|
99
|
+
const abs = join(cwd, key);
|
|
100
|
+
|
|
101
|
+
const recorded = manifest?.managed?.[key]?.hash ?? null;
|
|
102
|
+
const current = existsSync(abs) ? hashContent(readFileSync(abs, 'utf8')) : null;
|
|
103
|
+
const incoming = incomingContent != null ? hashContent(incomingContent) : null;
|
|
104
|
+
|
|
105
|
+
if (current === null) {
|
|
106
|
+
return recorded === null ? 'new' : 'missing';
|
|
107
|
+
}
|
|
108
|
+
if (incoming !== null && current === incoming) {
|
|
109
|
+
return 'uptodate';
|
|
110
|
+
}
|
|
111
|
+
if (recorded === null) {
|
|
112
|
+
// On disk, diverges from incoming, never managed by us -> don't clobber.
|
|
113
|
+
return 'local-edit';
|
|
114
|
+
}
|
|
115
|
+
if (current === recorded) {
|
|
116
|
+
return 'clean';
|
|
117
|
+
}
|
|
118
|
+
return 'local-edit';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isScaffold(manifest, cwd, relPath) {
|
|
122
|
+
const key = manifestKey(cwd, relPath);
|
|
123
|
+
return manifest?.managed?.[key]?.scaffold === true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Library-script paths (relative to postman/scripts/) the repo owns. stageScripts
|
|
128
|
+
* leaves these in place instead of overwriting them from the package — for repos
|
|
129
|
+
* that customize a library script (e.g. a service-specific runners/run-tests-local.sh).
|
|
130
|
+
*/
|
|
131
|
+
export function getProtectedPaths(manifest) {
|
|
132
|
+
return manifest && Array.isArray(manifest.protected) ? manifest.protected : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Normalize a user-supplied protected path to the postman/scripts-relative,
|
|
137
|
+
* forward-slash form stored in the manifest (and matched by stageScripts excludes).
|
|
138
|
+
*/
|
|
139
|
+
export function normalizeScriptPath(p) {
|
|
140
|
+
return p
|
|
141
|
+
.split('\\').join('/')
|
|
142
|
+
.replace(/^\.?\//, '')
|
|
143
|
+
.replace(/^postman\/scripts\//, '')
|
|
144
|
+
.replace(/\/+$/, '');
|
|
145
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Files in the base `postman/scripts` tree that the Node overlay
|
|
6
|
+
* (`postman-node/scripts`) replaces or that don't apply to Node. Excluded from
|
|
7
|
+
* the base copy so only the overlay writes them (avoids double-writes that make
|
|
8
|
+
* a clean repo perpetually report drift). Shared by init-postman and update.
|
|
9
|
+
*/
|
|
10
|
+
export const NODE_BASE_SCRIPT_EXCLUDES = [
|
|
11
|
+
'run-all.sh',
|
|
12
|
+
'runners/run-tests-local.sh',
|
|
13
|
+
'runners/run-tests-staging.sh',
|
|
14
|
+
'infra/start-mocks.sh',
|
|
15
|
+
'infra/stop-mocks.sh',
|
|
16
|
+
'report-generators/java-api-coverage-matrix.sh',
|
|
17
|
+
'report-generators/mutation-report.sh',
|
|
18
|
+
'report-generators/stage-report-artifacts.sh',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the EJS render context for a service. Shared by init-postman and
|
|
23
|
+
* update so that templates needing `serviceName` render consistently.
|
|
24
|
+
*/
|
|
25
|
+
export function buildContext(serviceName, stack, includeStaging = false) {
|
|
26
|
+
const name = serviceName || 'service';
|
|
27
|
+
return {
|
|
28
|
+
serviceName: name,
|
|
29
|
+
serviceDisplayName: name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
30
|
+
serviceNameUpper: name.toUpperCase().replace(/-/g, '_'),
|
|
31
|
+
serviceNameLower: name.toLowerCase().replace(/-/g, '_'),
|
|
32
|
+
includeStaging,
|
|
33
|
+
stack,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect project stack from cwd or explicit --stack option.
|
|
39
|
+
* @param {string} cwd
|
|
40
|
+
* @param {'java'|'node'|undefined} explicit
|
|
41
|
+
* @returns {'java'|'node'|null}
|
|
42
|
+
*/
|
|
43
|
+
export function detectStack(cwd, explicit) {
|
|
44
|
+
if (explicit === 'java' || explicit === 'node') {
|
|
45
|
+
return explicit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hasPom = existsSync(join(cwd, 'pom.xml'));
|
|
49
|
+
const hasPkg = existsSync(join(cwd, 'package.json'));
|
|
50
|
+
|
|
51
|
+
if (hasPom) return 'java';
|
|
52
|
+
if (hasPkg) return 'node';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Infer service name from pom.xml or package.json.
|
|
58
|
+
*/
|
|
59
|
+
export function inferServiceName(cwd) {
|
|
60
|
+
const pomPath = join(cwd, 'pom.xml');
|
|
61
|
+
if (existsSync(pomPath)) {
|
|
62
|
+
const pom = readFileSync(pomPath, 'utf8');
|
|
63
|
+
const match = pom.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
64
|
+
if (match) return match[1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pkgPath = join(cwd, 'package.json');
|
|
68
|
+
if (existsSync(pkgPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
71
|
+
if (pkg.name) {
|
|
72
|
+
return pkg.name.replace(/^@[^/]+\//, '').replace(/_/g, '-');
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore parse errors
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|