@eduardbar/drift 1.2.0 → 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/.gga +50 -0
- package/.github/actions/drift-review/README.md +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +94 -9
- package/AGENTS.md +75 -245
- package/CHANGELOG.md +28 -0
- package/README.md +308 -51
- package/ROADMAP.md +6 -5
- package/dist/analyzer.d.ts +2 -2
- package/dist/analyzer.js +420 -159
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.js +204 -0
- package/dist/cli.js +693 -67
- package/dist/config.js +16 -2
- package/dist/diff.js +66 -10
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/git.js +12 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +6 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +13 -0
- package/dist/output-metadata.js +17 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +80 -28
- package/dist/printer.js +4 -0
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +4 -3
- package/dist/rules/comments.js +2 -2
- package/dist/rules/complexity.js +2 -7
- package/dist/rules/nesting.js +3 -13
- package/dist/rules/phase0-basic.js +10 -10
- package/dist/rules/phase3-configurable.js +23 -15
- package/dist/rules/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -82
- package/dist/saas.js +7 -320
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +7 -0
- package/dist/trust-kpi.js +185 -0
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +37 -0
- package/dist/trust.js +168 -0
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -211
- package/docs/PRD.md +187 -109
- package/docs/plugin-contract.md +61 -0
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +87 -0
- package/package.json +6 -3
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +266 -0
- package/src/cli.ts +840 -85
- package/src/config.ts +19 -2
- package/src/diff.ts +84 -10
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/git.ts +16 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +83 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +30 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +148 -27
- package/src/printer.ts +4 -0
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +6 -4
- package/src/rules/comments.ts +2 -2
- package/src/rules/complexity.ts +2 -7
- package/src/rules/nesting.ts +3 -13
- package/src/rules/phase0-basic.ts +11 -12
- package/src/rules/phase3-configurable.ts +39 -26
- package/src/rules/shared.ts +31 -3
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -433
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +210 -0
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +260 -0
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +78 -238
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/diff.test.ts +124 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +80 -1
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/plugins.test.ts +219 -0
- package/tests/rules.test.ts +23 -1
- package/tests/saas-foundation.test.ts +358 -1
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +147 -0
- package/tests/trust.test.ts +602 -0
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// drift-ignore-file
|
|
3
3
|
import { Command } from 'commander'
|
|
4
|
-
import { writeFileSync } from 'node:fs'
|
|
5
|
-
import { basename, resolve } from 'node:path'
|
|
4
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { basename, relative, resolve } from 'node:path'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import { createInterface } from 'node:readline/promises'
|
|
8
8
|
import { stdin as input, stdout as output } from 'node:process'
|
|
@@ -14,6 +14,7 @@ import { printConsole, printDiff } from './printer.js'
|
|
|
14
14
|
import { loadConfig } from './config.js'
|
|
15
15
|
import { extractFilesAtRef, cleanupTempDir } from './git.js'
|
|
16
16
|
import { computeDiff } from './diff.js'
|
|
17
|
+
import { runGuard } from './guard.js'
|
|
17
18
|
import { generateHtmlReport } from './report.js'
|
|
18
19
|
import { generateBadge } from './badge.js'
|
|
19
20
|
import { emitCIAnnotations, printCISummary } from './ci.js'
|
|
@@ -21,43 +22,261 @@ import { applyFixes, type FixResult } from './fix.js'
|
|
|
21
22
|
import { loadHistory, saveSnapshot, printHistory, printSnapshotDiff } from './snapshot.js'
|
|
22
23
|
import { generateReview } from './review.js'
|
|
23
24
|
import { generateArchitectureMap } from './map.js'
|
|
24
|
-
import {
|
|
25
|
-
|
|
25
|
+
import {
|
|
26
|
+
changeOrganizationPlan,
|
|
27
|
+
generateSaasDashboardHtml,
|
|
28
|
+
getOrganizationEffectiveLimits,
|
|
29
|
+
getOrganizationUsageSnapshot,
|
|
30
|
+
getSaasSummary,
|
|
31
|
+
ingestSnapshotFromReport,
|
|
32
|
+
listOrganizationPlanChanges,
|
|
33
|
+
} from './saas.js'
|
|
34
|
+
import {
|
|
35
|
+
buildTrustReport,
|
|
36
|
+
explainTrustGatePolicy,
|
|
37
|
+
formatTrustGatePolicyExplanation,
|
|
38
|
+
formatTrustJson,
|
|
39
|
+
renderTrustOutput,
|
|
40
|
+
shouldFailTrustGate,
|
|
41
|
+
normalizeMergeRiskLevel,
|
|
42
|
+
MERGE_RISK_ORDER,
|
|
43
|
+
detectBranchName,
|
|
44
|
+
} from './trust.js'
|
|
45
|
+
import { computeTrustKpis, formatTrustKpiConsole, formatTrustKpiJson } from './trust-kpi.js'
|
|
46
|
+
import { runBenchmarkCli } from './benchmark.js'
|
|
47
|
+
import { runInit, INIT_PRESETS } from './init.js'
|
|
48
|
+
import { runDoctor } from './doctor.js'
|
|
49
|
+
import { resolveOutputFormat } from './format.js'
|
|
50
|
+
import { toSarif, diffToSarif } from './sarif.js'
|
|
51
|
+
import type { DriftDiff, DriftTrustReport, DriftAnalysisOptions, MergeRiskLevel } from './types.js'
|
|
52
|
+
import type { GuardResult, GuardThresholds } from './guard-types.js'
|
|
53
|
+
import type { TrustGatePolicyExplanation } from './trust.js'
|
|
54
|
+
import type { SnapshotHistory } from './snapshot.js'
|
|
26
55
|
const program = new Command()
|
|
27
56
|
|
|
57
|
+
type ResourceOptionFlags = {
|
|
58
|
+
lowMemory?: boolean
|
|
59
|
+
chunkSize?: string
|
|
60
|
+
maxFiles?: string
|
|
61
|
+
maxFileSizeKb?: string
|
|
62
|
+
withSemanticDuplication?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseOptionalPositiveInt(rawValue: string | undefined, flagName: string): number | undefined {
|
|
66
|
+
if (rawValue == null) return undefined
|
|
67
|
+
const value = Number(rawValue)
|
|
68
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
69
|
+
throw new Error(`${flagName} must be a non-negative integer`)
|
|
70
|
+
}
|
|
71
|
+
return value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveAnalysisOptions(options: ResourceOptionFlags): DriftAnalysisOptions {
|
|
75
|
+
return {
|
|
76
|
+
lowMemory: options.lowMemory,
|
|
77
|
+
chunkSize: parseOptionalPositiveInt(options.chunkSize, '--chunk-size'),
|
|
78
|
+
maxFiles: parseOptionalPositiveInt(options.maxFiles, '--max-files'),
|
|
79
|
+
maxFileSizeKb: parseOptionalPositiveInt(options.maxFileSizeKb, '--max-file-size-kb'),
|
|
80
|
+
includeSemanticDuplication: options.withSemanticDuplication ? true : undefined,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addResourceOptions(command: Command): Command {
|
|
85
|
+
return command
|
|
86
|
+
.option('--low-memory', 'Reduce peak memory usage by chunking AST analysis')
|
|
87
|
+
.option('--chunk-size <n>', 'Files per chunk in low-memory mode (default: 40)')
|
|
88
|
+
.option('--max-files <n>', 'Maximum files to analyze before soft-skipping extras')
|
|
89
|
+
.option('--max-file-size-kb <n>', 'Skip files above this size and report diagnostics')
|
|
90
|
+
.option('--with-semantic-duplication', 'Keep semantic-duplication rule enabled in low-memory mode')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseOptionalNumber(rawValue: string | undefined, flagName: string): number | undefined {
|
|
94
|
+
if (rawValue == null) return undefined
|
|
95
|
+
const value = Number(rawValue)
|
|
96
|
+
if (!Number.isFinite(value)) {
|
|
97
|
+
throw new Error(`${flagName} must be a valid number`)
|
|
98
|
+
}
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseBySeverity(rawValue: string | undefined): GuardThresholds | undefined {
|
|
103
|
+
if (rawValue == null) return undefined
|
|
104
|
+
|
|
105
|
+
const spec = rawValue.trim()
|
|
106
|
+
if (!spec) {
|
|
107
|
+
throw new Error('--by-severity must not be empty. Expected format: error=0,warning=2,info=5')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const thresholds: GuardThresholds = {}
|
|
111
|
+
const seen = new Set<string>()
|
|
112
|
+
|
|
113
|
+
for (const segment of spec.split(',')) {
|
|
114
|
+
const pair = segment.trim()
|
|
115
|
+
if (!pair) continue
|
|
116
|
+
|
|
117
|
+
const equalIndex = pair.indexOf('=')
|
|
118
|
+
if (equalIndex <= 0 || equalIndex === pair.length - 1) {
|
|
119
|
+
throw new Error(`Invalid --by-severity entry '${pair}'. Expected key=value (e.g. warning=2).`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const key = pair.slice(0, equalIndex).trim().toLowerCase()
|
|
123
|
+
const rawThreshold = pair.slice(equalIndex + 1).trim()
|
|
124
|
+
|
|
125
|
+
if (key !== 'error' && key !== 'warning' && key !== 'info') {
|
|
126
|
+
throw new Error(`Invalid --by-severity key '${key}'. Allowed keys: error, warning, info.`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (seen.has(key)) {
|
|
130
|
+
throw new Error(`Duplicate --by-severity key '${key}'.`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const threshold = Number(rawThreshold)
|
|
134
|
+
if (!Number.isFinite(threshold)) {
|
|
135
|
+
throw new Error(`Invalid --by-severity value for '${key}': '${rawThreshold}'. Must be a valid number.`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const severityKey: keyof GuardThresholds = key
|
|
139
|
+
thresholds[severityKey] = threshold
|
|
140
|
+
seen.add(severityKey)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (seen.size === 0) {
|
|
144
|
+
throw new Error('--by-severity must include at least one threshold. Example: error=0,warning=2')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return thresholds
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatSigned(value: number): string {
|
|
151
|
+
return value > 0 ? `+${value}` : `${value}`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printGuardSummary(result: GuardResult): void {
|
|
155
|
+
const modeLabel = result.mode === 'diff' ? `diff (${result.baseRef ?? 'unknown base'})` : 'baseline'
|
|
156
|
+
const statusLabel = result.passed ? 'PASS' : 'FAIL'
|
|
157
|
+
|
|
158
|
+
process.stdout.write('\n')
|
|
159
|
+
process.stdout.write(`Guard mode: ${modeLabel}\n`)
|
|
160
|
+
process.stdout.write(`Result: ${statusLabel}\n`)
|
|
161
|
+
process.stdout.write(`Score delta: ${formatSigned(result.metrics.scoreDelta)}\n`)
|
|
162
|
+
process.stdout.write(`Total issues delta: ${formatSigned(result.metrics.totalIssuesDelta)}\n`)
|
|
163
|
+
process.stdout.write(
|
|
164
|
+
`Severity delta: error=${formatSigned(result.metrics.severityDelta.error)}, warning=${formatSigned(result.metrics.severityDelta.warning)}, info=${formatSigned(result.metrics.severityDelta.info)}\n`,
|
|
165
|
+
)
|
|
166
|
+
if (result.mode === 'baseline' && result.baselinePath) {
|
|
167
|
+
process.stdout.write(`Baseline file: ${result.baselinePath}\n`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (result.checks.length === 0) {
|
|
171
|
+
process.stdout.write('Checks: none configured\n')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process.stdout.write('Checks:\n')
|
|
176
|
+
for (const check of result.checks) {
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
` - [${check.passed ? 'PASS' : 'FAIL'}] ${check.id}: ${check.message} (actual=${check.actual}, limit=${check.limit})\n`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseTrustGateOverrides(options: { minTrust?: string; maxRisk?: string }): { minTrust?: number; maxRisk?: MergeRiskLevel } {
|
|
184
|
+
const cliMinTrust = options.minTrust ? Number(options.minTrust) : undefined
|
|
185
|
+
if (options.minTrust && Number.isNaN(cliMinTrust)) {
|
|
186
|
+
process.stderr.write('\n Error: --min-trust must be a valid number\n\n')
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let cliMaxRisk: MergeRiskLevel | undefined
|
|
191
|
+
if (options.maxRisk) {
|
|
192
|
+
cliMaxRisk = normalizeMergeRiskLevel(options.maxRisk)
|
|
193
|
+
if (!cliMaxRisk) {
|
|
194
|
+
process.stderr.write(`\n Error: --max-risk must be one of ${MERGE_RISK_ORDER.join(', ')}\n\n`)
|
|
195
|
+
process.exit(1)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
minTrust: typeof cliMinTrust === 'number' ? cliMinTrust : undefined,
|
|
201
|
+
maxRisk: cliMaxRisk,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveBranchFromOption(branch?: string): string | undefined {
|
|
206
|
+
const normalized = branch?.trim()
|
|
207
|
+
if (normalized) return normalized
|
|
208
|
+
return detectBranchName()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function printTrustGatePolicyDebug(explanation: TrustGatePolicyExplanation): void {
|
|
212
|
+
process.stderr.write(`${formatTrustGatePolicyExplanation(explanation)}\n`)
|
|
213
|
+
if (explanation.invalidPolicyPack) {
|
|
214
|
+
process.stderr.write(`Warning: policy pack '${explanation.invalidPolicyPack}' was not found. Falling back to base/preset policy.\n`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function printSaasErrorAndExit(error: unknown): never {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
220
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
221
|
+
process.exit(1)
|
|
222
|
+
}
|
|
223
|
+
|
|
28
224
|
program
|
|
29
225
|
.name('drift')
|
|
30
|
-
.description('
|
|
226
|
+
.description('AI Code Audit CLI for merge trust in AI-assisted PRs')
|
|
31
227
|
.version(VERSION)
|
|
32
228
|
|
|
33
|
-
|
|
34
|
-
|
|
229
|
+
addResourceOptions(
|
|
230
|
+
program
|
|
231
|
+
.command('scan [path]', { isDefault: true })
|
|
35
232
|
.description('Scan a directory for vibe coding drift')
|
|
36
233
|
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
234
|
+
.option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
|
|
37
235
|
.option('--json', 'Output raw JSON report')
|
|
38
236
|
.option('--ai', 'Output AI-optimized JSON for LLM consumption')
|
|
39
237
|
.option('--fix', 'Show fix suggestions for each issue')
|
|
40
238
|
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
41
|
-
.action(async (targetPath: string | undefined, options: { output?: string; json?: boolean; ai?: boolean; fix?: boolean; minScore: string }) => {
|
|
239
|
+
.action(async (targetPath: string | undefined, options: { output?: string; format?: string; json?: boolean; ai?: boolean; fix?: boolean; minScore: string } & ResourceOptionFlags) => {
|
|
42
240
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
43
241
|
|
|
44
242
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
45
243
|
const config = await loadConfig(resolvedPath)
|
|
46
|
-
const files = analyzeProject(resolvedPath, config)
|
|
244
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
47
245
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
48
246
|
const report = buildReport(resolvedPath, files)
|
|
49
247
|
|
|
50
|
-
|
|
248
|
+
const format = resolveOutputFormat({
|
|
249
|
+
command: 'scan',
|
|
250
|
+
format: options.format,
|
|
251
|
+
supported: ['console', 'json', 'markdown', 'ai', 'sarif'],
|
|
252
|
+
legacyAliases: [
|
|
253
|
+
{ flag: 'json', used: options.json, mapsTo: 'json' },
|
|
254
|
+
{ flag: 'ai', used: options.ai, mapsTo: 'ai' },
|
|
255
|
+
],
|
|
256
|
+
onWarning: (message) => process.stderr.write(`${message}\n`),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (format === 'sarif') {
|
|
260
|
+
process.stdout.write(`${JSON.stringify(toSarif(report), null, 2)}\n`)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (format === 'ai') {
|
|
51
265
|
const aiOutput = formatAIOutput(report)
|
|
52
266
|
process.stdout.write(JSON.stringify(aiOutput, null, 2))
|
|
53
267
|
return
|
|
54
268
|
}
|
|
55
269
|
|
|
56
|
-
if (
|
|
270
|
+
if (format === 'json') {
|
|
57
271
|
process.stdout.write(JSON.stringify(report, null, 2))
|
|
58
272
|
return
|
|
59
273
|
}
|
|
60
274
|
|
|
275
|
+
if (format === 'markdown') {
|
|
276
|
+
process.stdout.write(`${formatMarkdown(report)}\n`)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
61
280
|
printConsole(report, { showFix: options.fix })
|
|
62
281
|
|
|
63
282
|
if (options.output) {
|
|
@@ -72,29 +291,63 @@ program
|
|
|
72
291
|
if (minScore > 0 && report.totalScore > minScore) {
|
|
73
292
|
process.exit(1)
|
|
74
293
|
}
|
|
75
|
-
})
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
76
296
|
|
|
77
297
|
program
|
|
78
|
-
.command('
|
|
298
|
+
.command('init')
|
|
299
|
+
.description('Initialize drift configuration with presets and scaffolding')
|
|
300
|
+
.option('--preset <type>', `Scaffold config with preset: ${INIT_PRESETS.join(', ')}`)
|
|
301
|
+
.option('--ci', 'Generate GitHub Actions workflow for drift review')
|
|
302
|
+
.option('--baseline', 'Create drift-baseline.json with current project score')
|
|
303
|
+
.action(async (options: { preset?: string; ci?: boolean; baseline?: boolean }) => {
|
|
304
|
+
const projectRoot = resolve('.')
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await runInit(projectRoot, {
|
|
308
|
+
preset: options.preset,
|
|
309
|
+
ci: options.ci,
|
|
310
|
+
baseline: options.baseline,
|
|
311
|
+
})
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
314
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
315
|
+
process.exit(1)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
addResourceOptions(
|
|
320
|
+
program
|
|
321
|
+
.command('diff [ref]')
|
|
79
322
|
.description('Compare current state against a git ref (default: HEAD~1)')
|
|
323
|
+
.option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
|
|
80
324
|
.option('--json', 'Output raw JSON diff')
|
|
81
|
-
.action(async (ref: string | undefined, options: { json?: boolean }) => {
|
|
325
|
+
.action(async (ref: string | undefined, options: { format?: string; json?: boolean } & ResourceOptionFlags) => {
|
|
82
326
|
const baseRef = ref ?? 'HEAD~1'
|
|
83
327
|
const projectPath = resolve('.')
|
|
328
|
+
const analysisOptions = resolveAnalysisOptions(options)
|
|
84
329
|
|
|
85
330
|
let tempDir: string | undefined
|
|
86
331
|
|
|
87
332
|
try {
|
|
88
333
|
process.stderr.write(`\nComputing diff: HEAD vs ${baseRef}...\n\n`)
|
|
89
334
|
|
|
335
|
+
const format = resolveOutputFormat({
|
|
336
|
+
command: 'diff',
|
|
337
|
+
format: options.format,
|
|
338
|
+
supported: ['console', 'json', 'sarif'],
|
|
339
|
+
legacyAliases: [{ flag: 'json', used: options.json, mapsTo: 'json' }],
|
|
340
|
+
onWarning: (message) => process.stderr.write(`${message}\n`),
|
|
341
|
+
})
|
|
342
|
+
|
|
90
343
|
// Scan current state
|
|
91
344
|
const config = await loadConfig(projectPath)
|
|
92
|
-
const currentFiles = analyzeProject(projectPath, config)
|
|
345
|
+
const currentFiles = analyzeProject(projectPath, config, analysisOptions)
|
|
93
346
|
const currentReport = buildReport(projectPath, currentFiles)
|
|
94
347
|
|
|
95
348
|
// Extract base state from git
|
|
96
349
|
tempDir = extractFilesAtRef(projectPath, baseRef)
|
|
97
|
-
const baseFiles = analyzeProject(tempDir, config)
|
|
350
|
+
const baseFiles = analyzeProject(tempDir, config, analysisOptions)
|
|
98
351
|
|
|
99
352
|
// Remap base file paths to match current project paths
|
|
100
353
|
// (temp dir paths → project paths for accurate comparison)
|
|
@@ -103,13 +356,15 @@ program
|
|
|
103
356
|
...baseReport,
|
|
104
357
|
files: baseReport.files.map(f => ({
|
|
105
358
|
...f,
|
|
106
|
-
path:
|
|
359
|
+
path: resolve(projectPath, relative(tempDir!, f.path)),
|
|
107
360
|
})),
|
|
108
361
|
}
|
|
109
362
|
|
|
110
363
|
const diff = computeDiff(remappedBase, currentReport, baseRef)
|
|
111
364
|
|
|
112
|
-
if (
|
|
365
|
+
if (format === 'sarif') {
|
|
366
|
+
process.stdout.write(`${JSON.stringify(diffToSarif(diff), null, 2)}\n`)
|
|
367
|
+
} else if (format === 'json') {
|
|
113
368
|
process.stdout.write(JSON.stringify(diff, null, 2) + '\n')
|
|
114
369
|
} else {
|
|
115
370
|
printDiff(diff)
|
|
@@ -121,23 +376,96 @@ program
|
|
|
121
376
|
} finally {
|
|
122
377
|
if (tempDir) cleanupTempDir(tempDir)
|
|
123
378
|
}
|
|
379
|
+
}),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
addResourceOptions(
|
|
383
|
+
program
|
|
384
|
+
.command('guard [path]')
|
|
385
|
+
.description('Evaluate drift guard thresholds against diff or baseline')
|
|
386
|
+
.option('--base <ref>', 'Git base ref for diff guard mode')
|
|
387
|
+
.option('--baseline <file>', 'Baseline file path (default: drift-baseline.json)')
|
|
388
|
+
.option('--budget <n>', 'Allowed score delta budget')
|
|
389
|
+
.option('--by-severity <spec>', 'Severity thresholds: error=0,warning=2,info=5')
|
|
390
|
+
.option('--json', 'Output raw JSON guard result')
|
|
391
|
+
.action(async (
|
|
392
|
+
targetPath: string | undefined,
|
|
393
|
+
options: {
|
|
394
|
+
base?: string
|
|
395
|
+
baseline?: string
|
|
396
|
+
budget?: string
|
|
397
|
+
bySeverity?: string
|
|
398
|
+
json?: boolean
|
|
399
|
+
} & ResourceOptionFlags,
|
|
400
|
+
) => {
|
|
401
|
+
try {
|
|
402
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
403
|
+
const budget = parseOptionalNumber(options.budget, '--budget')
|
|
404
|
+
const bySeverity = parseBySeverity(options.bySeverity)
|
|
405
|
+
|
|
406
|
+
const result = await runGuard(resolvedPath, {
|
|
407
|
+
baseRef: options.base,
|
|
408
|
+
baselinePath: options.baseline,
|
|
409
|
+
budget,
|
|
410
|
+
bySeverity,
|
|
411
|
+
analysis: resolveAnalysisOptions(options),
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if (options.json) {
|
|
415
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n')
|
|
416
|
+
} else {
|
|
417
|
+
printGuardSummary(result)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!result.passed) {
|
|
421
|
+
process.exit(1)
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
425
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
426
|
+
process.exit(1)
|
|
427
|
+
}
|
|
428
|
+
}),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
program
|
|
432
|
+
.command('benchmark')
|
|
433
|
+
.description('Run benchmark harness for scan/review/trust commands')
|
|
434
|
+
.allowUnknownOption(true)
|
|
435
|
+
.action(async () => {
|
|
436
|
+
await runBenchmarkCli(process.argv.slice(3))
|
|
124
437
|
})
|
|
125
438
|
|
|
126
439
|
program
|
|
127
440
|
.command('review')
|
|
128
441
|
.description('Review drift against a base ref and output PR markdown')
|
|
129
442
|
.option('--base <ref>', 'Git base ref to compare against', 'origin/main')
|
|
443
|
+
.option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
|
|
130
444
|
.option('--json', 'Output structured review JSON')
|
|
131
445
|
.option('--comment', 'Output markdown comment body')
|
|
132
446
|
.option('--fail-on <n>', 'Exit with code 1 if score delta is >= n')
|
|
133
|
-
.action(async (options: { base: string; json?: boolean; comment?: boolean; failOn?: string }) => {
|
|
447
|
+
.action(async (options: { base: string; format?: string; json?: boolean; comment?: boolean; failOn?: string }) => {
|
|
134
448
|
try {
|
|
135
449
|
const review = await generateReview(resolve('.'), options.base)
|
|
450
|
+
const format = resolveOutputFormat({
|
|
451
|
+
command: 'review',
|
|
452
|
+
format: options.format,
|
|
453
|
+
supported: ['console', 'json', 'markdown', 'sarif'],
|
|
454
|
+
legacyAliases: [
|
|
455
|
+
{ flag: 'json', used: options.json, mapsTo: 'json' },
|
|
456
|
+
{ flag: 'comment', used: options.comment, mapsTo: 'markdown' },
|
|
457
|
+
],
|
|
458
|
+
onWarning: (message) => process.stderr.write(`${message}\n`),
|
|
459
|
+
})
|
|
136
460
|
|
|
137
|
-
if (
|
|
461
|
+
if (format === 'sarif') {
|
|
462
|
+
process.stdout.write(`${JSON.stringify(diffToSarif(review.diff), null, 2)}\n`)
|
|
463
|
+
} else if (format === 'json') {
|
|
138
464
|
process.stdout.write(JSON.stringify(review, null, 2) + '\n')
|
|
465
|
+
} else if (format === 'markdown') {
|
|
466
|
+
process.stdout.write(`${review.markdown}\n`)
|
|
139
467
|
} else {
|
|
140
|
-
process.stdout.write(
|
|
468
|
+
process.stdout.write(`${review.summary}\n\n${review.markdown}\n`)
|
|
141
469
|
}
|
|
142
470
|
|
|
143
471
|
const failOn = options.failOn ? Number(options.failOn) : undefined
|
|
@@ -151,6 +479,271 @@ program
|
|
|
151
479
|
}
|
|
152
480
|
})
|
|
153
481
|
|
|
482
|
+
addResourceOptions(
|
|
483
|
+
program
|
|
484
|
+
.command('trust [path]')
|
|
485
|
+
.description('Compute merge trust baseline from drift signals')
|
|
486
|
+
.option('--base <ref>', 'Git base ref for diff-aware trust scoring')
|
|
487
|
+
.option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
|
|
488
|
+
.option('--json', 'Output structured trust JSON')
|
|
489
|
+
.option('--markdown', 'Output trust report as markdown (PR comment ready)')
|
|
490
|
+
.option('-o, --output <file>', 'Write trust output to file')
|
|
491
|
+
.option('--json-output <file>', 'Write structured trust JSON to file without changing stdout format')
|
|
492
|
+
.option('--min-trust <n>', 'Exit with code 1 if trust score is below threshold')
|
|
493
|
+
.option('--max-risk <level>', 'Exit with code 1 if merge risk exceeds level (LOW|MEDIUM|HIGH|CRITICAL)')
|
|
494
|
+
.option('--branch <name>', 'Branch name for trust policy matching (default: auto-detect from CI env)')
|
|
495
|
+
.option('--policy-pack <name>', 'Trust policy pack from drift.config trustGate.policyPacks')
|
|
496
|
+
.option('--explain-policy', 'Print effective trust gate policy resolution to stderr')
|
|
497
|
+
.option('--advanced-trust', 'Enable advanced trust mode with historical comparison and team guidance')
|
|
498
|
+
.option('--previous-trust <file>', 'Previous trust JSON file to compare against (used in advanced mode)')
|
|
499
|
+
.option('--history-file <file>', 'Snapshot history JSON file (default: <path>/drift-history.json) for advanced mode')
|
|
500
|
+
.action(async (
|
|
501
|
+
targetPath: string | undefined,
|
|
502
|
+
options: {
|
|
503
|
+
base?: string
|
|
504
|
+
format?: string
|
|
505
|
+
json?: boolean
|
|
506
|
+
markdown?: boolean
|
|
507
|
+
output?: string
|
|
508
|
+
jsonOutput?: string
|
|
509
|
+
minTrust?: string
|
|
510
|
+
maxRisk?: string
|
|
511
|
+
branch?: string
|
|
512
|
+
policyPack?: string
|
|
513
|
+
explainPolicy?: boolean
|
|
514
|
+
advancedTrust?: boolean
|
|
515
|
+
previousTrust?: string
|
|
516
|
+
historyFile?: string
|
|
517
|
+
} & ResourceOptionFlags,
|
|
518
|
+
) => {
|
|
519
|
+
let tempDir: string | undefined
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
523
|
+
const analysisOptions = resolveAnalysisOptions(options)
|
|
524
|
+
|
|
525
|
+
process.stderr.write(`\nScanning ${resolvedPath} for trust signals...\n`)
|
|
526
|
+
const config = await loadConfig(resolvedPath)
|
|
527
|
+
const files = analyzeProject(resolvedPath, config, analysisOptions)
|
|
528
|
+
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
529
|
+
|
|
530
|
+
const report = buildReport(resolvedPath, files)
|
|
531
|
+
const branchName = resolveBranchFromOption(options.branch)
|
|
532
|
+
const policyExplanation = explainTrustGatePolicy(config, {
|
|
533
|
+
branchName,
|
|
534
|
+
policyPack: options.policyPack,
|
|
535
|
+
overrides: parseTrustGateOverrides(options),
|
|
536
|
+
})
|
|
537
|
+
const policy = policyExplanation.effectivePolicy
|
|
538
|
+
|
|
539
|
+
if (options.explainPolicy) {
|
|
540
|
+
printTrustGatePolicyDebug(policyExplanation)
|
|
541
|
+
} else if (policyExplanation.invalidPolicyPack) {
|
|
542
|
+
process.stderr.write(`Warning: policy pack '${policyExplanation.invalidPolicyPack}' was not found. Falling back to base/preset policy.\n`)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let diff: DriftDiff | undefined
|
|
546
|
+
if (options.base) {
|
|
547
|
+
process.stderr.write(`Computing diff signals against ${options.base}...\n`)
|
|
548
|
+
tempDir = extractFilesAtRef(resolvedPath, options.base)
|
|
549
|
+
const baseFiles = analyzeProject(tempDir, config, analysisOptions)
|
|
550
|
+
const baseReport = buildReport(tempDir, baseFiles)
|
|
551
|
+
const remappedBase = {
|
|
552
|
+
...baseReport,
|
|
553
|
+
files: baseReport.files.map((file) => ({
|
|
554
|
+
...file,
|
|
555
|
+
path: resolve(resolvedPath, relative(tempDir!, file.path)),
|
|
556
|
+
})),
|
|
557
|
+
}
|
|
558
|
+
diff = computeDiff(remappedBase, report, options.base)
|
|
559
|
+
process.stderr.write(` Diff: ${diff.totalDelta >= 0 ? '+' : ''}${diff.totalDelta} score, +${diff.newIssuesCount} new / -${diff.resolvedIssuesCount} resolved\n\n`)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let previousTrustReport: Partial<DriftTrustReport> | undefined
|
|
563
|
+
let snapshots: SnapshotHistory['snapshots'] | undefined
|
|
564
|
+
if (options.advancedTrust) {
|
|
565
|
+
if (options.previousTrust) {
|
|
566
|
+
const previousTrustPath = resolve(options.previousTrust)
|
|
567
|
+
const rawPreviousTrust = readFileSync(previousTrustPath, 'utf8')
|
|
568
|
+
previousTrustReport = JSON.parse(rawPreviousTrust) as Partial<DriftTrustReport>
|
|
569
|
+
process.stderr.write(`Advanced trust: loaded previous trust JSON from ${previousTrustPath}\n`)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (options.historyFile) {
|
|
573
|
+
const historyPath = resolve(options.historyFile)
|
|
574
|
+
const rawHistory = readFileSync(historyPath, 'utf8')
|
|
575
|
+
const history = JSON.parse(rawHistory) as SnapshotHistory
|
|
576
|
+
snapshots = history.snapshots
|
|
577
|
+
process.stderr.write(`Advanced trust: loaded snapshot history from ${historyPath}\n`)
|
|
578
|
+
} else {
|
|
579
|
+
snapshots = loadHistory(resolvedPath).snapshots
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const trust = buildTrustReport(report, {
|
|
584
|
+
diff,
|
|
585
|
+
advanced: {
|
|
586
|
+
enabled: options.advancedTrust,
|
|
587
|
+
previousTrust: previousTrustReport,
|
|
588
|
+
snapshots,
|
|
589
|
+
},
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
const format = resolveOutputFormat({
|
|
593
|
+
command: 'trust',
|
|
594
|
+
format: options.format,
|
|
595
|
+
supported: ['console', 'json', 'markdown', 'sarif'],
|
|
596
|
+
legacyAliases: [
|
|
597
|
+
{ flag: 'json', used: options.json, mapsTo: 'json' },
|
|
598
|
+
{ flag: 'markdown', used: options.markdown, mapsTo: 'markdown' },
|
|
599
|
+
],
|
|
600
|
+
onWarning: (message) => process.stderr.write(`${message}\n`),
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
const rendered = format === 'sarif'
|
|
604
|
+
? `${JSON.stringify(toSarif(report), null, 2)}\n`
|
|
605
|
+
: `${renderTrustOutput(trust, {
|
|
606
|
+
json: format === 'json',
|
|
607
|
+
markdown: format === 'markdown',
|
|
608
|
+
})}\n`
|
|
609
|
+
|
|
610
|
+
process.stdout.write(rendered)
|
|
611
|
+
|
|
612
|
+
if (options.output) {
|
|
613
|
+
const outPath = resolve(options.output)
|
|
614
|
+
writeFileSync(outPath, rendered, 'utf8')
|
|
615
|
+
process.stderr.write(`Trust output saved to ${outPath}\n`)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (options.jsonOutput) {
|
|
619
|
+
const jsonOutPath = resolve(options.jsonOutput)
|
|
620
|
+
writeFileSync(jsonOutPath, `${formatTrustJson(trust)}\n`, 'utf8')
|
|
621
|
+
process.stderr.write(`Trust JSON saved to ${jsonOutPath}\n`)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (policy.enabled === false) {
|
|
625
|
+
process.stderr.write(`Trust gate skipped by policy${branchName ? ` (branch: ${branchName})` : ''}\n`)
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (shouldFailTrustGate(trust, policy)) {
|
|
630
|
+
process.exit(1)
|
|
631
|
+
}
|
|
632
|
+
} catch (err) {
|
|
633
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
634
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
635
|
+
process.exit(1)
|
|
636
|
+
} finally {
|
|
637
|
+
if (tempDir) cleanupTempDir(tempDir)
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
program
|
|
643
|
+
.command('trust-gate <trustJsonFile>')
|
|
644
|
+
.description('Evaluate trust gate thresholds from an existing trust JSON file')
|
|
645
|
+
.option('--min-trust <n>', 'Fail if trust score is below threshold')
|
|
646
|
+
.option('--max-risk <level>', 'Fail if merge risk exceeds level (LOW|MEDIUM|HIGH|CRITICAL)')
|
|
647
|
+
.option('--branch <name>', 'Branch name for trust policy matching (default: auto-detect from CI env)')
|
|
648
|
+
.option('--policy-pack <name>', 'Trust policy pack from drift.config trustGate.policyPacks')
|
|
649
|
+
.option('--explain-policy', 'Print effective trust gate policy resolution to stderr')
|
|
650
|
+
.action(async (trustJsonFile: string, options: { minTrust?: string; maxRisk?: string; branch?: string; policyPack?: string; explainPolicy?: boolean }) => {
|
|
651
|
+
try {
|
|
652
|
+
const filePath = resolve(trustJsonFile)
|
|
653
|
+
const raw = readFileSync(filePath, 'utf8')
|
|
654
|
+
const parsed = JSON.parse(raw) as Partial<DriftTrustReport>
|
|
655
|
+
const config = await loadConfig(resolve('.'))
|
|
656
|
+
const branchName = resolveBranchFromOption(options.branch)
|
|
657
|
+
const policyExplanation = explainTrustGatePolicy(config, {
|
|
658
|
+
branchName,
|
|
659
|
+
policyPack: options.policyPack,
|
|
660
|
+
overrides: parseTrustGateOverrides(options),
|
|
661
|
+
})
|
|
662
|
+
const policy = policyExplanation.effectivePolicy
|
|
663
|
+
|
|
664
|
+
if (options.explainPolicy) {
|
|
665
|
+
printTrustGatePolicyDebug(policyExplanation)
|
|
666
|
+
} else if (policyExplanation.invalidPolicyPack) {
|
|
667
|
+
process.stderr.write(`Warning: policy pack '${policyExplanation.invalidPolicyPack}' was not found. Falling back to base/preset policy.\n`)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (typeof parsed.trust_score !== 'number') {
|
|
671
|
+
process.stderr.write('\n Error: trust JSON is missing numeric trust_score\n\n')
|
|
672
|
+
process.exit(1)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (typeof parsed.merge_risk !== 'string') {
|
|
676
|
+
process.stderr.write('\n Error: trust JSON is missing merge_risk\n\n')
|
|
677
|
+
process.exit(1)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const actualRisk = normalizeMergeRiskLevel(parsed.merge_risk)
|
|
681
|
+
if (!actualRisk) {
|
|
682
|
+
process.stderr.write(`\n Error: trust JSON merge_risk must be one of ${MERGE_RISK_ORDER.join(', ')}\n\n`)
|
|
683
|
+
process.exit(1)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const trust: DriftTrustReport = {
|
|
687
|
+
scannedAt: parsed.scannedAt ?? new Date().toISOString(),
|
|
688
|
+
targetPath: parsed.targetPath ?? '.',
|
|
689
|
+
trust_score: parsed.trust_score,
|
|
690
|
+
merge_risk: actualRisk,
|
|
691
|
+
top_reasons: parsed.top_reasons ?? [],
|
|
692
|
+
fix_priorities: parsed.fix_priorities ?? [],
|
|
693
|
+
diff_context: parsed.diff_context,
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (policy.enabled === false) {
|
|
697
|
+
process.stdout.write(`Trust gate skipped by policy${branchName ? ` (branch: ${branchName})` : ''}\n`)
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (shouldFailTrustGate(trust, policy)) {
|
|
702
|
+
process.exit(1)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
process.stdout.write(`Trust gate passed: trust=${trust.trust_score} risk=${trust.merge_risk}\n`)
|
|
706
|
+
} catch (err) {
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
708
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
709
|
+
process.exit(1)
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
program
|
|
714
|
+
.command('doctor')
|
|
715
|
+
.description('Run project environment diagnostics')
|
|
716
|
+
.option('--json', 'Output structured doctor JSON')
|
|
717
|
+
.action(async (opts: { json?: boolean }) => {
|
|
718
|
+
try {
|
|
719
|
+
await runDoctor(process.cwd(), { json: opts.json })
|
|
720
|
+
} catch (err) {
|
|
721
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
722
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
723
|
+
process.exitCode = 1
|
|
724
|
+
}
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
program
|
|
728
|
+
.command('kpi <path>')
|
|
729
|
+
.description('Aggregate trust KPIs from trust JSON artifacts')
|
|
730
|
+
.option('--no-summary', 'Disable console KPI summary in stderr')
|
|
731
|
+
.action((targetPath: string, options: { summary?: boolean }) => {
|
|
732
|
+
try {
|
|
733
|
+
const kpi = computeTrustKpis(targetPath)
|
|
734
|
+
|
|
735
|
+
if (options.summary !== false) {
|
|
736
|
+
process.stderr.write(`${formatTrustKpiConsole(kpi)}\n`)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
process.stdout.write(`${formatTrustKpiJson(kpi)}\n`)
|
|
740
|
+
} catch (err) {
|
|
741
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
742
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
743
|
+
process.exit(1)
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
|
|
154
747
|
program
|
|
155
748
|
.command('map [path]')
|
|
156
749
|
.description('Generate architecture.svg with simple layer dependencies')
|
|
@@ -163,56 +756,79 @@ program
|
|
|
163
756
|
process.stderr.write(` Architecture map saved to ${out}\n\n`)
|
|
164
757
|
})
|
|
165
758
|
|
|
166
|
-
|
|
167
|
-
|
|
759
|
+
addResourceOptions(
|
|
760
|
+
program
|
|
761
|
+
.command('report [path]')
|
|
168
762
|
.description('Generate a self-contained HTML report')
|
|
169
763
|
.option('-o, --output <file>', 'Output file path (default: drift-report.html)', 'drift-report.html')
|
|
170
|
-
.action(async (targetPath: string | undefined, options: { output: string }) => {
|
|
764
|
+
.action(async (targetPath: string | undefined, options: { output: string } & ResourceOptionFlags) => {
|
|
171
765
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
172
766
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
173
767
|
const config = await loadConfig(resolvedPath)
|
|
174
|
-
const files = analyzeProject(resolvedPath, config)
|
|
768
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
175
769
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
176
770
|
const report = buildReport(resolvedPath, files)
|
|
177
771
|
const html = generateHtmlReport(report)
|
|
178
772
|
const outPath = resolve(options.output)
|
|
179
773
|
writeFileSync(outPath, html, 'utf8')
|
|
180
774
|
process.stderr.write(` Report saved to ${outPath}\n\n`)
|
|
181
|
-
})
|
|
775
|
+
}),
|
|
776
|
+
)
|
|
182
777
|
|
|
183
|
-
|
|
184
|
-
|
|
778
|
+
addResourceOptions(
|
|
779
|
+
program
|
|
780
|
+
.command('badge [path]')
|
|
185
781
|
.description('Generate a badge.svg with the current drift score')
|
|
186
782
|
.option('-o, --output <file>', 'Output file path (default: badge.svg)', 'badge.svg')
|
|
187
|
-
.action(async (targetPath: string | undefined, options: { output: string }) => {
|
|
783
|
+
.action(async (targetPath: string | undefined, options: { output: string } & ResourceOptionFlags) => {
|
|
188
784
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
189
785
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
190
786
|
const config = await loadConfig(resolvedPath)
|
|
191
|
-
const files = analyzeProject(resolvedPath, config)
|
|
787
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
192
788
|
const report = buildReport(resolvedPath, files)
|
|
193
789
|
const svg = generateBadge(report.totalScore)
|
|
194
790
|
const outPath = resolve(options.output)
|
|
195
791
|
writeFileSync(outPath, svg, 'utf8')
|
|
196
792
|
process.stderr.write(` Badge saved to ${outPath}\n`)
|
|
197
793
|
process.stderr.write(` Score: ${report.totalScore}/100\n\n`)
|
|
198
|
-
})
|
|
794
|
+
}),
|
|
795
|
+
)
|
|
199
796
|
|
|
200
|
-
|
|
201
|
-
|
|
797
|
+
addResourceOptions(
|
|
798
|
+
program
|
|
799
|
+
.command('ci [path]')
|
|
202
800
|
.description('Emit GitHub Actions annotations and step summary')
|
|
801
|
+
.option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
|
|
802
|
+
.option('--json', 'Output raw JSON report (legacy alias for --format json)')
|
|
203
803
|
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
204
|
-
.action(async (targetPath: string | undefined, options: { minScore: string }) => {
|
|
804
|
+
.action(async (targetPath: string | undefined, options: { format?: string; json?: boolean; minScore: string } & ResourceOptionFlags) => {
|
|
205
805
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
206
806
|
const config = await loadConfig(resolvedPath)
|
|
207
|
-
const files = analyzeProject(resolvedPath, config)
|
|
807
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
208
808
|
const report = buildReport(resolvedPath, files)
|
|
209
|
-
|
|
210
|
-
|
|
809
|
+
|
|
810
|
+
const format = resolveOutputFormat({
|
|
811
|
+
command: 'ci',
|
|
812
|
+
format: options.format,
|
|
813
|
+
supported: ['console', 'json', 'sarif'],
|
|
814
|
+
legacyAliases: [{ flag: 'json', used: options.json, mapsTo: 'json' }],
|
|
815
|
+
onWarning: (message) => process.stderr.write(`${message}\n`),
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
if (format === 'sarif') {
|
|
819
|
+
process.stdout.write(`${JSON.stringify(toSarif(report), null, 2)}\n`)
|
|
820
|
+
} else if (format === 'json') {
|
|
821
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n')
|
|
822
|
+
} else {
|
|
823
|
+
emitCIAnnotations(report)
|
|
824
|
+
printCISummary(report)
|
|
825
|
+
}
|
|
211
826
|
const minScore = Number(options.minScore)
|
|
212
827
|
if (minScore > 0 && report.totalScore > minScore) {
|
|
213
828
|
process.exit(1)
|
|
214
829
|
}
|
|
215
|
-
})
|
|
830
|
+
}),
|
|
831
|
+
)
|
|
216
832
|
|
|
217
833
|
program
|
|
218
834
|
.command('trend [period]')
|
|
@@ -340,15 +956,16 @@ program
|
|
|
340
956
|
}
|
|
341
957
|
})
|
|
342
958
|
|
|
343
|
-
|
|
344
|
-
|
|
959
|
+
addResourceOptions(
|
|
960
|
+
program
|
|
961
|
+
.command('snapshot [path]')
|
|
345
962
|
.description('Record a score snapshot to drift-history.json')
|
|
346
963
|
.option('-l, --label <label>', 'label for this snapshot (e.g. sprint name, version)')
|
|
347
964
|
.option('--history', 'show all recorded snapshots')
|
|
348
965
|
.option('--diff', 'compare current score vs last snapshot')
|
|
349
966
|
.action(async (
|
|
350
967
|
targetPath: string | undefined,
|
|
351
|
-
opts: { label?: string; history?: boolean; diff?: boolean },
|
|
968
|
+
opts: { label?: string; history?: boolean; diff?: boolean } & ResourceOptionFlags,
|
|
352
969
|
) => {
|
|
353
970
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
354
971
|
|
|
@@ -360,7 +977,7 @@ program
|
|
|
360
977
|
|
|
361
978
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
362
979
|
const config = await loadConfig(resolvedPath)
|
|
363
|
-
const files = analyzeProject(resolvedPath, config)
|
|
980
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(opts))
|
|
364
981
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
365
982
|
const report = buildReport(resolvedPath, files)
|
|
366
983
|
|
|
@@ -376,73 +993,211 @@ program
|
|
|
376
993
|
` Snapshot recorded${labelStr}: score ${entry.score} (${entry.grade}) — ${entry.totalIssues} issues across ${entry.files} files\n`,
|
|
377
994
|
)
|
|
378
995
|
process.stdout.write(` Saved to drift-history.json\n\n`)
|
|
379
|
-
})
|
|
996
|
+
}),
|
|
997
|
+
)
|
|
380
998
|
|
|
381
999
|
const cloud = program
|
|
382
1000
|
.command('cloud')
|
|
383
1001
|
.description('Local SaaS foundations: ingest, summary, and dashboard')
|
|
384
1002
|
|
|
385
|
-
|
|
386
|
-
|
|
1003
|
+
addResourceOptions(
|
|
1004
|
+
cloud
|
|
1005
|
+
.command('ingest [path]')
|
|
387
1006
|
.description('Scan path, build report, and store cloud snapshot')
|
|
1007
|
+
.option('--org <id>', 'Organization id (default: default-org)', 'default-org')
|
|
388
1008
|
.requiredOption('--workspace <id>', 'Workspace id')
|
|
389
1009
|
.requiredOption('--user <id>', 'User id')
|
|
1010
|
+
.option('--role <role>', 'Role hint (owner|member|viewer)')
|
|
1011
|
+
.option('--plan <plan>', 'Organization plan (free|sponsor|team|business)')
|
|
390
1012
|
.option('--repo <name>', 'Repo name (default: basename of scanned path)')
|
|
1013
|
+
.option('--actor <user>', 'Actor user id for permission checks (local-only authz context)')
|
|
391
1014
|
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
392
|
-
.action(async (targetPath: string | undefined, options: { workspace: string; user: string; repo?: string; store?: string }) => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
1015
|
+
.action(async (targetPath: string | undefined, options: { org: string; workspace: string; user: string; role?: string; plan?: string; repo?: string; actor?: string; store?: string } & ResourceOptionFlags) => {
|
|
1016
|
+
try {
|
|
1017
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
1018
|
+
process.stderr.write(`\nScanning ${resolvedPath} for cloud ingest...\n`)
|
|
1019
|
+
const config = await loadConfig(resolvedPath)
|
|
1020
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
1021
|
+
const report = buildReport(resolvedPath, files)
|
|
398
1022
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1023
|
+
const snapshot = ingestSnapshotFromReport(report, {
|
|
1024
|
+
organizationId: options.org,
|
|
1025
|
+
workspaceId: options.workspace,
|
|
1026
|
+
userId: options.user,
|
|
1027
|
+
role: options.role as 'owner' | 'member' | 'viewer' | undefined,
|
|
1028
|
+
plan: options.plan as 'free' | 'sponsor' | 'team' | 'business' | undefined,
|
|
1029
|
+
repoName: options.repo ?? basename(resolvedPath),
|
|
1030
|
+
actorUserId: options.actor,
|
|
1031
|
+
storeFile: options.store,
|
|
1032
|
+
policy: config?.saas,
|
|
1033
|
+
})
|
|
406
1034
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
})
|
|
1035
|
+
process.stdout.write(`Ingested snapshot ${snapshot.id}\n`)
|
|
1036
|
+
process.stdout.write(`Organization: ${snapshot.organizationId} Workspace: ${snapshot.workspaceId} Repo: ${snapshot.repoName}\n`)
|
|
1037
|
+
process.stdout.write(`Role: ${snapshot.role} Plan: ${snapshot.plan}\n`)
|
|
1038
|
+
process.stdout.write(`Score: ${snapshot.totalScore}/100 Issues: ${snapshot.totalIssues}\n\n`)
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
printSaasErrorAndExit(error)
|
|
1041
|
+
}
|
|
1042
|
+
}),
|
|
1043
|
+
)
|
|
411
1044
|
|
|
412
1045
|
cloud
|
|
413
1046
|
.command('summary')
|
|
414
1047
|
.description('Show SaaS usage metrics and free threshold status')
|
|
415
1048
|
.option('--json', 'Output raw JSON summary')
|
|
1049
|
+
.option('--org <id>', 'Filter summary by organization id')
|
|
1050
|
+
.option('--workspace <id>', 'Filter summary by workspace id')
|
|
1051
|
+
.option('--actor <user>', 'Actor user id for permission checks (local-only authz context)')
|
|
416
1052
|
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
417
|
-
.action((options: { json?: boolean; store?: string }) => {
|
|
418
|
-
|
|
1053
|
+
.action((options: { json?: boolean; org?: string; workspace?: string; actor?: string; store?: string }) => {
|
|
1054
|
+
try {
|
|
1055
|
+
const summary = getSaasSummary({
|
|
1056
|
+
storeFile: options.store,
|
|
1057
|
+
organizationId: options.org,
|
|
1058
|
+
workspaceId: options.workspace,
|
|
1059
|
+
actorUserId: options.actor,
|
|
1060
|
+
})
|
|
419
1061
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
1062
|
+
if (options.json) {
|
|
1063
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + '\n')
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
process.stdout.write('\n')
|
|
1068
|
+
process.stdout.write(`Phase: ${summary.phase.toUpperCase()}\n`)
|
|
1069
|
+
process.stdout.write(`Users registered: ${summary.usersRegistered}\n`)
|
|
1070
|
+
process.stdout.write(`Active workspaces (30d): ${summary.workspacesActive}\n`)
|
|
1071
|
+
process.stdout.write(`Active repos (30d): ${summary.reposActive}\n`)
|
|
1072
|
+
process.stdout.write(`Total snapshots: ${summary.totalSnapshots}\n`)
|
|
1073
|
+
process.stdout.write(`Free user threshold: ${summary.policy.freeUserThreshold}\n`)
|
|
1074
|
+
process.stdout.write(`Threshold reached: ${summary.thresholdReached ? 'yes' : 'no'}\n`)
|
|
1075
|
+
process.stdout.write(`Free users remaining: ${summary.freeUsersRemaining}\n`)
|
|
1076
|
+
process.stdout.write('Runs per month:\n')
|
|
1077
|
+
|
|
1078
|
+
const monthly = Object.entries(summary.runsPerMonth).sort(([a], [b]) => a.localeCompare(b))
|
|
1079
|
+
if (monthly.length === 0) {
|
|
1080
|
+
process.stdout.write(' - none\n\n')
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
for (const [month, runs] of monthly) {
|
|
1085
|
+
process.stdout.write(` - ${month}: ${runs}\n`)
|
|
1086
|
+
}
|
|
1087
|
+
process.stdout.write('\n')
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
printSaasErrorAndExit(error)
|
|
423
1090
|
}
|
|
1091
|
+
})
|
|
424
1092
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1093
|
+
cloud
|
|
1094
|
+
.command('plan-set')
|
|
1095
|
+
.description('Set organization plan (owner role required when actor is provided)')
|
|
1096
|
+
.requiredOption('--org <id>', 'Organization id')
|
|
1097
|
+
.requiredOption('--plan <plan>', 'New organization plan (free|sponsor|team|business)')
|
|
1098
|
+
.requiredOption('--actor <user>', 'Actor user id used for owner-gated billing writes')
|
|
1099
|
+
.option('--reason <text>', 'Optional reason for audit trail')
|
|
1100
|
+
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
1101
|
+
.option('--json', 'Output raw JSON plan change')
|
|
1102
|
+
.action((options: { org: string; plan: string; actor: string; reason?: string; store?: string; json?: boolean }) => {
|
|
1103
|
+
try {
|
|
1104
|
+
const change = changeOrganizationPlan({
|
|
1105
|
+
organizationId: options.org,
|
|
1106
|
+
actorUserId: options.actor,
|
|
1107
|
+
newPlan: options.plan as 'free' | 'sponsor' | 'team' | 'business',
|
|
1108
|
+
reason: options.reason,
|
|
1109
|
+
storeFile: options.store,
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
if (options.json) {
|
|
1113
|
+
process.stdout.write(JSON.stringify(change, null, 2) + '\n')
|
|
1114
|
+
return
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
process.stdout.write(`Plan updated for org '${change.organizationId}': ${change.fromPlan} -> ${change.toPlan}\n`)
|
|
1118
|
+
process.stdout.write(`Changed by: ${change.changedByUserId} at ${change.changedAt}\n`)
|
|
1119
|
+
if (change.reason) process.stdout.write(`Reason: ${change.reason}\n`)
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
printSaasErrorAndExit(error)
|
|
440
1122
|
}
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
cloud
|
|
1126
|
+
.command('plan-changes')
|
|
1127
|
+
.description('List organization plan change audit trail')
|
|
1128
|
+
.requiredOption('--org <id>', 'Organization id')
|
|
1129
|
+
.requiredOption('--actor <user>', 'Actor user id used for billing read permissions')
|
|
1130
|
+
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
1131
|
+
.option('--json', 'Output raw JSON plan changes')
|
|
1132
|
+
.action((options: { org: string; actor: string; store?: string; json?: boolean }) => {
|
|
1133
|
+
try {
|
|
1134
|
+
const changes = listOrganizationPlanChanges({
|
|
1135
|
+
organizationId: options.org,
|
|
1136
|
+
actorUserId: options.actor,
|
|
1137
|
+
storeFile: options.store,
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
if (options.json) {
|
|
1141
|
+
process.stdout.write(JSON.stringify(changes, null, 2) + '\n')
|
|
1142
|
+
return
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (changes.length === 0) {
|
|
1146
|
+
process.stdout.write(`No plan changes found for org '${options.org}'.\n`)
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
process.stdout.write(`Plan changes for org '${options.org}':\n`)
|
|
1151
|
+
for (const change of changes) {
|
|
1152
|
+
const reasonSuffix = change.reason ? ` reason='${change.reason}'` : ''
|
|
1153
|
+
process.stdout.write(`- ${change.changedAt}: ${change.fromPlan} -> ${change.toPlan} by ${change.changedByUserId}${reasonSuffix}\n`)
|
|
1154
|
+
}
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
printSaasErrorAndExit(error)
|
|
1157
|
+
}
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
cloud
|
|
1161
|
+
.command('usage')
|
|
1162
|
+
.description('Show organization usage and effective limits')
|
|
1163
|
+
.requiredOption('--org <id>', 'Organization id')
|
|
1164
|
+
.requiredOption('--actor <user>', 'Actor user id used for billing read permissions')
|
|
1165
|
+
.option('--month <yyyy-mm>', 'Month filter for runCountThisMonth (default: current UTC month)')
|
|
1166
|
+
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
1167
|
+
.option('--json', 'Output usage and limits as raw JSON')
|
|
1168
|
+
.action((options: { org: string; actor: string; month?: string; store?: string; json?: boolean }) => {
|
|
1169
|
+
try {
|
|
1170
|
+
const usage = getOrganizationUsageSnapshot({
|
|
1171
|
+
organizationId: options.org,
|
|
1172
|
+
actorUserId: options.actor,
|
|
1173
|
+
month: options.month,
|
|
1174
|
+
storeFile: options.store,
|
|
1175
|
+
})
|
|
1176
|
+
const limits = getOrganizationEffectiveLimits({
|
|
1177
|
+
organizationId: options.org,
|
|
1178
|
+
storeFile: options.store,
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
if (options.json) {
|
|
1182
|
+
process.stdout.write(JSON.stringify({ usage, limits }, null, 2) + '\n')
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
441
1185
|
|
|
442
|
-
|
|
443
|
-
process.stdout.write(`
|
|
1186
|
+
process.stdout.write(`Organization: ${usage.organizationId}\n`)
|
|
1187
|
+
process.stdout.write(`Plan: ${usage.plan}\n`)
|
|
1188
|
+
process.stdout.write(`Captured at: ${usage.capturedAt}\n`)
|
|
1189
|
+
process.stdout.write(`Workspace count: ${usage.workspaceCount}\n`)
|
|
1190
|
+
process.stdout.write(`Repo count: ${usage.repoCount}\n`)
|
|
1191
|
+
process.stdout.write(`Runs total: ${usage.runCount}\n`)
|
|
1192
|
+
process.stdout.write(`Runs this month: ${usage.runCountThisMonth}\n`)
|
|
1193
|
+
process.stdout.write('Effective limits:\n')
|
|
1194
|
+
process.stdout.write(` - maxWorkspaces: ${limits.maxWorkspaces}\n`)
|
|
1195
|
+
process.stdout.write(` - maxReposPerWorkspace: ${limits.maxReposPerWorkspace}\n`)
|
|
1196
|
+
process.stdout.write(` - maxRunsPerWorkspacePerMonth: ${limits.maxRunsPerWorkspacePerMonth}\n`)
|
|
1197
|
+
process.stdout.write(` - retentionDays: ${limits.retentionDays}\n`)
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
printSaasErrorAndExit(error)
|
|
444
1200
|
}
|
|
445
|
-
process.stdout.write('\n')
|
|
446
1201
|
})
|
|
447
1202
|
|
|
448
1203
|
cloud
|