forged-cli 0.2.0 → 0.3.0

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/bin/forged.js CHANGED
@@ -44,9 +44,13 @@ if (command === 'readme') {
44
44
 
45
45
  if (command === 'scan') {
46
46
  const { scanCommand } = await import('../src/commands/scan.js');
47
+ const { resolve } = await import('path');
47
48
  const report = args.includes('--report') || args.includes('--report-md');
48
49
  const reportFormat = args.includes('--report-md') ? 'markdown' : 'json';
49
- await scanCommand(process.cwd(), { report, reportFormat });
50
+ const verbose = args.includes('--verbose') || args.includes('-v');
51
+ const pathArg = args.find(a => !a.startsWith('--'));
52
+ const targetPath = pathArg ? resolve(pathArg) : process.cwd();
53
+ await scanCommand(targetPath, { report, reportFormat, verbose });
50
54
  process.exit(0);
51
55
  }
52
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forged-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A complete dev environment toolkit — shell, workflows, and project scaffolding in one install.",
5
5
  "keywords": [
6
6
  "cli",
@@ -107,12 +107,12 @@ function saveMarkdownReport(reportPath, data) {
107
107
  }
108
108
 
109
109
  export async function scanCommand(cwd = process.cwd(), opts = {}) {
110
- const { report, reportFormat = 'json' } = opts;
110
+ const { report, reportFormat = 'json', verbose = false } = opts;
111
111
 
112
112
  console.log(`\n${bold}⚒ Forged Scanner${reset}`);
113
113
  console.log(` Scanning: ${cwd}\n`);
114
114
 
115
- const findings = { warnings: [], errors: [], info: [] };
115
+ const findings = { warnings: [], errors: [], info: [], suppressed: [] };
116
116
  let packagesVerified = 0;
117
117
 
118
118
  // 1. Parse package.json
@@ -127,7 +127,9 @@ export async function scanCommand(cwd = process.cwd(), opts = {}) {
127
127
  }
128
128
 
129
129
  // 3. Typosquat check
130
+ const popularSet = new Set(POPULAR_PACKAGES);
130
131
  for (const name of Object.keys(deps)) {
132
+ if (popularSet.has(name)) continue;
131
133
  for (const { known, distance } of isSuspiciousName(name, POPULAR_PACKAGES)) {
132
134
  findings.warnings.push({
133
135
  package: name,
@@ -153,7 +155,9 @@ export async function scanCommand(cwd = process.cwd(), opts = {}) {
153
155
  } else {
154
156
  packagesVerified = total || 0;
155
157
  for (const f of integrityFindings) {
156
- (f.type === 'error' ? findings.errors : findings.warnings).push(f);
158
+ if (f.type === 'error') findings.errors.push(f);
159
+ else if (f.type === 'info') findings.suppressed.push(f);
160
+ else findings.warnings.push(f);
157
161
  }
158
162
  if (total) findings.info.push(`Verified ${total} packages against npm registry`);
159
163
  }
@@ -192,12 +196,25 @@ export async function scanCommand(cwd = process.cwd(), opts = {}) {
192
196
  }
193
197
  }
194
198
 
199
+ // Verbose: show suppressed trusted rotations
200
+ if (verbose && findings.suppressed.length > 0) {
201
+ console.log(`${bold}SUPPRESSED — trusted publisher rotations:${reset}`);
202
+ for (const s of findings.suppressed) {
203
+ console.log(` ${green}~${reset} ${s.package}@${s.version}: ${s.message}`);
204
+ }
205
+ console.log();
206
+ }
207
+
195
208
  if (findings.info.length > 0) {
196
209
  for (const i of findings.info) console.log(` ${green}ℹ${reset} ${i}`);
197
- console.log();
198
210
  }
211
+ if (findings.suppressed.length > 0) {
212
+ const note = verbose ? '' : ' (run with --verbose to see them)';
213
+ console.log(` ${green}ℹ${reset} ${findings.suppressed.length} trusted publisher rotation(s) suppressed${note}`);
214
+ }
215
+ console.log();
199
216
 
200
- console.log(`${bold}Summary:${reset} ${findings.errors.length} error(s), ${findings.warnings.length} warning(s)\n`);
217
+ console.log(`${bold}Summary:${reset} ${findings.errors.length} error(s), ${findings.warnings.length} warning(s), ${findings.suppressed.length} suppressed\n`);
201
218
 
202
219
  // Save report if requested
203
220
  if (report) {
@@ -0,0 +1,27 @@
1
+ // Known npm core team — ownership rotates freely between these accounts
2
+ export const NPM_TEAM = new Set([
3
+ 'isaacs', 'gar', 'lukekarrys', 'nlf', 'zkat', 'ruyadorno',
4
+ 'npm-cli-ops', 'iarna', 'darcyclarke', 'wraithgar', 'fritzy',
5
+ 'GitHub Actions',
6
+ ]);
7
+
8
+ // Trusted community maintainers — well-known, long-standing accounts
9
+ export const TRUSTED_COMMUNITY = new Set([
10
+ 'sindresorhus', 'jbnicolai', 'jonschlinkert', 'doowb',
11
+ 'tjholowaychuk', 'dominicbarnes', 'loganfsmyth', 'eemeli',
12
+ 'styfle', 'leo', 'dsblv', 'ljharb', 'nicolo-ribaudo',
13
+ 'mysticatea', 'feross', 'mafintosh',
14
+ 'phated', 'nodejs-foundation', 'legendecas',
15
+ 'ulisesgascon', 'formidablelabs',
16
+ 'blakeembrey', 'wesleytodd', 'danez', 'domenic', 'panva', 'joshuakgoldberg',
17
+ 'matteo.collina', 'rvagg',
18
+ ]);
19
+
20
+ export function classifyPublisherChange(from, to) {
21
+ const fromTrusted = NPM_TEAM.has(from) || TRUSTED_COMMUNITY.has(from);
22
+ const toTrusted = NPM_TEAM.has(to) || TRUSTED_COMMUNITY.has(to);
23
+
24
+ if (toTrusted) return 'info'; // handed to a trusted publisher — suppress
25
+ if (fromTrusted) return 'warn'; // trusted → unknown — flag it
26
+ return 'warn'; // unknown → unknown — flag it
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { classifyPublisherChange } from './trustedPublishers.js';
3
4
 
4
5
  // Fetch registry metadata with a simple in-memory cache to avoid duplicate requests
5
6
  const registryCache = new Map();
@@ -108,12 +109,13 @@ export async function verifyTarballIntegrity(cwd, onProgress) {
108
109
  const versions = Object.keys(registryMeta.versions || {});
109
110
  const idx = versions.indexOf(meta.version);
110
111
  if (idx > 0) {
111
- const prevVersion = versions[idx - 1];
112
- const prevPublisher = registryMeta.versions[prevVersion]?._npmUser?.name;
113
- const currPublisher = versionData._npmUser?.name;
112
+ const prevVersion = versions[idx - 1];
113
+ const prevPublisher = registryMeta.versions[prevVersion]?._npmUser?.name;
114
+ const currPublisher = versionData._npmUser?.name;
114
115
  if (prevPublisher && currPublisher && prevPublisher !== currPublisher) {
116
+ const severity = classifyPublisherChange(prevPublisher, currPublisher);
115
117
  findings.push({
116
- type: 'warning',
118
+ type: severity,
117
119
  package: name,
118
120
  version: meta.version,
119
121
  message: `Publisher changed from "${prevPublisher}" to "${currPublisher}" in this version`,