@chappibunny/repolens 1.9.4 → 1.9.6
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/CHANGELOG.md +25 -0
- package/README.md +3 -3
- package/docs/ENVIRONMENT.md +23 -1
- package/package.json +1 -1
- package/src/ai/provider.js +39 -4
- package/src/core/config-schema.js +86 -0
- package/src/docs/generate-doc-set.js +5 -0
- package/src/doctor.js +27 -4
- package/src/publishers/index.js +2 -2
- package/src/utils/doc-cache.js +24 -8
- package/src/utils/logger.js +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.9.6
|
|
6
|
+
|
|
7
|
+
### 📹 Documentation
|
|
8
|
+
|
|
9
|
+
- **Updated demo video**: README now embeds new Loom demo video
|
|
10
|
+
|
|
11
|
+
## 1.9.5
|
|
12
|
+
|
|
13
|
+
### ✨ UX Improvements
|
|
14
|
+
|
|
15
|
+
- **AI Provider Presets**: New `REPOLENS_AI_PRESET` environment variable for one-line AI setup. Supports `github`, `openai`, `anthropic`, and `google` presets that automatically configure provider, base URL, and model.
|
|
16
|
+
|
|
17
|
+
- **Progress Indicator**: Document generation now shows step-by-step progress: `[1/15] Generating executive_summary...`, `[2/15] Generating business_domains...`, etc.
|
|
18
|
+
|
|
19
|
+
- **Config Typo Detection**: `repolens doctor` now detects typos in config keys and suggests corrections using Levenshtein distance. For example: `Unknown config key "projet" — did you mean "project"?`
|
|
20
|
+
|
|
21
|
+
- **Cache Age Display**: Publishing shows when the cache was last updated: `Cache: 5/15 documents unchanged (last run: 2h ago)`.
|
|
22
|
+
|
|
23
|
+
- **Verbose Export**: The `--verbose` flag and `isVerbose` constant are now exported from logger for use by plugins and custom scripts.
|
|
24
|
+
|
|
25
|
+
### 🧪 Tests
|
|
26
|
+
|
|
27
|
+
- Updated doc-cache tests to verify new `{ cache, age }` return format
|
|
28
|
+
- **380 tests passing** across 22 test files
|
|
29
|
+
|
|
5
30
|
## 1.9.3
|
|
6
31
|
|
|
7
32
|
### ✨ Premium Deterministic Documentation
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
RepoLens scans your repository, generates living architecture documentation, and publishes it to Notion, Confluence, GitHub Wiki, or Markdown — automatically on every push. Engineers get technical docs. Stakeholders get readable system overviews. Nobody writes a word.
|
|
19
19
|
|
|
20
|
-
> Stable as of v1.0 — [API guarantees](docs/STABILITY.md) · [Security hardened](SECURITY.md) · v1.9.
|
|
20
|
+
> Stable as of v1.0 — [API guarantees](docs/STABILITY.md) · [Security hardened](SECURITY.md) · v1.9.6
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -25,9 +25,9 @@ RepoLens scans your repository, generates living architecture documentation, and
|
|
|
25
25
|
|
|
26
26
|
> **Try it now** — no installation required. Run `npx @chappibunny/repolens demo` on any repo for an instant local preview.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
<div style="position: relative; padding-bottom: 40.955631399317404%; height: 0;"><iframe src="https://www.loom.com/embed/8e077624e69f41319fd93acbbe03871e" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>
|
|
29
29
|
|
|
30
|
-
▶️ *Click to watch
|
|
30
|
+
▶️ *Click to watch demo*
|
|
31
31
|
|
|
32
32
|
<details>
|
|
33
33
|
<summary>🔍 <strong>Supported Languages</strong> (16 auto-detected)</summary>
|
package/docs/ENVIRONMENT.md
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
| Variable | Required | Description |
|
|
37
37
|
|----------|----------|-------------|
|
|
38
38
|
| `REPOLENS_AI_ENABLED` | No | Enable AI-powered sections (`true`/`false`) |
|
|
39
|
+
| `REPOLENS_AI_PRESET` | No | Quick setup preset: `github`, `openai`, `anthropic`, `google` (overrides provider/baseUrl/model) |
|
|
39
40
|
| `REPOLENS_AI_PROVIDER` | No | Provider: `openai_compatible` (default), `anthropic`, `google`, `github` |
|
|
40
41
|
| `REPOLENS_AI_API_KEY` | No | API key for AI provider (not needed for `github` provider) |
|
|
41
42
|
| `REPOLENS_AI_BASE_URL` | No | API base URL (auto-set per provider; override for custom endpoints) |
|
|
@@ -43,7 +44,28 @@
|
|
|
43
44
|
| `REPOLENS_AI_TEMPERATURE` | No | Generation temperature (omitted by default for GPT-5 compatibility) |
|
|
44
45
|
| `REPOLENS_AI_MAX_TOKENS` | No | Max completion tokens per request (default: `2000`) |
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
### AI Presets (Quick Setup)
|
|
48
|
+
|
|
49
|
+
Instead of configuring provider, base URL, and model separately, use a preset:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# GitHub Models (free in GitHub Actions)
|
|
53
|
+
REPOLENS_AI_PRESET=github
|
|
54
|
+
|
|
55
|
+
# OpenAI
|
|
56
|
+
REPOLENS_AI_PRESET=openai
|
|
57
|
+
REPOLENS_AI_API_KEY=sk-...
|
|
58
|
+
|
|
59
|
+
# Anthropic
|
|
60
|
+
REPOLENS_AI_PRESET=anthropic
|
|
61
|
+
REPOLENS_AI_API_KEY=sk-ant-...
|
|
62
|
+
|
|
63
|
+
# Google Gemini
|
|
64
|
+
REPOLENS_AI_PRESET=google
|
|
65
|
+
REPOLENS_AI_API_KEY=...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **GitHub Models (free):** Set `REPOLENS_AI_PRESET=github` in GitHub Actions — uses `GITHUB_TOKEN` automatically, no separate API key required. See [AI.md](AI.md) for details.
|
|
47
69
|
|
|
48
70
|
## Telemetry
|
|
49
71
|
|
package/package.json
CHANGED
package/src/ai/provider.js
CHANGED
|
@@ -6,6 +6,33 @@ import { executeAIRequest } from "../utils/rate-limit.js";
|
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
7
7
|
const DEFAULT_MAX_TOKENS = 2500;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* AI Provider Presets - one env var to configure common providers.
|
|
11
|
+
* REPOLENS_AI_PRESET takes precedence over individual settings.
|
|
12
|
+
*/
|
|
13
|
+
const AI_PRESETS = {
|
|
14
|
+
github: {
|
|
15
|
+
provider: "github",
|
|
16
|
+
baseUrl: "https://models.inference.ai.azure.com",
|
|
17
|
+
model: "gpt-4o-mini",
|
|
18
|
+
},
|
|
19
|
+
openai: {
|
|
20
|
+
provider: "openai_compatible",
|
|
21
|
+
baseUrl: "https://api.openai.com/v1",
|
|
22
|
+
model: "gpt-4o-mini",
|
|
23
|
+
},
|
|
24
|
+
anthropic: {
|
|
25
|
+
provider: "anthropic",
|
|
26
|
+
baseUrl: "https://api.anthropic.com",
|
|
27
|
+
model: "claude-sonnet-4-20250514",
|
|
28
|
+
},
|
|
29
|
+
google: {
|
|
30
|
+
provider: "google",
|
|
31
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
32
|
+
model: "gemini-2.0-flash",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
9
36
|
export async function generateText({ system, user, temperature, maxTokens, config, jsonMode, jsonSchema }) {
|
|
10
37
|
// Check if AI is enabled (env var takes precedence, then config)
|
|
11
38
|
const aiConfig = config?.ai || {};
|
|
@@ -19,13 +46,21 @@ export async function generateText({ system, user, temperature, maxTokens, confi
|
|
|
19
46
|
};
|
|
20
47
|
}
|
|
21
48
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
const
|
|
49
|
+
// Check for preset (takes precedence over individual settings)
|
|
50
|
+
const preset = process.env.REPOLENS_AI_PRESET?.toLowerCase();
|
|
51
|
+
const presetConfig = preset ? AI_PRESETS[preset] : null;
|
|
52
|
+
|
|
53
|
+
if (preset && !presetConfig) {
|
|
54
|
+
warn(`Unknown AI preset "${preset}". Valid presets: ${Object.keys(AI_PRESETS).join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get provider configuration (preset > env vars > config > defaults)
|
|
58
|
+
const provider = presetConfig?.provider || process.env.REPOLENS_AI_PROVIDER || aiConfig.provider || "openai_compatible";
|
|
59
|
+
const baseUrl = presetConfig?.baseUrl || process.env.REPOLENS_AI_BASE_URL || aiConfig.base_url;
|
|
25
60
|
// For "github" provider, fall back to GITHUB_TOKEN when no explicit AI key is set
|
|
26
61
|
const apiKey = process.env.REPOLENS_AI_API_KEY
|
|
27
62
|
|| (provider === "github" ? process.env.GITHUB_TOKEN : undefined);
|
|
28
|
-
const model = process.env.REPOLENS_AI_MODEL || aiConfig.model || getDefaultModel(provider);
|
|
63
|
+
const model = presetConfig?.model || process.env.REPOLENS_AI_MODEL || aiConfig.model || getDefaultModel(provider);
|
|
29
64
|
const timeoutMs = parseInt(process.env.REPOLENS_AI_TIMEOUT_MS || aiConfig.timeout_ms || DEFAULT_TIMEOUT_MS);
|
|
30
65
|
|
|
31
66
|
// Use config values as fallback for maxTokens; temperature only when explicitly set
|
|
@@ -367,3 +367,89 @@ export function isFeatureEnabled(config, featureName, defaultValue = true) {
|
|
|
367
367
|
? config.features[featureName]
|
|
368
368
|
: defaultValue;
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
// Valid top-level config keys
|
|
372
|
+
const VALID_CONFIG_KEYS = [
|
|
373
|
+
"configVersion",
|
|
374
|
+
"project",
|
|
375
|
+
"publishers",
|
|
376
|
+
"scan",
|
|
377
|
+
"module_roots",
|
|
378
|
+
"outputs",
|
|
379
|
+
"notion",
|
|
380
|
+
"confluence",
|
|
381
|
+
"github_wiki",
|
|
382
|
+
"discord",
|
|
383
|
+
"features",
|
|
384
|
+
"ai",
|
|
385
|
+
"documentation",
|
|
386
|
+
"domains",
|
|
387
|
+
"plugins",
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Calculate Levenshtein distance between two strings.
|
|
392
|
+
*/
|
|
393
|
+
function levenshteinDistance(a, b) {
|
|
394
|
+
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
395
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
for (let i = 1; i <= a.length; i++) {
|
|
399
|
+
for (let j = 1; j <= b.length; j++) {
|
|
400
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
401
|
+
matrix[i][j] = Math.min(
|
|
402
|
+
matrix[i - 1][j] + 1, // deletion
|
|
403
|
+
matrix[i][j - 1] + 1, // insertion
|
|
404
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return matrix[a.length][b.length];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Find closest matching valid key for a typo.
|
|
414
|
+
* @param {string} typo - The potentially misspelled key
|
|
415
|
+
* @param {string[]} validKeys - Array of valid keys
|
|
416
|
+
* @param {number} maxDistance - Maximum Levenshtein distance to consider (default: 3)
|
|
417
|
+
* @returns {string|null} - Suggestion or null if no close match
|
|
418
|
+
*/
|
|
419
|
+
function findClosestKey(typo, validKeys, maxDistance = 3) {
|
|
420
|
+
let closest = null;
|
|
421
|
+
let minDist = Infinity;
|
|
422
|
+
|
|
423
|
+
for (const key of validKeys) {
|
|
424
|
+
const dist = levenshteinDistance(typo.toLowerCase(), key.toLowerCase());
|
|
425
|
+
if (dist < minDist && dist <= maxDistance) {
|
|
426
|
+
minDist = dist;
|
|
427
|
+
closest = key;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return closest;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Check config for typos in top-level keys.
|
|
436
|
+
* @param {object} config - The parsed config object
|
|
437
|
+
* @returns {Array<{key: string, suggestion: string}>} - List of detected typos with suggestions
|
|
438
|
+
*/
|
|
439
|
+
export function detectConfigTypos(config) {
|
|
440
|
+
const typos = [];
|
|
441
|
+
|
|
442
|
+
for (const key of Object.keys(config)) {
|
|
443
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
444
|
+
const suggestion = findClosestKey(key, VALID_CONFIG_KEYS);
|
|
445
|
+
if (suggestion) {
|
|
446
|
+
typos.push({ key, suggestion });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return typos;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Export constants for use in doctor
|
|
455
|
+
export { SUPPORTED_PUBLISHERS, SUPPORTED_PAGE_KEYS, VALID_CONFIG_KEYS };
|
|
@@ -99,7 +99,12 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Generate each document
|
|
102
|
+
const totalDocs = activeDocuments.length;
|
|
103
|
+
let docIndex = 0;
|
|
104
|
+
|
|
102
105
|
for (const docPlan of activeDocuments) {
|
|
106
|
+
docIndex++;
|
|
107
|
+
info(`[${docIndex}/${totalDocs}] Generating ${docPlan.key}...`);
|
|
103
108
|
let content = null;
|
|
104
109
|
|
|
105
110
|
try {
|
package/src/doctor.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
3
4
|
import { loadConfig } from "./core/config.js";
|
|
5
|
+
import { detectConfigTypos } from "./core/config-schema.js";
|
|
4
6
|
import { info } from "./utils/logger.js";
|
|
5
7
|
import { forceCheckForUpdates } from "./utils/update-check.js";
|
|
6
8
|
|
|
@@ -112,13 +114,34 @@ export async function runDoctor(targetDir = process.cwd()) {
|
|
|
112
114
|
let cfg = null;
|
|
113
115
|
|
|
114
116
|
if (await fileExists(repolensConfigPath)) {
|
|
117
|
+
// First parse YAML to check for typos (before full validation)
|
|
118
|
+
let rawConfig = null;
|
|
115
119
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
const rawYaml = await fs.readFile(repolensConfigPath, "utf8");
|
|
121
|
+
rawConfig = yaml.load(rawYaml);
|
|
122
|
+
|
|
123
|
+
// Check for config key typos before validation
|
|
124
|
+
if (rawConfig && typeof rawConfig === "object") {
|
|
125
|
+
const typos = detectConfigTypos(rawConfig);
|
|
126
|
+
for (const { key, suggestion } of typos) {
|
|
127
|
+
warn(`Unknown config key "${key}" — did you mean "${suggestion}"?`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (yamlError) {
|
|
131
|
+
fail(`RepoLens config has invalid YAML syntax: ${yamlError.message}`);
|
|
120
132
|
hasFailures = true;
|
|
121
133
|
}
|
|
134
|
+
|
|
135
|
+
// Now run full validation
|
|
136
|
+
if (rawConfig) {
|
|
137
|
+
try {
|
|
138
|
+
cfg = await loadConfig(repolensConfigPath);
|
|
139
|
+
ok("RepoLens config parsed successfully");
|
|
140
|
+
} catch (error) {
|
|
141
|
+
fail(`RepoLens config is invalid: ${error.message}`);
|
|
142
|
+
hasFailures = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
if (cfg) {
|
package/src/publishers/index.js
CHANGED
|
@@ -28,9 +28,9 @@ export async function publishDocs(cfg, renderedPages, scanResult, pluginManager
|
|
|
28
28
|
|
|
29
29
|
// --- Hash-based caching: skip unchanged documents ---
|
|
30
30
|
const cacheDir = path.join(process.cwd(), cfg.documentation?.output_dir || ".repolens");
|
|
31
|
-
const previousCache = await loadDocCache(cacheDir);
|
|
31
|
+
const { cache: previousCache, age: cacheAge } = await loadDocCache(cacheDir);
|
|
32
32
|
const { changedPages, unchangedKeys, newCache } = filterChangedDocs(renderedPages, previousCache);
|
|
33
|
-
logCacheStats(Object.keys(changedPages).length, unchangedKeys.length);
|
|
33
|
+
logCacheStats(Object.keys(changedPages).length, unchangedKeys.length, cacheAge);
|
|
34
34
|
|
|
35
35
|
// Use changedPages for API publishers (Notion / Confluence / Wiki), full set for Markdown
|
|
36
36
|
const pagesForAPIs = Object.keys(changedPages).length > 0 ? changedPages : renderedPages;
|
package/src/utils/doc-cache.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { info } from "./logger.js";
|
|
9
|
+
import { info, verbose } from "./logger.js";
|
|
10
10
|
|
|
11
11
|
const CACHE_FILENAME = "doc-hashes.json";
|
|
12
12
|
|
|
@@ -19,14 +19,19 @@ function hashContent(content) {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Load the previous cache from disk.
|
|
22
|
-
* @returns {Record<string, string
|
|
22
|
+
* @returns {{ cache: Record<string, string>, age: number|null }} Map of docKey → contentHash and cache age in ms
|
|
23
23
|
*/
|
|
24
24
|
export async function loadDocCache(cacheDir) {
|
|
25
|
+
const cachePath = path.join(cacheDir, CACHE_FILENAME);
|
|
25
26
|
try {
|
|
26
|
-
const raw = await
|
|
27
|
-
|
|
27
|
+
const [raw, stat] = await Promise.all([
|
|
28
|
+
fs.readFile(cachePath, "utf8"),
|
|
29
|
+
fs.stat(cachePath),
|
|
30
|
+
]);
|
|
31
|
+
const age = Date.now() - stat.mtimeMs;
|
|
32
|
+
return { cache: JSON.parse(raw), age };
|
|
28
33
|
} catch {
|
|
29
|
-
return {};
|
|
34
|
+
return { cache: {}, age: null };
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -65,14 +70,25 @@ export function filterChangedDocs(renderedPages, previousCache) {
|
|
|
65
70
|
return { changedPages, unchangedKeys, newCache };
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Format duration in human-readable form.
|
|
75
|
+
*/
|
|
76
|
+
function formatAge(ms) {
|
|
77
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s ago`;
|
|
78
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}m ago`;
|
|
79
|
+
if (ms < 86400000) return `${Math.round(ms / 3600000)}h ago`;
|
|
80
|
+
return `${Math.round(ms / 86400000)}d ago`;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
/**
|
|
69
84
|
* Log cache statistics.
|
|
70
85
|
*/
|
|
71
|
-
export function logCacheStats(changedCount, unchangedCount) {
|
|
86
|
+
export function logCacheStats(changedCount, unchangedCount, cacheAge = null) {
|
|
72
87
|
const total = changedCount + unchangedCount;
|
|
88
|
+
const ageStr = cacheAge ? ` (last run: ${formatAge(cacheAge)})` : "";
|
|
73
89
|
if (unchangedCount > 0) {
|
|
74
|
-
info(`Cache: ${unchangedCount}/${total} documents unchanged, skipping. ${changedCount} to publish
|
|
90
|
+
info(`Cache: ${unchangedCount}/${total} documents unchanged, skipping. ${changedCount} to publish.${ageStr}`);
|
|
75
91
|
} else {
|
|
76
|
-
info(`Cache: All ${total} documents changed or new
|
|
92
|
+
info(`Cache: All ${total} documents changed or new.${ageStr}`);
|
|
77
93
|
}
|
|
78
94
|
}
|
package/src/utils/logger.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sanitizeSecrets } from "./secrets.js";
|
|
2
2
|
|
|
3
|
-
const isVerbose = process.argv.includes("--verbose");
|
|
3
|
+
export const isVerbose = process.argv.includes("--verbose");
|
|
4
4
|
const isTest = process.env.NODE_ENV === "test";
|
|
5
5
|
|
|
6
6
|
// Terminal color support detection
|
|
@@ -71,6 +71,16 @@ export function log(...args) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Verbose-only logging (alias for log, for semantic clarity).
|
|
76
|
+
* Only outputs when --verbose flag is present.
|
|
77
|
+
*/
|
|
78
|
+
export function verbose(...args) {
|
|
79
|
+
if (!isTest && isVerbose) {
|
|
80
|
+
console.log("[RepoLens]", ...sanitizeArgs(args));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
export function info(...args) {
|
|
75
85
|
if (!isTest) {
|
|
76
86
|
console.log("[RepoLens]", ...sanitizeArgs(args));
|