@eduardbar/drift 1.1.0 → 1.2.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/docs/PRD.md CHANGED
@@ -1,208 +1,157 @@
1
1
  # PRD - drift
2
2
 
3
- Version: 1.1-draft
4
- Estado: Draft
3
+ Version: 1.2.0
4
+ Estado: Activo
5
5
  Producto: `@eduardbar/drift`
6
6
 
7
7
  ## 1) Contexto
8
8
 
9
- `drift` es un CLI de analisis estatico para TypeScript que detecta deuda tecnica asociada a codigo generado por IA y calcula un score de calidad por archivo y por repositorio.
9
+ `drift` es un CLI de analisis estatico para TypeScript que detecta deuda tecnica asociada a codigo generado por IA y calcula score por archivo y por repositorio.
10
10
 
11
- Hoy el producto ya cubre escaneo AST, score, reportes y comandos operativos (`scan`, `fix`, `report`, `ci`, `diff`, `badge`, `snapshot`, `trend`, `blame`).
11
+ Con release `v1.2.0`, el producto entrega comandos operativos, analisis AST, reglas de arquitectura configurables, salida accionable, workflow CI para PR comments, y foundations SaaS (`drift cloud ingest|summary|dashboard`) con politica free-until-7500.
12
12
 
13
- Este PRD define la evolucion para convertir a drift en una plataforma de calidad de codigo AI-first, con foco en revision de PRs, reglas de arquitectura y accionabilidad.
13
+ ## 2) Vision de producto
14
14
 
15
- ## 2) Vision de Producto
15
+ Ser la herramienta de referencia para equipos que usan IA para programar y necesitan detectar, priorizar y corregir deuda tecnica antes de mergear a produccion.
16
16
 
17
- Ser la herramienta de referencia para equipos que usan IA para programar y necesitan detectar, priorizar y corregir deuda tecnica antes de que llegue a produccion.
18
-
19
- ## 3) Killer Feature
17
+ ## 3) Killer feature
20
18
 
21
19
  ## AI Code Smell Detector
22
20
 
23
- El diferencial principal de drift es detectar patrones de olor tecnico vinculados a codigo IA, estimar probabilidad de origen IA y traducir hallazgos en acciones concretas (fixes, comentarios de PR, reglas de arquitectura y reportes).
24
-
25
- ## 4) Objetivos de Negocio y Producto
26
-
27
- - Reducir riesgo de mantenimiento en repos con alto volumen de codigo IA.
28
- - Dar feedback accionable en el punto de trabajo (CLI, PR y editor).
29
- - Estandarizar calidad arquitectonica con reglas configurables por equipo.
30
- - Escalar desde CLI local a experiencia organizacional (dashboard SaaS).
31
-
32
- ## 5) Alcance MVP por Feature (sin humo)
33
-
34
- Cada feature incluye objetivo, alcance MVP y criterios de aceptacion.
35
-
36
- ### 5.1 Detector de codigo generado por IA
37
-
38
- - Objetivo: estimar `ai_likelihood` por archivo y listar `files_suspected`.
39
- - Alcance MVP:
40
- - Exponer `ai_likelihood` (0-100) dentro de salida JSON/AI.
41
- - Agregar lista ordenada de archivos sospechados con score y reglas disparadas.
42
- - No hacer afirmaciones absolutas; reportar como probabilidad.
43
- - Criterios de aceptacion:
44
- - `drift scan <path> --ai` incluye `ai_likelihood` por archivo.
45
- - Reporte global incluye `files_suspected` y top N archivos.
46
- - Tests cubren serializacion y orden de prioridad.
47
-
48
- ### 5.2 PR reviewer automatico (`drift review` + comentario en PR)
49
-
50
- - Objetivo: revisar cambios de PR y publicar feedback tecnico automatico.
51
- - Alcance MVP:
52
- - Nuevo comando `drift review` para analizar diff contra base branch.
53
- - Salida markdown apta para comentario de PR.
54
- - Integracion base via GitHub CLI (`gh`) en CI.
55
- - Criterios de aceptacion:
56
- - `drift review --base main` devuelve score de PR y top issues.
57
- - En workflow CI se publica un comentario unico actualizable.
58
- - Si score supera umbral configurable, CI falla.
59
-
60
- ### 5.3 Reglas de arquitectura configurables
61
-
62
- - Objetivo: habilitar reglas de arquitectura de negocio definidas por el equipo.
63
- - Alcance MVP:
64
- - Soporte en `drift.config.ts` para:
65
- - `controller-no-db`
66
- - `service-no-http`
67
- - `max-function-lines`
68
- - Mensajes de error claros con ubicacion y recomendacion.
69
- - Criterios de aceptacion:
70
- - Config valida y tipada.
71
- - Reglas activas afectan score y aparecen en reporte.
72
- - Fixtures de test para casos validos e invalidos.
73
-
74
- ### 5.4 Score de calidad por repo con breakdown
75
-
76
- - Objetivo: mostrar salud del repo en forma ejecutiva y tecnica.
77
- - Alcance MVP:
78
- - Score global del repo.
79
- - Breakdown por severidad, regla y carpeta.
80
- - Tendencia minima (ultimos snapshots locales).
81
- - Criterios de aceptacion:
82
- - `drift scan` muestra score repo + breakdown resumido.
83
- - `--json` expone estructura consumible por CI/dashboard.
84
-
85
- ### 5.5 Mapa de arquitectura automatico (`drift map` -> `architecture.svg`)
86
-
87
- - Objetivo: visualizar dependencias y violaciones de capas.
88
- - Alcance MVP:
89
- - Nuevo comando `drift map`.
90
- - Genera `architecture.svg` desde imports y modulos detectados.
91
- - Marca ciclos y violaciones de capas.
92
- - Criterios de aceptacion:
93
- - `drift map ./src` crea `architecture.svg` sin edicion manual.
94
- - El SVG es legible en repos medianos (ej. <= 300 archivos TS).
95
-
96
- ### 5.6 VSCode extension con feedback en tiempo real
97
-
98
- - Objetivo: bajar el tiempo entre error y correccion.
99
- - Alcance MVP:
100
- - Diagnosticos por archivo al guardar.
101
- - Score visible por archivo.
102
- - Quick actions para sugerencias simples.
103
- - Criterios de aceptacion:
104
- - La extension muestra issues drift en panel Problems.
105
- - La latencia por archivo en save se mantiene en nivel usable.
106
-
107
- ### 5.7 Fix automatico (`drift fix`) con ejemplo antes/despues
108
-
109
- - Objetivo: convertir hallazgos en cambios concretos de bajo riesgo.
110
- - Alcance MVP:
111
- - `drift fix` aplica fixes seguros en reglas seleccionadas.
112
- - Modo preview con diff antes/despues.
113
- - Modo write con confirmacion.
114
- - Criterios de aceptacion:
115
- - `drift fix --preview` imprime diff legible.
116
- - `drift fix --write` modifica solo reglas soportadas.
117
- - Tests de no-regresion para no romper sintaxis TS.
118
-
119
- Ejemplo (antes/despues):
120
-
121
- ```ts
122
- // Antes
123
- console.log(userData)
124
-
125
- // Despues (sugerencia simple)
126
- // Removed debug leftover; use structured logger if needed.
127
- ```
128
-
129
- ### 5.8 Reporte tecnico (`drift report` -> `drift-report.html`)
130
-
131
- - Objetivo: entregar reporte compartible para devs, tech leads y QA.
132
- - Alcance MVP:
133
- - Salida HTML `drift-report.html` con score, breakdown y top issues.
134
- - Secciones por archivo con snippets y sugerencias.
135
- - Criterios de aceptacion:
136
- - `drift report ./src --html` genera archivo navegable.
137
- - El reporte puede adjuntarse en CI artifacts.
138
-
139
- ### 5.9 Metricas de riesgo de mantenimiento (hotspots)
140
-
141
- - Objetivo: priorizar deuda por impacto real.
142
- - Alcance MVP:
143
- - Hotspots combinando score + frecuencia de cambios + criticidad.
144
- - Ranking de archivos para plan de refactor.
145
- - Criterios de aceptacion:
146
- - `drift trend` o salida dedicada muestra top hotspots.
147
- - Metodo de ranking documentado y testeado.
148
-
149
- ### 5.10 Plugin system (`drift-plugin-*`)
150
-
151
- - Objetivo: extender drift sin tocar el core.
152
- - Alcance MVP:
153
- - Carga de plugins por convension `drift-plugin-*`.
154
- - API minima para registrar reglas y metadata.
155
- - Aislamiento de errores de plugins para no romper scan completo.
156
- - Criterios de aceptacion:
157
- - Plugin de ejemplo funcional en repo de ejemplo.
158
- - Si un plugin falla, drift sigue ejecutando y reporta el error.
159
-
160
- ## 6) Roadmap Realista
161
-
162
- ### v1.1
163
-
164
- - `drift review` para PR comments.
165
- - Score de PR y score de repo con breakdown minimo.
166
-
167
- ### v1.2
21
+ Detectar patrones de olor tecnico vinculados a codigo IA, estimar probabilidad de origen IA y traducir hallazgos en acciones concretas (fixes, review de PR, reglas de arquitectura y reportes).
22
+
23
+ ## 4) Estado de cumplimiento (actualizado)
24
+
25
+ ### Entregado
26
+
27
+ - `drift review` en CLI para analizar diff contra base y producir markdown usable en PR.
28
+ - `drift map` basico para generar `architecture.svg`.
29
+ - Senial de IA en salida (`ai_likelihood` y `files_suspected`).
30
+ - Reglas de arquitectura configurables via `drift.config.ts`.
31
+ - Score y breakdown por dimensiones para lectura ejecutiva y tecnica.
32
+ - Metricas de maintenance risk/hotspots.
33
+ - Plugin system MVP (`drift-plugin-*`) con aislamiento de errores.
34
+ - `drift fix` con modos preview/write.
35
+ - Workflow CI para comentario automatico unico y actualizable de `drift review`.
36
+ - `drift map` con marcado de cycle edges y layer violations en el SVG.
37
+ - VSCode quick actions para fixes de bajo riesgo.
38
+ - Confirmacion interactiva para `drift fix --write` (con `--yes` para CI/no-interactive).
39
+ - `drift report` HTML (`drift-report.html`) sin flag extra.
40
+ - Documentacion y tests del release.
41
+
42
+ ### Parcial
43
+
44
+ - Consolidacion/hardening de API de plugins para ecosistema externo amplio.
45
+
46
+ ### Pendiente
47
+
48
+ - Hardening del contrato de plugins para ecosistema externo amplio.
49
+ - Evolucion del dashboard SaaS foundations a experiencia multi-tenant full (auth, permisos por rol y billing activo post-umbral).
50
+
51
+ ## 5) Criterios de aceptacion vigentes
52
+
53
+ ### 5.1 Entregables cerrados en v1.1.0
54
+
55
+ - `drift review --base <ref>` devuelve score delta de PR, issues nuevos/resueltos y markdown.
56
+ - `drift scan --ai` incluye `ai_likelihood` y ranking `files_suspected`.
57
+ - `drift map <path>` genera `architecture.svg` utilizable sin edicion manual.
58
+ - `drift report [path]` genera HTML self-contained (no requiere `--html`).
59
+ - `drift fix --preview` muestra antes/despues y `drift fix --write` aplica reglas soportadas.
60
+
61
+ ### 5.1.b Entregables cerrados en v1.2 (scope tecnico)
62
+
63
+ - Workflow CI publica/actualiza comentario unico en PR para `drift review`.
64
+ - `drift map` marca visualmente ciclos y violaciones por capa.
65
+ - Extension VSCode expone quick actions para `debug-leftover` y `catch-swallow`.
66
+ - `drift fix --write` pide confirmacion interactiva por defecto y admite `--yes`.
67
+
68
+ ### 5.2 Objetivos aun abiertos (CI/editor/UX)
69
+
70
+ - Hardening del contrato de plugins para compatibilidad de largo plazo (versionado/migraciones).
71
+
72
+ ## 6) Roadmap actualizado
73
+
74
+ ### v1.1 (completado - release 1.1.0)
75
+
76
+ Prioridades cerradas:
77
+ - CLI de review para PR, mapa basico, salida AI, reglas configurables, report HTML, fix preview/write, hotspots, plugin MVP.
78
+
79
+ Done del bloque:
80
+ - Features documentadas.
81
+ - Tests de paths principales.
82
+ - Salidas CLI/JSON/AI consistentes para uso local y CI.
83
+
84
+ ### v1.2 (completado - cierre de pendientes tecnicos)
85
+
86
+ Prioridades cerradas:
87
+ - Comentario automatico actualizable en PR desde workflow CI.
88
+ - Mejora de `drift map` para destacar ciclos y violaciones.
89
+ - UX de seguridad para `drift fix --write` con confirmacion interactiva.
90
+
91
+ Done del bloque:
92
+ - Flujo CI reproducible con comentario unico por PR.
93
+ - Visualizaciones verificables en SVG sobre repos medianos.
94
+ - Confirmacion interactiva implementada para write mode.
95
+
96
+ ### v2 (prioridad: experiencia de editor + extensibilidad)
97
+
98
+ Prioridades:
99
+ - Consolidacion de API de plugins y hardening de compatibilidad.
100
+ - Reglas de plugin versionadas y validacion de contrato avanzada.
101
+
102
+ Criterio de done:
103
+ - Plugins con contrato estable y manejo de errores robusto.
104
+ - Documentacion de versionado para autores de plugins.
168
105
 
169
- - Reglas de arquitectura configurables.
170
- - `drift map` y generacion de `architecture.svg`.
106
+ ### v3 (fundations completadas en v1.2.0)
171
107
 
172
- ### v2
108
+ Prioridades cerradas:
109
+ - Base de datos local de snapshots para cloud MVP.
110
+ - Ingestion de reportes en storage local SaaS-like.
111
+ - Summary de uso/threshold y dashboard HTML inicial.
112
+ - Guardrails de fase gratuita por workspace + politica free-until-7500.
173
113
 
174
- - VSCode extension con feedback en tiempo real.
175
- - `drift fix` con preview/write y fixes seguros.
114
+ Siguiente incremento (v3.x):
115
+ - Auth real multi-tenant, permisos por equipo y backend remoto persistente.
116
+ - Activacion de billing cuando el umbral de 7.500 usuarios se cumpla.
176
117
 
177
- ### v3
118
+ ## 6.1) Estrategia de monetizacion (aprobada)
178
119
 
179
- - SaaS dashboard para historico, equipos y gobierno de calidad.
120
+ - Fase gratuita: Drift SaaS gratis hasta alcanzar 7.500 usuarios registrados.
121
+ - Trigger de monetizacion: al alcanzar 7.500 usuarios, activar planes pagos para nuevos usuarios y definir politica de migracion para cohortes gratuitas.
122
+ - Objetivo: priorizar adopcion y proof-of-value temprano sin friccion comercial inicial.
123
+ - Guardrails durante fase gratuita:
124
+ - Limites tecnicos por workspace (runs/mes, retencion de historial, repositorios activos).
125
+ - Instrumentacion de uso desde el dia 1 para evitar abuso y medir unit economics.
126
+ - Feature flags de pricing listas antes del trigger para evitar corte abrupto.
180
127
 
181
- ## 7) Fuera de Alcance (por ahora)
128
+ ## 7) Fuera de alcance actual
182
129
 
183
130
  - Soporte multi-lenguaje completo fuera de TypeScript/JS.
184
- - Autofix de reglas de alto riesgo sin confirmacion.
131
+ - Autofix de reglas de alto riesgo sin confirmacion explicita.
185
132
  - Integraciones propietarias cerradas sin API estable.
186
133
 
187
- ## 8) KPIs de Exito
134
+ ## 8) KPIs de exito
188
135
 
189
136
  - Reduccion de score promedio en repos activos.
190
137
  - % de PRs con feedback drift resuelto antes de merge.
191
138
  - Tiempo medio desde deteccion hasta fix aplicado.
192
- - Adopcion de reglas de arquitectura por equipo.
139
+ - Adopcion de reglas de arquitectura configurables por equipo.
193
140
 
194
- ## 9) Dependencias y Riesgos
141
+ ## 9) Dependencias y riesgos
195
142
 
196
- - Performance en repos grandes (AST + analisis cross-file).
197
- - Calidad de senial en `ai_likelihood` (riesgo de falsos positivos).
198
- - Compatibilidad de integraciones CI/PR entre plataformas.
199
- - Diseno de API de plugins sin romper backward compatibility.
143
+ - Performance en repos grandes (AST + cross-file).
144
+ - Calidad de senial en `ai_likelihood` (falsos positivos/negativos).
145
+ - Variabilidad de entornos CI para publicar comentarios de PR.
146
+ - Evolucion de API de plugins sin romper backward compatibility.
200
147
 
201
- ## 10) Definicion de Done (global)
148
+ ## 10) Definition of Done por release
202
149
 
203
- Una feature del roadmap se considera terminada cuando:
150
+ Checklist minimo por release:
204
151
 
205
- - Tiene comando/flujo usable y documentado.
206
- - Tiene tests automatizados de casos base y borde.
207
- - Tiene salida estable en CLI/JSON para CI.
208
- - Tiene criterios de aceptacion de esta PRD cumplidos.
152
+ - [ ] Scope del release cerrado y trazable a este PRD.
153
+ - [ ] Comandos/flujo documentados con ejemplos reales.
154
+ - [ ] Tests de regresion en paths principales y casos borde.
155
+ - [ ] Salidas CLI/JSON/AI estables para automatizacion.
156
+ - [ ] Criterios de aceptacion del bloque marcados como cumplidos o movidos a pendiente.
157
+ - [ ] Riesgos y tradeoffs explicitados en notas de release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardbar/drift",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Detect silent technical debt left by AI-generated code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,53 @@
1
+ import * as vscode from 'vscode'
2
+
3
+ function buildRemoveLineEdit(document: vscode.TextDocument, line: number): vscode.WorkspaceEdit {
4
+ const edit = new vscode.WorkspaceEdit()
5
+ const targetLine = document.lineAt(line)
6
+ const start = targetLine.range.start
7
+ const end = line < document.lineCount - 1
8
+ ? document.lineAt(line + 1).range.start
9
+ : targetLine.range.end
10
+ edit.delete(document.uri, new vscode.Range(start, end))
11
+ return edit
12
+ }
13
+
14
+ function buildCatchTodoEdit(document: vscode.TextDocument, line: number): vscode.WorkspaceEdit {
15
+ const edit = new vscode.WorkspaceEdit()
16
+ const targetLine = document.lineAt(line)
17
+ const baseIndent = targetLine.text.match(/^\s*/)?.[0] ?? ''
18
+ const indent = `${baseIndent} `
19
+ edit.insert(document.uri, new vscode.Position(line + 1, 0), `${indent}// TODO: handle error\n`)
20
+ return edit
21
+ }
22
+
23
+ export class DriftCodeActionProvider implements vscode.CodeActionProvider {
24
+ provideCodeActions(
25
+ document: vscode.TextDocument,
26
+ _range: vscode.Range,
27
+ context: vscode.CodeActionContext,
28
+ ): vscode.CodeAction[] {
29
+ const actions: vscode.CodeAction[] = []
30
+
31
+ for (const diagnostic of context.diagnostics) {
32
+ if (diagnostic.source !== 'drift') continue
33
+ const rule = String(diagnostic.code ?? '')
34
+ const line = diagnostic.range.start.line
35
+
36
+ if (rule === 'debug-leftover') {
37
+ const quickFix = new vscode.CodeAction('drift: remove debug leftover line', vscode.CodeActionKind.QuickFix)
38
+ quickFix.diagnostics = [diagnostic]
39
+ quickFix.edit = buildRemoveLineEdit(document, line)
40
+ actions.push(quickFix)
41
+ }
42
+
43
+ if (rule === 'catch-swallow') {
44
+ const quickFix = new vscode.CodeAction('drift: add TODO in empty catch', vscode.CodeActionKind.QuickFix)
45
+ quickFix.diagnostics = [diagnostic]
46
+ quickFix.edit = buildCatchTodoEdit(document, line)
47
+ actions.push(quickFix)
48
+ }
49
+ }
50
+
51
+ return actions
52
+ }
53
+ }
@@ -5,6 +5,7 @@ import { analyzeFilePath } from './analyzer'
5
5
  import { DriftDiagnosticsProvider } from './diagnostics'
6
6
  import { DriftTreeProvider } from './treeview'
7
7
  import { DriftStatusBarItem } from './statusbar'
8
+ import { DriftCodeActionProvider } from './code-actions'
8
9
  import type { FileReport } from '@eduardbar/drift'
9
10
 
10
11
  const SUPPORTED_LANGUAGES = ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']
@@ -87,6 +88,7 @@ export function activate(context: vscode.ExtensionContext): void {
87
88
  const diagnostics = new DriftDiagnosticsProvider()
88
89
  const treeProvider = new DriftTreeProvider()
89
90
  const statusBar = new DriftStatusBarItem()
91
+ const codeActions = new DriftCodeActionProvider()
90
92
 
91
93
  const treeView = vscode.window.createTreeView('driftIssues', {
92
94
  treeDataProvider: treeProvider,
@@ -121,6 +123,14 @@ export function activate(context: vscode.ExtensionContext): void {
121
123
  }
122
124
  )
123
125
 
126
+ const codeActionRegistration = vscode.languages.registerCodeActionsProvider(
127
+ SUPPORTED_LANGUAGES.map((language) => ({ language })),
128
+ codeActions,
129
+ {
130
+ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix],
131
+ },
132
+ )
133
+
124
134
  context.subscriptions.push(
125
135
  { dispose: () => diagnostics.dispose() },
126
136
  { dispose: () => statusBar.dispose() },
@@ -129,6 +139,7 @@ export function activate(context: vscode.ExtensionContext): void {
129
139
  scanCmd,
130
140
  clearCmd,
131
141
  goToCmd,
142
+ codeActionRegistration,
132
143
  )
133
144
  }
134
145
 
package/src/cli.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  // drift-ignore-file
3
3
  import { Command } from 'commander'
4
4
  import { writeFileSync } from 'node:fs'
5
- import { resolve } from 'node:path'
5
+ import { basename, resolve } from 'node:path'
6
6
  import { createRequire } from 'node:module'
7
+ import { createInterface } from 'node:readline/promises'
8
+ import { stdin as input, stdout as output } from 'node:process'
7
9
  const require = createRequire(import.meta.url)
8
10
  const { version: VERSION } = require('../package.json') as { version: string }
9
11
  import { analyzeProject, analyzeFile, TrendAnalyzer, BlameAnalyzer } from './analyzer.js'
@@ -19,6 +21,7 @@ import { applyFixes, type FixResult } from './fix.js'
19
21
  import { loadHistory, saveSnapshot, printHistory, printSnapshotDiff } from './snapshot.js'
20
22
  import { generateReview } from './review.js'
21
23
  import { generateArchitectureMap } from './map.js'
24
+ import { ingestSnapshotFromReport, getSaasSummary, generateSaasDashboardHtml } from './saas.js'
22
25
 
23
26
  const program = new Command()
24
27
 
@@ -155,7 +158,8 @@ program
155
158
  .action(async (targetPath: string | undefined, options: { output: string }) => {
156
159
  const resolvedPath = resolve(targetPath ?? '.')
157
160
  process.stderr.write(`\nBuilding architecture map for ${resolvedPath}...\n`)
158
- const out = generateArchitectureMap(resolvedPath, options.output)
161
+ const config = await loadConfig(resolvedPath)
162
+ const out = generateArchitectureMap(resolvedPath, options.output, config)
159
163
  process.stderr.write(` Architecture map saved to ${out}\n\n`)
160
164
  })
161
165
 
@@ -259,12 +263,38 @@ program
259
263
  .option('--preview', 'Preview changes without writing files')
260
264
  .option('--write', 'Write fixes to disk')
261
265
  .option('--dry-run', 'Show what would change without writing files')
262
- .action(async (targetPath: string | undefined, options: { rule?: string; dryRun?: boolean; preview?: boolean; write?: boolean }) => {
266
+ .option('-y, --yes', 'Skip interactive confirmation for --write')
267
+ .action(async (targetPath: string | undefined, options: { rule?: string; dryRun?: boolean; preview?: boolean; write?: boolean; yes?: boolean }) => {
263
268
  const resolvedPath = resolve(targetPath ?? '.')
264
269
  const config = await loadConfig(resolvedPath)
265
270
  const previewMode = Boolean(options.preview || options.dryRun)
266
271
  const writeMode = options.write ?? !previewMode
267
272
 
273
+ if (writeMode && !options.yes) {
274
+ const previewResults = await applyFixes(resolvedPath, config, {
275
+ rule: options.rule,
276
+ dryRun: true,
277
+ preview: true,
278
+ write: false,
279
+ })
280
+
281
+ if (previewResults.length === 0) {
282
+ console.log('No fixable issues found.')
283
+ return
284
+ }
285
+
286
+ const files = new Set(previewResults.map((result) => result.file)).size
287
+ const prompt = `Apply ${previewResults.length} fix(es) across ${files} file(s)? [y/N] `
288
+ const rl = createInterface({ input, output })
289
+ const answer = (await rl.question(prompt)).trim().toLowerCase()
290
+ rl.close()
291
+
292
+ if (answer !== 'y' && answer !== 'yes') {
293
+ console.log('Aborted. No files were modified.')
294
+ return
295
+ }
296
+ }
297
+
268
298
  const results = await applyFixes(resolvedPath, config, {
269
299
  rule: options.rule,
270
300
  dryRun: previewMode,
@@ -348,4 +378,83 @@ program
348
378
  process.stdout.write(` Saved to drift-history.json\n\n`)
349
379
  })
350
380
 
381
+ const cloud = program
382
+ .command('cloud')
383
+ .description('Local SaaS foundations: ingest, summary, and dashboard')
384
+
385
+ cloud
386
+ .command('ingest [path]')
387
+ .description('Scan path, build report, and store cloud snapshot')
388
+ .requiredOption('--workspace <id>', 'Workspace id')
389
+ .requiredOption('--user <id>', 'User id')
390
+ .option('--repo <name>', 'Repo name (default: basename of scanned path)')
391
+ .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
+ const resolvedPath = resolve(targetPath ?? '.')
394
+ process.stderr.write(`\nScanning ${resolvedPath} for cloud ingest...\n`)
395
+ const config = await loadConfig(resolvedPath)
396
+ const files = analyzeProject(resolvedPath, config)
397
+ const report = buildReport(resolvedPath, files)
398
+
399
+ const snapshot = ingestSnapshotFromReport(report, {
400
+ workspaceId: options.workspace,
401
+ userId: options.user,
402
+ repoName: options.repo ?? basename(resolvedPath),
403
+ storeFile: options.store,
404
+ policy: config?.saas,
405
+ })
406
+
407
+ process.stdout.write(`Ingested snapshot ${snapshot.id}\n`)
408
+ process.stdout.write(`Workspace: ${snapshot.workspaceId} Repo: ${snapshot.repoName}\n`)
409
+ process.stdout.write(`Score: ${snapshot.totalScore}/100 Issues: ${snapshot.totalIssues}\n\n`)
410
+ })
411
+
412
+ cloud
413
+ .command('summary')
414
+ .description('Show SaaS usage metrics and free threshold status')
415
+ .option('--json', 'Output raw JSON summary')
416
+ .option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
417
+ .action((options: { json?: boolean; store?: string }) => {
418
+ const summary = getSaasSummary({ storeFile: options.store })
419
+
420
+ if (options.json) {
421
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n')
422
+ return
423
+ }
424
+
425
+ process.stdout.write('\n')
426
+ process.stdout.write(`Phase: ${summary.phase.toUpperCase()}\n`)
427
+ process.stdout.write(`Users registered: ${summary.usersRegistered}\n`)
428
+ process.stdout.write(`Active workspaces (30d): ${summary.workspacesActive}\n`)
429
+ process.stdout.write(`Active repos (30d): ${summary.reposActive}\n`)
430
+ process.stdout.write(`Total snapshots: ${summary.totalSnapshots}\n`)
431
+ process.stdout.write(`Free user threshold: ${summary.policy.freeUserThreshold}\n`)
432
+ process.stdout.write(`Threshold reached: ${summary.thresholdReached ? 'yes' : 'no'}\n`)
433
+ process.stdout.write(`Free users remaining: ${summary.freeUsersRemaining}\n`)
434
+ process.stdout.write('Runs per month:\n')
435
+
436
+ const monthly = Object.entries(summary.runsPerMonth).sort(([a], [b]) => a.localeCompare(b))
437
+ if (monthly.length === 0) {
438
+ process.stdout.write(' - none\n\n')
439
+ return
440
+ }
441
+
442
+ for (const [month, runs] of monthly) {
443
+ process.stdout.write(` - ${month}: ${runs}\n`)
444
+ }
445
+ process.stdout.write('\n')
446
+ })
447
+
448
+ cloud
449
+ .command('dashboard')
450
+ .description('Generate an HTML dashboard with trends and hotspots')
451
+ .option('-o, --output <file>', 'Output HTML file', 'drift-cloud-dashboard.html')
452
+ .option('--store <file>', 'Store file path (default: .drift-cloud/store.json)')
453
+ .action((options: { output: string; store?: string }) => {
454
+ const html = generateSaasDashboardHtml({ storeFile: options.store })
455
+ const outPath = resolve(options.output)
456
+ writeFileSync(outPath, html, 'utf8')
457
+ process.stdout.write(`Dashboard saved to ${outPath}\n`)
458
+ })
459
+
351
460
  program.parse()
package/src/index.ts CHANGED
@@ -17,3 +17,18 @@ export type {
17
17
  } from './types.js'
18
18
  export { loadHistory, saveSnapshot } from './snapshot.js'
19
19
  export type { SnapshotEntry, SnapshotHistory } from './snapshot.js'
20
+ export {
21
+ DEFAULT_SAAS_POLICY,
22
+ defaultSaasStorePath,
23
+ resolveSaasPolicy,
24
+ ingestSnapshotFromReport,
25
+ getSaasSummary,
26
+ generateSaasDashboardHtml,
27
+ } from './saas.js'
28
+ export type {
29
+ SaasPolicy,
30
+ SaasStore,
31
+ SaasSummary,
32
+ SaasSnapshot,
33
+ IngestOptions,
34
+ } from './saas.js'