@activemind/scd 1.4.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/LICENSE.md +35 -0
- package/README.md +417 -0
- package/bin/scd.js +140 -0
- package/lib/audit-report.js +93 -0
- package/lib/audit-sync.js +172 -0
- package/lib/audit.js +356 -0
- package/lib/cli-helpers.js +108 -0
- package/lib/commands/accept.js +28 -0
- package/lib/commands/audit.js +17 -0
- package/lib/commands/configure.js +200 -0
- package/lib/commands/doctor.js +14 -0
- package/lib/commands/exceptions.js +19 -0
- package/lib/commands/export-findings.js +46 -0
- package/lib/commands/findings.js +306 -0
- package/lib/commands/ignore.js +28 -0
- package/lib/commands/init.js +16 -0
- package/lib/commands/insights.js +24 -0
- package/lib/commands/install.js +15 -0
- package/lib/commands/list.js +109 -0
- package/lib/commands/remove.js +16 -0
- package/lib/commands/repo.js +862 -0
- package/lib/commands/report.js +234 -0
- package/lib/commands/resolve.js +25 -0
- package/lib/commands/rules.js +185 -0
- package/lib/commands/scan.js +519 -0
- package/lib/commands/scope.js +341 -0
- package/lib/commands/sync.js +40 -0
- package/lib/commands/uninstall.js +15 -0
- package/lib/commands/version.js +33 -0
- package/lib/comment-map.js +388 -0
- package/lib/config.js +325 -0
- package/lib/context-modifiers.js +211 -0
- package/lib/deep-analyzer.js +225 -0
- package/lib/doctor.js +236 -0
- package/lib/exception-manager.js +675 -0
- package/lib/export-findings.js +376 -0
- package/lib/file-context.js +380 -0
- package/lib/file-filter.js +204 -0
- package/lib/file-manifest.js +145 -0
- package/lib/git-utils.js +102 -0
- package/lib/global-config.js +239 -0
- package/lib/hooks-manager.js +130 -0
- package/lib/init-repo.js +147 -0
- package/lib/insights-analyzer.js +416 -0
- package/lib/insights-output.js +160 -0
- package/lib/installer.js +128 -0
- package/lib/output-constants.js +32 -0
- package/lib/output-terminal.js +407 -0
- package/lib/push-queue.js +322 -0
- package/lib/remove-repo.js +108 -0
- package/lib/repo-context.js +187 -0
- package/lib/report-html.js +1154 -0
- package/lib/report-index.js +157 -0
- package/lib/report-json.js +136 -0
- package/lib/report-markdown.js +250 -0
- package/lib/resolve-manager.js +148 -0
- package/lib/rule-registry.js +205 -0
- package/lib/scan-cache.js +171 -0
- package/lib/scan-context.js +312 -0
- package/lib/scan-schema.js +67 -0
- package/lib/scanner-full.js +681 -0
- package/lib/scanner-manual.js +348 -0
- package/lib/scanner-secrets.js +83 -0
- package/lib/scope.js +331 -0
- package/lib/store-verify.js +395 -0
- package/lib/store.js +310 -0
- package/lib/taint-register.js +196 -0
- package/lib/version-check.js +46 -0
- package/package.json +37 -0
- package/rules/rule-loader.js +324 -0
- package/rules/rules-aspx-cs.json +399 -0
- package/rules/rules-aspx.json +222 -0
- package/rules/rules-infra-leakage.json +434 -0
- package/rules/rules-js.json +664 -0
- package/rules/rules-php.json +521 -0
- package/rules/rules-python.json +466 -0
- package/rules/rules-secrets.json +99 -0
- package/rules/rules-sensitive-files.json +475 -0
- package/rules/rules-ts.json +76 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* insights-output.js
|
|
3
|
+
* Terminal-rendering of behavioural analysis from scd insights.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
const { RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, CYAN } = require('./output-constants');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function bar(value, max, width = 20, color = CYAN) {
|
|
11
|
+
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
12
|
+
return color + '█'.repeat(filled) + DIM + '░'.repeat(width - filled) + RESET;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pad(str, len) {
|
|
16
|
+
return String(str).slice(0, len).padEnd(len);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderInsights(analysis) {
|
|
20
|
+
if (analysis.empty) {
|
|
21
|
+
console.log(`\n${DIM} ℹ️ ${analysis.reason || 'No audit data found. Run scd scan to start collecting data.'}${RESET}\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { meta, recurringRules, avoidance, timePatterns,
|
|
26
|
+
knowledgeGaps, fileHotspots, trend, developers, signals } = analysis;
|
|
27
|
+
|
|
28
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
29
|
+
console.log(`\n${CYAN}${BOLD}╔═══════════════════════════════════════════════╗${RESET}`);
|
|
30
|
+
console.log(`${CYAN}${BOLD}║ Secure Code by Design – Behavior Insights ║${RESET}`);
|
|
31
|
+
console.log(`${CYAN}${BOLD}╚═══════════════════════════════════════════════╝${RESET}`);
|
|
32
|
+
console.log(` ${DIM}Period: ${meta.firstFinding} → ${meta.lastFinding} · ${meta.periodDays} days${RESET}`);
|
|
33
|
+
console.log(` ${DIM}Scans: ${meta.totalScans} · Findings: ${meta.totalFindings} · Developers: ${meta.uniqueDevelopers}${RESET}\n`);
|
|
34
|
+
|
|
35
|
+
// ── Signals (most important first) ──────────────────────────────────────────
|
|
36
|
+
if (signals && signals.length > 0) {
|
|
37
|
+
console.log(`${BOLD}Detected patterns${RESET}`);
|
|
38
|
+
console.log('─'.repeat(52));
|
|
39
|
+
for (const s of signals) {
|
|
40
|
+
console.log(`\n ${s.level} ${BOLD}${s.title}${RESET}`);
|
|
41
|
+
console.log(` ${DIM}${s.detail}${RESET}`);
|
|
42
|
+
console.log(` ${GREEN}→ ${s.action}${RESET}`);
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Trend ────────────────────────────────────────────────────────────────
|
|
48
|
+
if (trend.signal !== 'INSUFFICIENT_DATA' && trend.weeks?.length > 2) {
|
|
49
|
+
console.log(`${BOLD}Findings per week (trend)${RESET}`);
|
|
50
|
+
console.log('─'.repeat(52));
|
|
51
|
+
const maxVal = Math.max(...trend.weeks.map(w => w.total), 1);
|
|
52
|
+
for (const w of trend.weeks.slice(-8)) { // visa max 8 veckor
|
|
53
|
+
const critStr = w.critical > 0 ? ` ${RED}(${w.critical} CRIT)${RESET}` : '';
|
|
54
|
+
console.log(` ${DIM}${w.week}${RESET} ${bar(w.total, maxVal, 24)} ${String(w.total).padStart(3)}${critStr}`);
|
|
55
|
+
}
|
|
56
|
+
const trendSymbol = trend.signal === 'IMPROVING' ? `${GREEN}↓ Improving${RESET}`
|
|
57
|
+
: trend.signal === 'WORSENING' ? `${RED}↑ Worsening${RESET}`
|
|
58
|
+
: `${DIM}→ Stable${RESET}`;
|
|
59
|
+
console.log(`\n Trend: ${trendSymbol}${trend.trendPct ? ` ${DIM}(${trend.trendPct > 0 ? '+' : ''}${trend.trendPct}%)${RESET}` : ''}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Kunskapsgap ──────────────────────────────────────────────────────────
|
|
63
|
+
if (knowledgeGaps.gaps.length > 0) {
|
|
64
|
+
console.log(`${BOLD}Knowledge gaps – OWASP categories${RESET}`);
|
|
65
|
+
console.log('─'.repeat(52));
|
|
66
|
+
const maxCat = Math.max(...knowledgeGaps.gaps.map(g => g.count), 1);
|
|
67
|
+
for (const g of knowledgeGaps.gaps) {
|
|
68
|
+
const critStr = g.breakdown?.CRITICAL > 0 ? ` ${RED}${g.breakdown.CRITICAL}C${RESET}` : '';
|
|
69
|
+
const highStr = g.breakdown?.HIGH > 0 ? ` ${YELLOW}${g.breakdown.HIGH}H${RESET}` : '';
|
|
70
|
+
// Shorten category name for terminal
|
|
71
|
+
const shortCat = g.category.replace(' (OWASP A0\\d+)', '').replace(/\(OWASP .+?\)/, '').trim();
|
|
72
|
+
console.log(` ${pad(shortCat, 38)} ${bar(g.count, maxCat, 12)} ${String(g.count).padStart(3)} ${DIM}(${g.percent}%)${RESET}${critStr}${highStr}`);
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Recurring rules ───────────────────────────────────────────────────
|
|
78
|
+
if (recurringRules.length > 0) {
|
|
79
|
+
console.log(`${BOLD}Recurring rules${RESET}`);
|
|
80
|
+
console.log('─'.repeat(52));
|
|
81
|
+
const maxR = Math.max(...recurringRules.map(r => r.count), 1);
|
|
82
|
+
for (const r of recurringRules.slice(0, 6)) {
|
|
83
|
+
const sevColor = r.severity === 'CRITICAL' ? RED : r.severity === 'HIGH' ? YELLOW : DIM;
|
|
84
|
+
const span = r.spanDays > 0 ? ` ${DIM}(${r.spanDays} days)${RESET}` : '';
|
|
85
|
+
console.log(` ${sevColor}${pad(r.ruleId, 16)}${RESET} ${bar(r.count, maxR, 14)} ${String(r.count).padStart(3)} findings${span}`);
|
|
86
|
+
}
|
|
87
|
+
console.log();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── File hotspots ──────────────────────────────────────────────────────────
|
|
91
|
+
if (fileHotspots.hotspots.length > 0) {
|
|
92
|
+
console.log(`${BOLD}File hotspots${RESET}`);
|
|
93
|
+
console.log('─'.repeat(52));
|
|
94
|
+
const maxH = Math.max(...fileHotspots.hotspots.map(h => h.count), 1);
|
|
95
|
+
for (const h of fileHotspots.hotspots) {
|
|
96
|
+
const critStr = h.critical > 0 ? ` ${RED}${h.critical} CRIT${RESET}` : '';
|
|
97
|
+
// Show only filename + one level up for readability
|
|
98
|
+
const parts = h.file.replace(/\\/g, '/').split('/');
|
|
99
|
+
const shortFile = parts.length > 2 ? '…/' + parts.slice(-2).join('/') : h.file;
|
|
100
|
+
console.log(` ${pad(shortFile, 36)} ${bar(h.count, maxH, 10)} ${String(h.count).padStart(3)}${critStr}`);
|
|
101
|
+
}
|
|
102
|
+
console.log();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Time patterns ───────────────────────────────────────────────────────────
|
|
106
|
+
if (timePatterns.byHour) {
|
|
107
|
+
console.log(`${BOLD}Time patterns – findings per hour${RESET}`);
|
|
108
|
+
console.log('─'.repeat(52));
|
|
109
|
+
const maxHour = Math.max(...timePatterns.byHour, 1);
|
|
110
|
+
// Show 4-hour buckets for compact output
|
|
111
|
+
for (let h = 0; h < 24; h += 4) {
|
|
112
|
+
const bucket = timePatterns.byHour.slice(h, h + 4).reduce((a, b) => a + b, 0);
|
|
113
|
+
const isLate = h >= 20 || h < 4;
|
|
114
|
+
const label = `${String(h).padStart(2, '0')}–${String(h + 4).padStart(2, '0')}`;
|
|
115
|
+
const color = isLate && bucket > 0 ? YELLOW : DIM;
|
|
116
|
+
console.log(` ${color}${label}${RESET} ${bar(bucket, maxHour * 4, 20)} ${String(bucket).padStart(3)}`);
|
|
117
|
+
}
|
|
118
|
+
if (timePatterns.lateRate > 0) {
|
|
119
|
+
const lateColor = timePatterns.lateRate >= 20 ? YELLOW : DIM;
|
|
120
|
+
console.log(`\n Late-night findings: ${lateColor}${timePatterns.lateRate}%${RESET} ${DIM}(peak: ${timePatterns.peakHour}:00, ${timePatterns.peakWeekday})${RESET}`);
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Per-developer ─────────────────────────────────────────────────────────
|
|
126
|
+
if (developers.isTeam) {
|
|
127
|
+
console.log(`${BOLD}Per developer${RESET}`);
|
|
128
|
+
console.log('─'.repeat(52));
|
|
129
|
+
console.log(` ${DIM}${'Name'.padEnd(20)} ${'Findings'.padEnd(10)} ${'CRITICAL%'.padEnd(11)} Top category${RESET}`);
|
|
130
|
+
console.log(` ${'─'.repeat(20)} ${'─'.repeat(9)} ${'─'.repeat(10)} ${'─'.repeat(18)}`);
|
|
131
|
+
for (const d of developers.developers) {
|
|
132
|
+
const critColor = d.criticalRate >= 50 ? RED : d.criticalRate >= 25 ? YELLOW : GREEN;
|
|
133
|
+
const shortCat = (d.topCategory || '–').replace(/\s*\(OWASP.+?\)/, '').slice(0, 28);
|
|
134
|
+
console.log(` ${pad(d.name, 20)} ${String(d.total).padStart(8)} ${critColor}${String(d.criticalRate).padStart(6)}%${RESET} ${DIM}${shortCat}${RESET}`);
|
|
135
|
+
}
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Undvikandebeteende ────────────────────────────────────────────────────
|
|
140
|
+
if (avoidance.totalFindings > 0) {
|
|
141
|
+
console.log(`${BOLD}Exception handling${RESET}`);
|
|
142
|
+
console.log('─'.repeat(52));
|
|
143
|
+
const excColor = avoidance.signal === 'HIGH' ? RED : avoidance.signal === 'MEDIUM' ? YELLOW : GREEN;
|
|
144
|
+
console.log(` Excepted: ${excColor}${avoidance.excepted}${RESET} of ${avoidance.totalFindings} findings ${DIM}(${avoidance.exceptRate}%)${RESET}`);
|
|
145
|
+
if (avoidance.expiredExceptions > 0) {
|
|
146
|
+
console.log(` Expired: ${RED}${avoidance.expiredExceptions} exceptions have expired – action required${RESET}`);
|
|
147
|
+
}
|
|
148
|
+
if (avoidance.topApprovers.length > 0) {
|
|
149
|
+
const approvers = avoidance.topApprovers.map(a => `${a.key} (${a.count})`).join(', ');
|
|
150
|
+
console.log(` Approvers: ${DIM}${approvers}${RESET}`);
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Footer ────────────────────────────────────────────────────────────────
|
|
156
|
+
console.log(`${DIM}${'─'.repeat(52)}${RESET}`);
|
|
157
|
+
console.log(`${DIM} Run 'scd audit' for event log · 'scd report' for full report${RESET}\n`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { renderInsights };
|
package/lib/installer.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { RESET, BOLD, DIM, RED, GREEN, YELLOW, CYAN } = require('./output-constants');
|
|
2
|
+
/**
|
|
3
|
+
* installer.js
|
|
4
|
+
* Sets up global git hooks that point to the CLI agent.
|
|
5
|
+
*
|
|
6
|
+
* After install, ALL repos on this machine are protected automatically.
|
|
7
|
+
* Uses git config --global core.hooksPath
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const HOOKS_DIR = path.join(os.homedir(), '.scd', 'hooks');
|
|
16
|
+
const CLI_PATH = path.resolve(__dirname, '../bin/scd.js');
|
|
17
|
+
|
|
18
|
+
const PRE_COMMIT_HOOK = `#!/bin/sh
|
|
19
|
+
# Secure Code by Design – pre-commit hook
|
|
20
|
+
# Scans for secrets BEFORE they enter git history
|
|
21
|
+
# Installed by: scd install
|
|
22
|
+
|
|
23
|
+
node "${CLI_PATH}" scan --hook=pre-commit
|
|
24
|
+
exit $?
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const PRE_PUSH_HOOK = `#!/bin/sh
|
|
28
|
+
# Secure Code by Design – pre-push hook
|
|
29
|
+
# Full security scan before code leaves this machine
|
|
30
|
+
# Installed by: scd install
|
|
31
|
+
|
|
32
|
+
node "${CLI_PATH}" scan --hook=pre-push
|
|
33
|
+
exit $?
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
async function install() {
|
|
37
|
+
console.log('\nCYAN Secure Code by Design – InstallationRESET\n');
|
|
38
|
+
|
|
39
|
+
// 1. Create hooks directory
|
|
40
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
41
|
+
console.log(`${GREEN}✅ Hooks directory created:${RESET} ${HOOKS_DIR}`);
|
|
42
|
+
|
|
43
|
+
// 2. Write hook files
|
|
44
|
+
const preCommitPath = path.join(HOOKS_DIR, 'pre-commit');
|
|
45
|
+
const prePushPath = path.join(HOOKS_DIR, 'pre-push');
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(preCommitPath, PRE_COMMIT_HOOK, { mode: 0o755 });
|
|
48
|
+
fs.writeFileSync(prePushPath, PRE_PUSH_HOOK, { mode: 0o755 });
|
|
49
|
+
console.log(`${GREEN}✅ pre-commit hook installed${RESET} (secrets scanning)`);
|
|
50
|
+
console.log(`${GREEN}✅ pre-push hook installed${RESET} (full OWASP scan)`);
|
|
51
|
+
|
|
52
|
+
// 3. Configure git to use our hooks globally
|
|
53
|
+
try {
|
|
54
|
+
execSync(`git config --global core.hooksPath "${HOOKS_DIR}"`, { encoding: 'utf8' });
|
|
55
|
+
console.log(`${GREEN}✅ Git configuredRESET (core.hooksPath → ${HOOKS_DIR})`);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(RED + '❌ Kunde inte konfigurera git:' + RESET, err.message);
|
|
58
|
+
console.log(YELLOW + ' Run manually:' + RESET + ' git config --global core.hooksPath "' + HOOKS_DIR + '"');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log('\nGREENBOLD Installation complete!' + RESET);
|
|
62
|
+
console.log(DIM + ' All git repos on this machine are now protected.' + RESET);
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(DIM + ' By using scd you agree to the disclaimer at:' + RESET);
|
|
65
|
+
console.log(DIM + ' https://github.com/activemindsolutions/scd/blob/main/DISCLAIMER.md' + RESET);
|
|
66
|
+
console.log(DIM + ' scd is a static analysis aid — it does not replace penetration testing.' + RESET + '\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function uninstall() {
|
|
70
|
+
|
|
71
|
+
console.log(`\n${CYAN} Secure Code by Design – Uninstall${RESET}\n`);
|
|
72
|
+
|
|
73
|
+
let anyAction = false;
|
|
74
|
+
|
|
75
|
+
// 1. Remove hook files
|
|
76
|
+
const preCommitPath = path.join(HOOKS_DIR, 'pre-commit');
|
|
77
|
+
const prePushPath = path.join(HOOKS_DIR, 'pre-push');
|
|
78
|
+
let hooksRemoved = false;
|
|
79
|
+
for (const p of [preCommitPath, prePushPath]) {
|
|
80
|
+
if (fs.existsSync(p)) {
|
|
81
|
+
fs.unlinkSync(p);
|
|
82
|
+
hooksRemoved = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (hooksRemoved) {
|
|
86
|
+
console.log(`${GREEN}✅ Hook files removed${RESET}`);
|
|
87
|
+
anyAction = true;
|
|
88
|
+
} else {
|
|
89
|
+
console.log(`${DIM} No hook files found — skipping${RESET}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Remove hooks directory if empty
|
|
93
|
+
if (fs.existsSync(HOOKS_DIR)) {
|
|
94
|
+
const remaining = fs.readdirSync(HOOKS_DIR);
|
|
95
|
+
if (remaining.length === 0) {
|
|
96
|
+
fs.rmdirSync(HOOKS_DIR);
|
|
97
|
+
console.log(`${GREEN}✅ Hooks directory removed${RESET} (${HOOKS_DIR})`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(`${DIM} Hooks directory not empty — left in place${RESET} (${HOOKS_DIR})`);
|
|
100
|
+
}
|
|
101
|
+
anyAction = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3. Remove global git core.hooksPath
|
|
105
|
+
try {
|
|
106
|
+
const current = execSync('git config --global core.hooksPath', { encoding: 'utf8' }).trim();
|
|
107
|
+
if (current) {
|
|
108
|
+
execSync('git config --global --unset core.hooksPath', { encoding: 'utf8' });
|
|
109
|
+
console.log(`${GREEN}✅ Global git core.hooksPath removed${RESET}`);
|
|
110
|
+
anyAction = true;
|
|
111
|
+
}
|
|
112
|
+
} catch (_) {
|
|
113
|
+
// Not set — nothing to do
|
|
114
|
+
console.log(`${DIM} No global core.hooksPath configured — skipping${RESET}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 4. Leave ~/.scd/ store intact — user data (scans, audit log, exceptions)
|
|
118
|
+
console.log(`${DIM} ~/.scd/ store preserved — your scan history and exceptions are kept${RESET}`);
|
|
119
|
+
console.log(`${DIM} Remove manually with: rm -rf ~/.scd${RESET}`);
|
|
120
|
+
|
|
121
|
+
console.log(`\n${GREEN}${BOLD} Uninstall complete!${RESET}`);
|
|
122
|
+
if (anyAction) {
|
|
123
|
+
console.log(`${DIM} Git hooks are no longer active on this machine.${RESET}`);
|
|
124
|
+
console.log(`${DIM} Run ${RESET}scd install${DIM} to re-enable.${RESET}\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { install, uninstall };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* output-constants.js
|
|
4
|
+
* Terminal presentation constants — colours and symbols.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for all ANSI styling used in scd CLI output.
|
|
7
|
+
* Import in command and lib files:
|
|
8
|
+
* const { CYAN, GREEN, DIM, RESET, OK, WARN, FAIL } = require('../output-constants');
|
|
9
|
+
* (from lib/commands/: ../output-constants)
|
|
10
|
+
* (from lib/: ./output-constants)
|
|
11
|
+
*
|
|
12
|
+
* DIM uses \x1b[90m (bright black) — more widely supported than \x1b[2m.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Colours ────────────────────────────────────────────────────────────────
|
|
16
|
+
const RESET = '\x1b[0m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
const DIM = '\x1b[90m';
|
|
19
|
+
const RED = '\x1b[31m';
|
|
20
|
+
const GREEN = '\x1b[32m';
|
|
21
|
+
const YELLOW = '\x1b[33m';
|
|
22
|
+
const BLUE = '\x1b[34m';
|
|
23
|
+
const CYAN = '\x1b[36m';
|
|
24
|
+
|
|
25
|
+
// ── Symbols ────────────────────────────────────────────────────────────────
|
|
26
|
+
const OK = '✓'; // success
|
|
27
|
+
const FAIL = '✗'; // hard failure / error
|
|
28
|
+
const WARN = '⚠'; // warning
|
|
29
|
+
const DASH = '–'; // en dash — used for empty/none values
|
|
30
|
+
const SEP = '─'; // horizontal separator (repeat as needed: SEP.repeat(52))
|
|
31
|
+
|
|
32
|
+
module.exports = { RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, CYAN, OK, FAIL, WARN, DASH, SEP };
|