@eduardbar/drift 1.2.0 → 1.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/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +98 -6
- package/AGENTS.md +6 -0
- package/README.md +160 -10
- 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 +185 -0
- package/dist/cli.js +453 -62
- package/dist/diff.js +74 -10
- package/dist/git.js +12 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +177 -28
- package/dist/printer.js +4 -0
- package/dist/review.js +2 -2
- 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/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas.d.ts +143 -7
- package/dist/saas.js +478 -37
- package/dist/trust-kpi.d.ts +9 -0
- package/dist/trust-kpi.js +445 -0
- package/dist/trust.d.ts +65 -0
- package/dist/trust.js +571 -0
- package/dist/types.d.ts +154 -0
- package/docs/PRD.md +187 -109
- package/docs/plugin-contract.md +61 -0
- package/docs/trust-core-release-checklist.md +55 -0
- package/package.json +5 -3
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +244 -0
- package/src/cli.ts +562 -79
- package/src/diff.ts +75 -10
- package/src/git.ts +16 -0
- package/src/index.ts +48 -0
- package/src/plugins.ts +354 -26
- package/src/printer.ts +4 -0
- package/src/review.ts +2 -2
- 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/shared.ts +31 -3
- package/src/saas.ts +641 -43
- package/src/trust-kpi.ts +518 -0
- package/src/trust.ts +774 -0
- package/src/types.ts +171 -0
- package/tests/diff.test.ts +124 -0
- package/tests/new-features.test.ts +71 -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/trust-kpi.test.ts +120 -0
- package/tests/trust.test.ts +584 -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'
|
|
@@ -21,29 +21,130 @@ import { applyFixes, type FixResult } from './fix.js'
|
|
|
21
21
|
import { loadHistory, saveSnapshot, printHistory, printSnapshotDiff } from './snapshot.js'
|
|
22
22
|
import { generateReview } from './review.js'
|
|
23
23
|
import { generateArchitectureMap } from './map.js'
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
changeOrganizationPlan,
|
|
26
|
+
generateSaasDashboardHtml,
|
|
27
|
+
getOrganizationEffectiveLimits,
|
|
28
|
+
getOrganizationUsageSnapshot,
|
|
29
|
+
getSaasSummary,
|
|
30
|
+
ingestSnapshotFromReport,
|
|
31
|
+
listOrganizationPlanChanges,
|
|
32
|
+
} from './saas.js'
|
|
33
|
+
import {
|
|
34
|
+
buildTrustReport,
|
|
35
|
+
explainTrustGatePolicy,
|
|
36
|
+
formatTrustGatePolicyExplanation,
|
|
37
|
+
formatTrustJson,
|
|
38
|
+
renderTrustOutput,
|
|
39
|
+
shouldFailTrustGate,
|
|
40
|
+
normalizeMergeRiskLevel,
|
|
41
|
+
MERGE_RISK_ORDER,
|
|
42
|
+
detectBranchName,
|
|
43
|
+
} from './trust.js'
|
|
44
|
+
import { computeTrustKpis, formatTrustKpiConsole, formatTrustKpiJson } from './trust-kpi.js'
|
|
45
|
+
import type { DriftDiff, DriftTrustReport, DriftAnalysisOptions, MergeRiskLevel } from './types.js'
|
|
46
|
+
import type { TrustGatePolicyExplanation } from './trust.js'
|
|
47
|
+
import type { SnapshotHistory } from './snapshot.js'
|
|
25
48
|
|
|
26
49
|
const program = new Command()
|
|
27
50
|
|
|
51
|
+
type ResourceOptionFlags = {
|
|
52
|
+
lowMemory?: boolean
|
|
53
|
+
chunkSize?: string
|
|
54
|
+
maxFiles?: string
|
|
55
|
+
maxFileSizeKb?: string
|
|
56
|
+
withSemanticDuplication?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseOptionalPositiveInt(rawValue: string | undefined, flagName: string): number | undefined {
|
|
60
|
+
if (rawValue == null) return undefined
|
|
61
|
+
const value = Number(rawValue)
|
|
62
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
63
|
+
throw new Error(`${flagName} must be a non-negative integer`)
|
|
64
|
+
}
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveAnalysisOptions(options: ResourceOptionFlags): DriftAnalysisOptions {
|
|
69
|
+
return {
|
|
70
|
+
lowMemory: options.lowMemory,
|
|
71
|
+
chunkSize: parseOptionalPositiveInt(options.chunkSize, '--chunk-size'),
|
|
72
|
+
maxFiles: parseOptionalPositiveInt(options.maxFiles, '--max-files'),
|
|
73
|
+
maxFileSizeKb: parseOptionalPositiveInt(options.maxFileSizeKb, '--max-file-size-kb'),
|
|
74
|
+
includeSemanticDuplication: options.withSemanticDuplication ? true : undefined,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addResourceOptions(command: Command): Command {
|
|
79
|
+
return command
|
|
80
|
+
.option('--low-memory', 'Reduce peak memory usage by chunking AST analysis')
|
|
81
|
+
.option('--chunk-size <n>', 'Files per chunk in low-memory mode (default: 40)')
|
|
82
|
+
.option('--max-files <n>', 'Maximum files to analyze before soft-skipping extras')
|
|
83
|
+
.option('--max-file-size-kb <n>', 'Skip files above this size and report diagnostics')
|
|
84
|
+
.option('--with-semantic-duplication', 'Keep semantic-duplication rule enabled in low-memory mode')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseTrustGateOverrides(options: { minTrust?: string; maxRisk?: string }): { minTrust?: number; maxRisk?: MergeRiskLevel } {
|
|
88
|
+
const cliMinTrust = options.minTrust ? Number(options.minTrust) : undefined
|
|
89
|
+
if (options.minTrust && Number.isNaN(cliMinTrust)) {
|
|
90
|
+
process.stderr.write('\n Error: --min-trust must be a valid number\n\n')
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let cliMaxRisk: MergeRiskLevel | undefined
|
|
95
|
+
if (options.maxRisk) {
|
|
96
|
+
cliMaxRisk = normalizeMergeRiskLevel(options.maxRisk)
|
|
97
|
+
if (!cliMaxRisk) {
|
|
98
|
+
process.stderr.write(`\n Error: --max-risk must be one of ${MERGE_RISK_ORDER.join(', ')}\n\n`)
|
|
99
|
+
process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
minTrust: typeof cliMinTrust === 'number' ? cliMinTrust : undefined,
|
|
105
|
+
maxRisk: cliMaxRisk,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveBranchFromOption(branch?: string): string | undefined {
|
|
110
|
+
const normalized = branch?.trim()
|
|
111
|
+
if (normalized) return normalized
|
|
112
|
+
return detectBranchName()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printTrustGatePolicyDebug(explanation: TrustGatePolicyExplanation): void {
|
|
116
|
+
process.stderr.write(`${formatTrustGatePolicyExplanation(explanation)}\n`)
|
|
117
|
+
if (explanation.invalidPolicyPack) {
|
|
118
|
+
process.stderr.write(`Warning: policy pack '${explanation.invalidPolicyPack}' was not found. Falling back to base/preset policy.\n`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printSaasErrorAndExit(error: unknown): never {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
124
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
|
|
28
128
|
program
|
|
29
129
|
.name('drift')
|
|
30
|
-
.description('
|
|
130
|
+
.description('AI Code Audit CLI for merge trust in AI-assisted PRs')
|
|
31
131
|
.version(VERSION)
|
|
32
132
|
|
|
33
|
-
|
|
34
|
-
|
|
133
|
+
addResourceOptions(
|
|
134
|
+
program
|
|
135
|
+
.command('scan [path]', { isDefault: true })
|
|
35
136
|
.description('Scan a directory for vibe coding drift')
|
|
36
137
|
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
37
138
|
.option('--json', 'Output raw JSON report')
|
|
38
139
|
.option('--ai', 'Output AI-optimized JSON for LLM consumption')
|
|
39
140
|
.option('--fix', 'Show fix suggestions for each issue')
|
|
40
141
|
.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 }) => {
|
|
142
|
+
.action(async (targetPath: string | undefined, options: { output?: string; json?: boolean; ai?: boolean; fix?: boolean; minScore: string } & ResourceOptionFlags) => {
|
|
42
143
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
43
144
|
|
|
44
145
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
45
146
|
const config = await loadConfig(resolvedPath)
|
|
46
|
-
const files = analyzeProject(resolvedPath, config)
|
|
147
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
47
148
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
48
149
|
const report = buildReport(resolvedPath, files)
|
|
49
150
|
|
|
@@ -72,15 +173,18 @@ program
|
|
|
72
173
|
if (minScore > 0 && report.totalScore > minScore) {
|
|
73
174
|
process.exit(1)
|
|
74
175
|
}
|
|
75
|
-
})
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
76
178
|
|
|
77
|
-
|
|
78
|
-
|
|
179
|
+
addResourceOptions(
|
|
180
|
+
program
|
|
181
|
+
.command('diff [ref]')
|
|
79
182
|
.description('Compare current state against a git ref (default: HEAD~1)')
|
|
80
183
|
.option('--json', 'Output raw JSON diff')
|
|
81
|
-
.action(async (ref: string | undefined, options: { json?: boolean }) => {
|
|
184
|
+
.action(async (ref: string | undefined, options: { json?: boolean } & ResourceOptionFlags) => {
|
|
82
185
|
const baseRef = ref ?? 'HEAD~1'
|
|
83
186
|
const projectPath = resolve('.')
|
|
187
|
+
const analysisOptions = resolveAnalysisOptions(options)
|
|
84
188
|
|
|
85
189
|
let tempDir: string | undefined
|
|
86
190
|
|
|
@@ -89,12 +193,12 @@ program
|
|
|
89
193
|
|
|
90
194
|
// Scan current state
|
|
91
195
|
const config = await loadConfig(projectPath)
|
|
92
|
-
const currentFiles = analyzeProject(projectPath, config)
|
|
196
|
+
const currentFiles = analyzeProject(projectPath, config, analysisOptions)
|
|
93
197
|
const currentReport = buildReport(projectPath, currentFiles)
|
|
94
198
|
|
|
95
199
|
// Extract base state from git
|
|
96
200
|
tempDir = extractFilesAtRef(projectPath, baseRef)
|
|
97
|
-
const baseFiles = analyzeProject(tempDir, config)
|
|
201
|
+
const baseFiles = analyzeProject(tempDir, config, analysisOptions)
|
|
98
202
|
|
|
99
203
|
// Remap base file paths to match current project paths
|
|
100
204
|
// (temp dir paths → project paths for accurate comparison)
|
|
@@ -103,7 +207,7 @@ program
|
|
|
103
207
|
...baseReport,
|
|
104
208
|
files: baseReport.files.map(f => ({
|
|
105
209
|
...f,
|
|
106
|
-
path:
|
|
210
|
+
path: resolve(projectPath, relative(tempDir!, f.path)),
|
|
107
211
|
})),
|
|
108
212
|
}
|
|
109
213
|
|
|
@@ -121,7 +225,8 @@ program
|
|
|
121
225
|
} finally {
|
|
122
226
|
if (tempDir) cleanupTempDir(tempDir)
|
|
123
227
|
}
|
|
124
|
-
})
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
125
230
|
|
|
126
231
|
program
|
|
127
232
|
.command('review')
|
|
@@ -151,6 +256,239 @@ program
|
|
|
151
256
|
}
|
|
152
257
|
})
|
|
153
258
|
|
|
259
|
+
addResourceOptions(
|
|
260
|
+
program
|
|
261
|
+
.command('trust [path]')
|
|
262
|
+
.description('Compute merge trust baseline from drift signals')
|
|
263
|
+
.option('--base <ref>', 'Git base ref for diff-aware trust scoring')
|
|
264
|
+
.option('--json', 'Output structured trust JSON')
|
|
265
|
+
.option('--markdown', 'Output trust report as markdown (PR comment ready)')
|
|
266
|
+
.option('-o, --output <file>', 'Write trust output to file')
|
|
267
|
+
.option('--json-output <file>', 'Write structured trust JSON to file without changing stdout format')
|
|
268
|
+
.option('--min-trust <n>', 'Exit with code 1 if trust score is below threshold')
|
|
269
|
+
.option('--max-risk <level>', 'Exit with code 1 if merge risk exceeds level (LOW|MEDIUM|HIGH|CRITICAL)')
|
|
270
|
+
.option('--branch <name>', 'Branch name for trust policy matching (default: auto-detect from CI env)')
|
|
271
|
+
.option('--policy-pack <name>', 'Trust policy pack from drift.config trustGate.policyPacks')
|
|
272
|
+
.option('--explain-policy', 'Print effective trust gate policy resolution to stderr')
|
|
273
|
+
.option('--advanced-trust', 'Enable advanced trust mode with historical comparison and team guidance')
|
|
274
|
+
.option('--previous-trust <file>', 'Previous trust JSON file to compare against (used in advanced mode)')
|
|
275
|
+
.option('--history-file <file>', 'Snapshot history JSON file (default: <path>/drift-history.json) for advanced mode')
|
|
276
|
+
.action(async (
|
|
277
|
+
targetPath: string | undefined,
|
|
278
|
+
options: {
|
|
279
|
+
base?: string
|
|
280
|
+
json?: boolean
|
|
281
|
+
markdown?: boolean
|
|
282
|
+
output?: string
|
|
283
|
+
jsonOutput?: string
|
|
284
|
+
minTrust?: string
|
|
285
|
+
maxRisk?: string
|
|
286
|
+
branch?: string
|
|
287
|
+
policyPack?: string
|
|
288
|
+
explainPolicy?: boolean
|
|
289
|
+
advancedTrust?: boolean
|
|
290
|
+
previousTrust?: string
|
|
291
|
+
historyFile?: string
|
|
292
|
+
} & ResourceOptionFlags,
|
|
293
|
+
) => {
|
|
294
|
+
let tempDir: string | undefined
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
298
|
+
const analysisOptions = resolveAnalysisOptions(options)
|
|
299
|
+
|
|
300
|
+
process.stderr.write(`\nScanning ${resolvedPath} for trust signals...\n`)
|
|
301
|
+
const config = await loadConfig(resolvedPath)
|
|
302
|
+
const files = analyzeProject(resolvedPath, config, analysisOptions)
|
|
303
|
+
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
304
|
+
|
|
305
|
+
const report = buildReport(resolvedPath, files)
|
|
306
|
+
const branchName = resolveBranchFromOption(options.branch)
|
|
307
|
+
const policyExplanation = explainTrustGatePolicy(config, {
|
|
308
|
+
branchName,
|
|
309
|
+
policyPack: options.policyPack,
|
|
310
|
+
overrides: parseTrustGateOverrides(options),
|
|
311
|
+
})
|
|
312
|
+
const policy = policyExplanation.effectivePolicy
|
|
313
|
+
|
|
314
|
+
if (options.explainPolicy) {
|
|
315
|
+
printTrustGatePolicyDebug(policyExplanation)
|
|
316
|
+
} else if (policyExplanation.invalidPolicyPack) {
|
|
317
|
+
process.stderr.write(`Warning: policy pack '${policyExplanation.invalidPolicyPack}' was not found. Falling back to base/preset policy.\n`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let diff: DriftDiff | undefined
|
|
321
|
+
if (options.base) {
|
|
322
|
+
process.stderr.write(`Computing diff signals against ${options.base}...\n`)
|
|
323
|
+
tempDir = extractFilesAtRef(resolvedPath, options.base)
|
|
324
|
+
const baseFiles = analyzeProject(tempDir, config, analysisOptions)
|
|
325
|
+
const baseReport = buildReport(tempDir, baseFiles)
|
|
326
|
+
const remappedBase = {
|
|
327
|
+
...baseReport,
|
|
328
|
+
files: baseReport.files.map((file) => ({
|
|
329
|
+
...file,
|
|
330
|
+
path: resolve(resolvedPath, relative(tempDir!, file.path)),
|
|
331
|
+
})),
|
|
332
|
+
}
|
|
333
|
+
diff = computeDiff(remappedBase, report, options.base)
|
|
334
|
+
process.stderr.write(` Diff: ${diff.totalDelta >= 0 ? '+' : ''}${diff.totalDelta} score, +${diff.newIssuesCount} new / -${diff.resolvedIssuesCount} resolved\n\n`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let previousTrustReport: Partial<DriftTrustReport> | undefined
|
|
338
|
+
let snapshots: SnapshotHistory['snapshots'] | undefined
|
|
339
|
+
if (options.advancedTrust) {
|
|
340
|
+
if (options.previousTrust) {
|
|
341
|
+
const previousTrustPath = resolve(options.previousTrust)
|
|
342
|
+
const rawPreviousTrust = readFileSync(previousTrustPath, 'utf8')
|
|
343
|
+
previousTrustReport = JSON.parse(rawPreviousTrust) as Partial<DriftTrustReport>
|
|
344
|
+
process.stderr.write(`Advanced trust: loaded previous trust JSON from ${previousTrustPath}\n`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (options.historyFile) {
|
|
348
|
+
const historyPath = resolve(options.historyFile)
|
|
349
|
+
const rawHistory = readFileSync(historyPath, 'utf8')
|
|
350
|
+
const history = JSON.parse(rawHistory) as SnapshotHistory
|
|
351
|
+
snapshots = history.snapshots
|
|
352
|
+
process.stderr.write(`Advanced trust: loaded snapshot history from ${historyPath}\n`)
|
|
353
|
+
} else {
|
|
354
|
+
snapshots = loadHistory(resolvedPath).snapshots
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const trust = buildTrustReport(report, {
|
|
359
|
+
diff,
|
|
360
|
+
advanced: {
|
|
361
|
+
enabled: options.advancedTrust,
|
|
362
|
+
previousTrust: previousTrustReport,
|
|
363
|
+
snapshots,
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const rendered = `${renderTrustOutput(trust, options)}\n`
|
|
368
|
+
|
|
369
|
+
process.stdout.write(rendered)
|
|
370
|
+
|
|
371
|
+
if (options.output) {
|
|
372
|
+
const outPath = resolve(options.output)
|
|
373
|
+
writeFileSync(outPath, rendered, 'utf8')
|
|
374
|
+
process.stderr.write(`Trust output saved to ${outPath}\n`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (options.jsonOutput) {
|
|
378
|
+
const jsonOutPath = resolve(options.jsonOutput)
|
|
379
|
+
writeFileSync(jsonOutPath, `${formatTrustJson(trust)}\n`, 'utf8')
|
|
380
|
+
process.stderr.write(`Trust JSON saved to ${jsonOutPath}\n`)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (policy.enabled === false) {
|
|
384
|
+
process.stderr.write(`Trust gate skipped by policy${branchName ? ` (branch: ${branchName})` : ''}\n`)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (shouldFailTrustGate(trust, policy)) {
|
|
389
|
+
process.exit(1)
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
393
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
394
|
+
process.exit(1)
|
|
395
|
+
} finally {
|
|
396
|
+
if (tempDir) cleanupTempDir(tempDir)
|
|
397
|
+
}
|
|
398
|
+
}),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
program
|
|
402
|
+
.command('trust-gate <trustJsonFile>')
|
|
403
|
+
.description('Evaluate trust gate thresholds from an existing trust JSON file')
|
|
404
|
+
.option('--min-trust <n>', 'Fail if trust score is below threshold')
|
|
405
|
+
.option('--max-risk <level>', 'Fail if merge risk exceeds level (LOW|MEDIUM|HIGH|CRITICAL)')
|
|
406
|
+
.option('--branch <name>', 'Branch name for trust policy matching (default: auto-detect from CI env)')
|
|
407
|
+
.option('--policy-pack <name>', 'Trust policy pack from drift.config trustGate.policyPacks')
|
|
408
|
+
.option('--explain-policy', 'Print effective trust gate policy resolution to stderr')
|
|
409
|
+
.action(async (trustJsonFile: string, options: { minTrust?: string; maxRisk?: string; branch?: string; policyPack?: string; explainPolicy?: boolean }) => {
|
|
410
|
+
try {
|
|
411
|
+
const filePath = resolve(trustJsonFile)
|
|
412
|
+
const raw = readFileSync(filePath, 'utf8')
|
|
413
|
+
const parsed = JSON.parse(raw) as Partial<DriftTrustReport>
|
|
414
|
+
const config = await loadConfig(resolve('.'))
|
|
415
|
+
const branchName = resolveBranchFromOption(options.branch)
|
|
416
|
+
const policyExplanation = explainTrustGatePolicy(config, {
|
|
417
|
+
branchName,
|
|
418
|
+
policyPack: options.policyPack,
|
|
419
|
+
overrides: parseTrustGateOverrides(options),
|
|
420
|
+
})
|
|
421
|
+
const policy = policyExplanation.effectivePolicy
|
|
422
|
+
|
|
423
|
+
if (options.explainPolicy) {
|
|
424
|
+
printTrustGatePolicyDebug(policyExplanation)
|
|
425
|
+
} else if (policyExplanation.invalidPolicyPack) {
|
|
426
|
+
process.stderr.write(`Warning: policy pack '${policyExplanation.invalidPolicyPack}' was not found. Falling back to base/preset policy.\n`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (typeof parsed.trust_score !== 'number') {
|
|
430
|
+
process.stderr.write('\n Error: trust JSON is missing numeric trust_score\n\n')
|
|
431
|
+
process.exit(1)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (typeof parsed.merge_risk !== 'string') {
|
|
435
|
+
process.stderr.write('\n Error: trust JSON is missing merge_risk\n\n')
|
|
436
|
+
process.exit(1)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const actualRisk = normalizeMergeRiskLevel(parsed.merge_risk)
|
|
440
|
+
if (!actualRisk) {
|
|
441
|
+
process.stderr.write(`\n Error: trust JSON merge_risk must be one of ${MERGE_RISK_ORDER.join(', ')}\n\n`)
|
|
442
|
+
process.exit(1)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const trust: DriftTrustReport = {
|
|
446
|
+
scannedAt: parsed.scannedAt ?? new Date().toISOString(),
|
|
447
|
+
targetPath: parsed.targetPath ?? '.',
|
|
448
|
+
trust_score: parsed.trust_score,
|
|
449
|
+
merge_risk: actualRisk,
|
|
450
|
+
top_reasons: parsed.top_reasons ?? [],
|
|
451
|
+
fix_priorities: parsed.fix_priorities ?? [],
|
|
452
|
+
diff_context: parsed.diff_context,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (policy.enabled === false) {
|
|
456
|
+
process.stdout.write(`Trust gate skipped by policy${branchName ? ` (branch: ${branchName})` : ''}\n`)
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (shouldFailTrustGate(trust, policy)) {
|
|
461
|
+
process.exit(1)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
process.stdout.write(`Trust gate passed: trust=${trust.trust_score} risk=${trust.merge_risk}\n`)
|
|
465
|
+
} catch (err) {
|
|
466
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
467
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
468
|
+
process.exit(1)
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
program
|
|
473
|
+
.command('kpi <path>')
|
|
474
|
+
.description('Aggregate trust KPIs from trust JSON artifacts')
|
|
475
|
+
.option('--no-summary', 'Disable console KPI summary in stderr')
|
|
476
|
+
.action((targetPath: string, options: { summary?: boolean }) => {
|
|
477
|
+
try {
|
|
478
|
+
const kpi = computeTrustKpis(targetPath)
|
|
479
|
+
|
|
480
|
+
if (options.summary !== false) {
|
|
481
|
+
process.stderr.write(`${formatTrustKpiConsole(kpi)}\n`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
process.stdout.write(`${formatTrustKpiJson(kpi)}\n`)
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
487
|
+
process.stderr.write(`\n Error: ${message}\n\n`)
|
|
488
|
+
process.exit(1)
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
154
492
|
program
|
|
155
493
|
.command('map [path]')
|
|
156
494
|
.description('Generate architecture.svg with simple layer dependencies')
|
|
@@ -163,48 +501,53 @@ program
|
|
|
163
501
|
process.stderr.write(` Architecture map saved to ${out}\n\n`)
|
|
164
502
|
})
|
|
165
503
|
|
|
166
|
-
|
|
167
|
-
|
|
504
|
+
addResourceOptions(
|
|
505
|
+
program
|
|
506
|
+
.command('report [path]')
|
|
168
507
|
.description('Generate a self-contained HTML report')
|
|
169
508
|
.option('-o, --output <file>', 'Output file path (default: drift-report.html)', 'drift-report.html')
|
|
170
|
-
.action(async (targetPath: string | undefined, options: { output: string }) => {
|
|
509
|
+
.action(async (targetPath: string | undefined, options: { output: string } & ResourceOptionFlags) => {
|
|
171
510
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
172
511
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
173
512
|
const config = await loadConfig(resolvedPath)
|
|
174
|
-
const files = analyzeProject(resolvedPath, config)
|
|
513
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
175
514
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
176
515
|
const report = buildReport(resolvedPath, files)
|
|
177
516
|
const html = generateHtmlReport(report)
|
|
178
517
|
const outPath = resolve(options.output)
|
|
179
518
|
writeFileSync(outPath, html, 'utf8')
|
|
180
519
|
process.stderr.write(` Report saved to ${outPath}\n\n`)
|
|
181
|
-
})
|
|
520
|
+
}),
|
|
521
|
+
)
|
|
182
522
|
|
|
183
|
-
|
|
184
|
-
|
|
523
|
+
addResourceOptions(
|
|
524
|
+
program
|
|
525
|
+
.command('badge [path]')
|
|
185
526
|
.description('Generate a badge.svg with the current drift score')
|
|
186
527
|
.option('-o, --output <file>', 'Output file path (default: badge.svg)', 'badge.svg')
|
|
187
|
-
.action(async (targetPath: string | undefined, options: { output: string }) => {
|
|
528
|
+
.action(async (targetPath: string | undefined, options: { output: string } & ResourceOptionFlags) => {
|
|
188
529
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
189
530
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
190
531
|
const config = await loadConfig(resolvedPath)
|
|
191
|
-
const files = analyzeProject(resolvedPath, config)
|
|
532
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
192
533
|
const report = buildReport(resolvedPath, files)
|
|
193
534
|
const svg = generateBadge(report.totalScore)
|
|
194
535
|
const outPath = resolve(options.output)
|
|
195
536
|
writeFileSync(outPath, svg, 'utf8')
|
|
196
537
|
process.stderr.write(` Badge saved to ${outPath}\n`)
|
|
197
538
|
process.stderr.write(` Score: ${report.totalScore}/100\n\n`)
|
|
198
|
-
})
|
|
539
|
+
}),
|
|
540
|
+
)
|
|
199
541
|
|
|
200
|
-
|
|
201
|
-
|
|
542
|
+
addResourceOptions(
|
|
543
|
+
program
|
|
544
|
+
.command('ci [path]')
|
|
202
545
|
.description('Emit GitHub Actions annotations and step summary')
|
|
203
546
|
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
204
|
-
.action(async (targetPath: string | undefined, options: { minScore: string }) => {
|
|
547
|
+
.action(async (targetPath: string | undefined, options: { minScore: string } & ResourceOptionFlags) => {
|
|
205
548
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
206
549
|
const config = await loadConfig(resolvedPath)
|
|
207
|
-
const files = analyzeProject(resolvedPath, config)
|
|
550
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
208
551
|
const report = buildReport(resolvedPath, files)
|
|
209
552
|
emitCIAnnotations(report)
|
|
210
553
|
printCISummary(report)
|
|
@@ -212,7 +555,8 @@ program
|
|
|
212
555
|
if (minScore > 0 && report.totalScore > minScore) {
|
|
213
556
|
process.exit(1)
|
|
214
557
|
}
|
|
215
|
-
})
|
|
558
|
+
}),
|
|
559
|
+
)
|
|
216
560
|
|
|
217
561
|
program
|
|
218
562
|
.command('trend [period]')
|
|
@@ -340,15 +684,16 @@ program
|
|
|
340
684
|
}
|
|
341
685
|
})
|
|
342
686
|
|
|
343
|
-
|
|
344
|
-
|
|
687
|
+
addResourceOptions(
|
|
688
|
+
program
|
|
689
|
+
.command('snapshot [path]')
|
|
345
690
|
.description('Record a score snapshot to drift-history.json')
|
|
346
691
|
.option('-l, --label <label>', 'label for this snapshot (e.g. sprint name, version)')
|
|
347
692
|
.option('--history', 'show all recorded snapshots')
|
|
348
693
|
.option('--diff', 'compare current score vs last snapshot')
|
|
349
694
|
.action(async (
|
|
350
695
|
targetPath: string | undefined,
|
|
351
|
-
opts: { label?: string; history?: boolean; diff?: boolean },
|
|
696
|
+
opts: { label?: string; history?: boolean; diff?: boolean } & ResourceOptionFlags,
|
|
352
697
|
) => {
|
|
353
698
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
354
699
|
|
|
@@ -360,7 +705,7 @@ program
|
|
|
360
705
|
|
|
361
706
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
362
707
|
const config = await loadConfig(resolvedPath)
|
|
363
|
-
const files = analyzeProject(resolvedPath, config)
|
|
708
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(opts))
|
|
364
709
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
365
710
|
const report = buildReport(resolvedPath, files)
|
|
366
711
|
|
|
@@ -376,73 +721,211 @@ program
|
|
|
376
721
|
` Snapshot recorded${labelStr}: score ${entry.score} (${entry.grade}) — ${entry.totalIssues} issues across ${entry.files} files\n`,
|
|
377
722
|
)
|
|
378
723
|
process.stdout.write(` Saved to drift-history.json\n\n`)
|
|
379
|
-
})
|
|
724
|
+
}),
|
|
725
|
+
)
|
|
380
726
|
|
|
381
727
|
const cloud = program
|
|
382
728
|
.command('cloud')
|
|
383
729
|
.description('Local SaaS foundations: ingest, summary, and dashboard')
|
|
384
730
|
|
|
385
|
-
|
|
386
|
-
|
|
731
|
+
addResourceOptions(
|
|
732
|
+
cloud
|
|
733
|
+
.command('ingest [path]')
|
|
387
734
|
.description('Scan path, build report, and store cloud snapshot')
|
|
735
|
+
.option('--org <id>', 'Organization id (default: default-org)', 'default-org')
|
|
388
736
|
.requiredOption('--workspace <id>', 'Workspace id')
|
|
389
737
|
.requiredOption('--user <id>', 'User id')
|
|
738
|
+
.option('--role <role>', 'Role hint (owner|member|viewer)')
|
|
739
|
+
.option('--plan <plan>', 'Organization plan (free|sponsor|team|business)')
|
|
390
740
|
.option('--repo <name>', 'Repo name (default: basename of scanned path)')
|
|
741
|
+
.option('--actor <user>', 'Actor user id for permission checks (local-only authz context)')
|
|
391
742
|
.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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
743
|
+
.action(async (targetPath: string | undefined, options: { org: string; workspace: string; user: string; role?: string; plan?: string; repo?: string; actor?: string; store?: string } & ResourceOptionFlags) => {
|
|
744
|
+
try {
|
|
745
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
746
|
+
process.stderr.write(`\nScanning ${resolvedPath} for cloud ingest...\n`)
|
|
747
|
+
const config = await loadConfig(resolvedPath)
|
|
748
|
+
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
|
|
749
|
+
const report = buildReport(resolvedPath, files)
|
|
750
|
+
|
|
751
|
+
const snapshot = ingestSnapshotFromReport(report, {
|
|
752
|
+
organizationId: options.org,
|
|
753
|
+
workspaceId: options.workspace,
|
|
754
|
+
userId: options.user,
|
|
755
|
+
role: options.role as 'owner' | 'member' | 'viewer' | undefined,
|
|
756
|
+
plan: options.plan as 'free' | 'sponsor' | 'team' | 'business' | undefined,
|
|
757
|
+
repoName: options.repo ?? basename(resolvedPath),
|
|
758
|
+
actorUserId: options.actor,
|
|
759
|
+
storeFile: options.store,
|
|
760
|
+
policy: config?.saas,
|
|
761
|
+
})
|
|
406
762
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
})
|
|
763
|
+
process.stdout.write(`Ingested snapshot ${snapshot.id}\n`)
|
|
764
|
+
process.stdout.write(`Organization: ${snapshot.organizationId} Workspace: ${snapshot.workspaceId} Repo: ${snapshot.repoName}\n`)
|
|
765
|
+
process.stdout.write(`Role: ${snapshot.role} Plan: ${snapshot.plan}\n`)
|
|
766
|
+
process.stdout.write(`Score: ${snapshot.totalScore}/100 Issues: ${snapshot.totalIssues}\n\n`)
|
|
767
|
+
} catch (error) {
|
|
768
|
+
printSaasErrorAndExit(error)
|
|
769
|
+
}
|
|
770
|
+
}),
|
|
771
|
+
)
|
|
411
772
|
|
|
412
773
|
cloud
|
|
413
774
|
.command('summary')
|
|
414
775
|
.description('Show SaaS usage metrics and free threshold status')
|
|
415
776
|
.option('--json', 'Output raw JSON summary')
|
|
777
|
+
.option('--org <id>', 'Filter summary by organization id')
|
|
778
|
+
.option('--workspace <id>', 'Filter summary by workspace id')
|
|
779
|
+
.option('--actor <user>', 'Actor user id for permission checks (local-only authz context)')
|
|
416
780
|
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
417
|
-
.action((options: { json?: boolean; store?: string }) => {
|
|
418
|
-
|
|
781
|
+
.action((options: { json?: boolean; org?: string; workspace?: string; actor?: string; store?: string }) => {
|
|
782
|
+
try {
|
|
783
|
+
const summary = getSaasSummary({
|
|
784
|
+
storeFile: options.store,
|
|
785
|
+
organizationId: options.org,
|
|
786
|
+
workspaceId: options.workspace,
|
|
787
|
+
actorUserId: options.actor,
|
|
788
|
+
})
|
|
419
789
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
790
|
+
if (options.json) {
|
|
791
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + '\n')
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
process.stdout.write('\n')
|
|
796
|
+
process.stdout.write(`Phase: ${summary.phase.toUpperCase()}\n`)
|
|
797
|
+
process.stdout.write(`Users registered: ${summary.usersRegistered}\n`)
|
|
798
|
+
process.stdout.write(`Active workspaces (30d): ${summary.workspacesActive}\n`)
|
|
799
|
+
process.stdout.write(`Active repos (30d): ${summary.reposActive}\n`)
|
|
800
|
+
process.stdout.write(`Total snapshots: ${summary.totalSnapshots}\n`)
|
|
801
|
+
process.stdout.write(`Free user threshold: ${summary.policy.freeUserThreshold}\n`)
|
|
802
|
+
process.stdout.write(`Threshold reached: ${summary.thresholdReached ? 'yes' : 'no'}\n`)
|
|
803
|
+
process.stdout.write(`Free users remaining: ${summary.freeUsersRemaining}\n`)
|
|
804
|
+
process.stdout.write('Runs per month:\n')
|
|
805
|
+
|
|
806
|
+
const monthly = Object.entries(summary.runsPerMonth).sort(([a], [b]) => a.localeCompare(b))
|
|
807
|
+
if (monthly.length === 0) {
|
|
808
|
+
process.stdout.write(' - none\n\n')
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
for (const [month, runs] of monthly) {
|
|
813
|
+
process.stdout.write(` - ${month}: ${runs}\n`)
|
|
814
|
+
}
|
|
815
|
+
process.stdout.write('\n')
|
|
816
|
+
} catch (error) {
|
|
817
|
+
printSaasErrorAndExit(error)
|
|
423
818
|
}
|
|
819
|
+
})
|
|
424
820
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
821
|
+
cloud
|
|
822
|
+
.command('plan-set')
|
|
823
|
+
.description('Set organization plan (owner role required when actor is provided)')
|
|
824
|
+
.requiredOption('--org <id>', 'Organization id')
|
|
825
|
+
.requiredOption('--plan <plan>', 'New organization plan (free|sponsor|team|business)')
|
|
826
|
+
.requiredOption('--actor <user>', 'Actor user id used for owner-gated billing writes')
|
|
827
|
+
.option('--reason <text>', 'Optional reason for audit trail')
|
|
828
|
+
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
829
|
+
.option('--json', 'Output raw JSON plan change')
|
|
830
|
+
.action((options: { org: string; plan: string; actor: string; reason?: string; store?: string; json?: boolean }) => {
|
|
831
|
+
try {
|
|
832
|
+
const change = changeOrganizationPlan({
|
|
833
|
+
organizationId: options.org,
|
|
834
|
+
actorUserId: options.actor,
|
|
835
|
+
newPlan: options.plan as 'free' | 'sponsor' | 'team' | 'business',
|
|
836
|
+
reason: options.reason,
|
|
837
|
+
storeFile: options.store,
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
if (options.json) {
|
|
841
|
+
process.stdout.write(JSON.stringify(change, null, 2) + '\n')
|
|
842
|
+
return
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
process.stdout.write(`Plan updated for org '${change.organizationId}': ${change.fromPlan} -> ${change.toPlan}\n`)
|
|
846
|
+
process.stdout.write(`Changed by: ${change.changedByUserId} at ${change.changedAt}\n`)
|
|
847
|
+
if (change.reason) process.stdout.write(`Reason: ${change.reason}\n`)
|
|
848
|
+
} catch (error) {
|
|
849
|
+
printSaasErrorAndExit(error)
|
|
440
850
|
}
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
cloud
|
|
854
|
+
.command('plan-changes')
|
|
855
|
+
.description('List organization plan change audit trail')
|
|
856
|
+
.requiredOption('--org <id>', 'Organization id')
|
|
857
|
+
.requiredOption('--actor <user>', 'Actor user id used for billing read permissions')
|
|
858
|
+
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
859
|
+
.option('--json', 'Output raw JSON plan changes')
|
|
860
|
+
.action((options: { org: string; actor: string; store?: string; json?: boolean }) => {
|
|
861
|
+
try {
|
|
862
|
+
const changes = listOrganizationPlanChanges({
|
|
863
|
+
organizationId: options.org,
|
|
864
|
+
actorUserId: options.actor,
|
|
865
|
+
storeFile: options.store,
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
if (options.json) {
|
|
869
|
+
process.stdout.write(JSON.stringify(changes, null, 2) + '\n')
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (changes.length === 0) {
|
|
874
|
+
process.stdout.write(`No plan changes found for org '${options.org}'.\n`)
|
|
875
|
+
return
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
process.stdout.write(`Plan changes for org '${options.org}':\n`)
|
|
879
|
+
for (const change of changes) {
|
|
880
|
+
const reasonSuffix = change.reason ? ` reason='${change.reason}'` : ''
|
|
881
|
+
process.stdout.write(`- ${change.changedAt}: ${change.fromPlan} -> ${change.toPlan} by ${change.changedByUserId}${reasonSuffix}\n`)
|
|
882
|
+
}
|
|
883
|
+
} catch (error) {
|
|
884
|
+
printSaasErrorAndExit(error)
|
|
885
|
+
}
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
cloud
|
|
889
|
+
.command('usage')
|
|
890
|
+
.description('Show organization usage and effective limits')
|
|
891
|
+
.requiredOption('--org <id>', 'Organization id')
|
|
892
|
+
.requiredOption('--actor <user>', 'Actor user id used for billing read permissions')
|
|
893
|
+
.option('--month <yyyy-mm>', 'Month filter for runCountThisMonth (default: current UTC month)')
|
|
894
|
+
.option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
|
|
895
|
+
.option('--json', 'Output usage and limits as raw JSON')
|
|
896
|
+
.action((options: { org: string; actor: string; month?: string; store?: string; json?: boolean }) => {
|
|
897
|
+
try {
|
|
898
|
+
const usage = getOrganizationUsageSnapshot({
|
|
899
|
+
organizationId: options.org,
|
|
900
|
+
actorUserId: options.actor,
|
|
901
|
+
month: options.month,
|
|
902
|
+
storeFile: options.store,
|
|
903
|
+
})
|
|
904
|
+
const limits = getOrganizationEffectiveLimits({
|
|
905
|
+
organizationId: options.org,
|
|
906
|
+
storeFile: options.store,
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
if (options.json) {
|
|
910
|
+
process.stdout.write(JSON.stringify({ usage, limits }, null, 2) + '\n')
|
|
911
|
+
return
|
|
912
|
+
}
|
|
441
913
|
|
|
442
|
-
|
|
443
|
-
process.stdout.write(`
|
|
914
|
+
process.stdout.write(`Organization: ${usage.organizationId}\n`)
|
|
915
|
+
process.stdout.write(`Plan: ${usage.plan}\n`)
|
|
916
|
+
process.stdout.write(`Captured at: ${usage.capturedAt}\n`)
|
|
917
|
+
process.stdout.write(`Workspace count: ${usage.workspaceCount}\n`)
|
|
918
|
+
process.stdout.write(`Repo count: ${usage.repoCount}\n`)
|
|
919
|
+
process.stdout.write(`Runs total: ${usage.runCount}\n`)
|
|
920
|
+
process.stdout.write(`Runs this month: ${usage.runCountThisMonth}\n`)
|
|
921
|
+
process.stdout.write('Effective limits:\n')
|
|
922
|
+
process.stdout.write(` - maxWorkspaces: ${limits.maxWorkspaces}\n`)
|
|
923
|
+
process.stdout.write(` - maxReposPerWorkspace: ${limits.maxReposPerWorkspace}\n`)
|
|
924
|
+
process.stdout.write(` - maxRunsPerWorkspacePerMonth: ${limits.maxRunsPerWorkspacePerMonth}\n`)
|
|
925
|
+
process.stdout.write(` - retentionDays: ${limits.retentionDays}\n`)
|
|
926
|
+
} catch (error) {
|
|
927
|
+
printSaasErrorAndExit(error)
|
|
444
928
|
}
|
|
445
|
-
process.stdout.write('\n')
|
|
446
929
|
})
|
|
447
930
|
|
|
448
931
|
cloud
|