arol-ai 0.1.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/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # arol-ai
2
+
3
+ Scan a local code repo for usage of third-party APIs/SDKs that have **upcoming deprecations**, and print a clean report.
4
+
5
+ **Everything runs locally.** No network calls, no telemetry, no uploads, no auth. Your code never leaves your machine.
6
+
7
+ ```
8
+ npx arol-ai scan
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Quick start
14
+
15
+ Scan the current directory:
16
+
17
+ ```sh
18
+ npx arol-ai scan
19
+ ```
20
+
21
+ Scan a specific path:
22
+
23
+ ```sh
24
+ npx arol-ai scan ./path/to/repo
25
+ ```
26
+
27
+ Install globally (optional):
28
+
29
+ ```sh
30
+ npm install -g arol-ai
31
+ arol-ai scan
32
+ ```
33
+
34
+ ## Example output
35
+
36
+ ```
37
+ arol · local deprecation scan
38
+ Scanned 128 files · 3 APIs detected
39
+
40
+ ⚠ 3 deprecations found (2 high, 1 medium)
41
+
42
+ ● AWS · AWS SDK for JavaScript v2 end-of-support HIGH
43
+ sunsets 2025-09-08 (passed 269 days ago)
44
+ The monolithic 'aws-sdk' (v2) entered maintenance mode in 2024 and reached
45
+ end-of-support. Migrate to the modular AWS SDK for JavaScript v3.
46
+ found in:
47
+ package.json → aws-sdk@^2.1400.0
48
+ src/storage.ts:1, 42
49
+ → migrate: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrating-to-v3.html
50
+
51
+ ...
52
+
53
+ These are today's deprecations. New ones land constantly — get
54
+ alerted before the next one breaks you → arol.ai
55
+ ```
56
+
57
+ When nothing is found:
58
+
59
+ ```
60
+ ✓ No upcoming deprecations detected in your stack.
61
+ ```
62
+
63
+ ## How detection works
64
+
65
+ For every entry in the dataset, `arol-ai` runs two independent checks:
66
+
67
+ 1. **Manifest scan** — parses the repo's root manifests for declared dependencies and their versions:
68
+ - `package.json` (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, plus simple npm `workspaces`)
69
+ - `requirements.txt` (Python)
70
+ - `go.mod` (Go)
71
+
72
+ A dependency matches if its name equals one of the entry's `detect.sdk` names (case-insensitively, with PyPI-style `_ . -` normalization).
73
+
74
+ 2. **Inline scan** — walks source files and regex-matches each entry's `detect.patterns`, recording the file paths and line numbers.
75
+ - Extensions scanned: `.js .mjs .cjs .jsx .ts .mts .cts .tsx .py .go`
76
+ - Skipped directories: `node_modules`, `.git`, `dist`, `build`, `.next`, `out`, `coverage`, `.venv`, `venv`, `vendor`
77
+
78
+ A deprecation is **detected** if its SDK is present **OR** any of its patterns match. Both kinds of evidence are shown in the report.
79
+
80
+ ## CLI
81
+
82
+ ```
83
+ arol-ai scan [path] [options]
84
+ ```
85
+
86
+ | Option | Description |
87
+ | --- | --- |
88
+ | `[path]` | Directory to scan (default: `.`) |
89
+ | `--json` | Output machine-readable JSON instead of the report |
90
+ | `--no-color` | Disable colored output (also respects `NO_COLOR`) |
91
+ | `--data <file>` | Use a custom `deprecations.json` instead of the bundled one |
92
+ | `--fail-on <severity>` | Exit non-zero if findings meet a level: `high` \| `medium` \| `low` \| `any` \| `none` (default `none`) |
93
+ | `-v, --version` | Print the version |
94
+ | `-h, --help` | Show help |
95
+
96
+ `scan` is the default command, so `arol-ai ./repo` works too.
97
+
98
+ **Exit codes:** `0` success · `1` `--fail-on` threshold met · `2` bad path or dataset error. The `--fail-on` flag makes `arol-ai` useful as a CI gate:
99
+
100
+ ```sh
101
+ npx arol-ai scan --fail-on high
102
+ ```
103
+
104
+ Colors are automatically disabled when output is not a TTY (e.g. piped to a file), or when `NO_COLOR` is set. Use `FORCE_COLOR=1` to force them on.
105
+
106
+ ## The dataset (`deprecations.json`)
107
+
108
+ All detections are **data-driven** — the bundled dataset lives at
109
+ [`src/data/deprecations.json`](src/data/deprecations.json) and can be extended **without changing any code**. Add an entry and re-run; or keep your own file and pass it with `--data ./my-deprecations.json`.
110
+
111
+ ### Schema
112
+
113
+ ```jsonc
114
+ {
115
+ "schema_version": 1, // optional, informational
116
+ "updated": "2026-06-01", // optional, informational
117
+ "deprecations": [
118
+ {
119
+ "id": "aws-sdk-js-v2-eos", // unique, stable identifier (required)
120
+ "vendor": "AWS", // who owns the API (required)
121
+ "title": "AWS SDK for JavaScript v2 end-of-support", // short headline (required)
122
+ "severity": "high", // "high" | "medium" | "low" (required)
123
+ "sunset_date": "2025-09-08",// ISO YYYY-MM-DD, or "" if no fixed date
124
+ "detect": {
125
+ "sdk": ["aws-sdk"], // dependency/module names to find in manifests
126
+ "patterns": [ // regex strings matched against source files
127
+ "require\\(\\s*['\"]aws-sdk['\"]\\s*\\)",
128
+ "from\\s+['\"]aws-sdk['\"]"
129
+ ]
130
+ },
131
+ "migration_url": "https://docs.aws.amazon.com/.../migrating-to-v3.html",
132
+ "summary": "One or two sentences explaining the change and what to do."
133
+ }
134
+ ]
135
+ }
136
+ ```
137
+
138
+ A bare top-level array (`[ { ...entry }, ... ]`) is also accepted.
139
+
140
+ ### Field reference
141
+
142
+ | Field | Type | Required | Notes |
143
+ | --- | --- | --- | --- |
144
+ | `id` | string | ✓ | Unique, stable slug. |
145
+ | `vendor` | string | ✓ | Displayed before the title. |
146
+ | `title` | string | ✓ | Short headline for the finding. |
147
+ | `severity` | `"high"` \| `"medium"` \| `"low"` | ✓ | Drives color, sort order, and `--fail-on`. |
148
+ | `sunset_date` | string | – | ISO `YYYY-MM-DD`. Use `""` for unmaintained/no-fixed-date items; the report shows a relative hint (e.g. *"in 42 days"* / *"passed 12 days ago"*). |
149
+ | `detect.sdk` | string[] | – | Manifest dependency/module names. May be empty for pattern-only detection. |
150
+ | `detect.patterns` | string[] | – | **JSON-escaped** regular-expression strings (so `\d` becomes `\\d`). Matched per source file; invalid regexes are skipped safely. May be empty for manifest-only detection. |
151
+ | `migration_url` | string | – | Link shown in the report. |
152
+ | `summary` | string | – | One or two sentences of guidance. |
153
+
154
+ > An entry with **both** `detect.sdk` and `detect.patterns` empty can never match and is ignored.
155
+
156
+ ### Writing good patterns
157
+
158
+ - Patterns are matched **case-sensitively** with the global flag over each file's contents; line numbers are reported.
159
+ - Escape backslashes for JSON: a regex `\bimport\b` is written `"\\bimport\\b"`.
160
+ - Prefer specific anchors (`require\(\s*['"]name['"]\s*\)`, `from\s+['"]name['"]`) over bare package names to avoid false positives.
161
+ - Avoid `^`/`$` line anchors — matching runs against the whole file, not line-by-line; use `\b` word boundaries instead.
162
+
163
+ ## Development
164
+
165
+ ```sh
166
+ npm install
167
+ npm run build # tsc -> dist/
168
+ node dist/cli.js scan ./some/repo
169
+ npm run scan -- ./some/repo
170
+ ```
171
+
172
+ Source layout:
173
+
174
+ | File | Responsibility |
175
+ | --- | --- |
176
+ | `src/cli.ts` | Argument parsing (`commander`), output mode, exit codes |
177
+ | `src/scanner.ts` | Orchestrates manifest + inline scans, combines findings |
178
+ | `src/manifests.ts` | Parsers for `package.json`, `requirements.txt`, `go.mod` |
179
+ | `src/report.ts` | Colorized terminal report rendering |
180
+ | `src/data.ts` | Loads & validates the dataset |
181
+ | `src/data/deprecations.json` | The bundled, extensible dataset |
182
+ | `src/types.ts` | Shared type definitions |
183
+
184
+ ## Privacy
185
+
186
+ `arol-ai` makes **zero** network requests. It reads files under the path you point it at, matches them against a local JSON dataset, and prints to your terminal. Nothing is uploaded, logged remotely, or phoned home.
187
+
188
+ ## License
189
+
190
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const commander_1 = require("commander");
40
+ const data_1 = require("./data");
41
+ const scanner_1 = require("./scanner");
42
+ const report_1 = require("./report");
43
+ /** Read this package's version without importing across the rootDir boundary. */
44
+ function readVersion() {
45
+ try {
46
+ const pkgPath = path.join(__dirname, "..", "package.json");
47
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
48
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
49
+ }
50
+ catch {
51
+ return "0.0.0";
52
+ }
53
+ }
54
+ function shouldUseColor(colorFlag) {
55
+ // commander sets colorFlag=false when --no-color is passed.
56
+ if (!colorFlag)
57
+ return false;
58
+ if (process.env.NO_COLOR)
59
+ return false;
60
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
61
+ return true;
62
+ return Boolean(process.stdout.isTTY);
63
+ }
64
+ const SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
65
+ function runScan(targetPath, opts) {
66
+ const root = path.resolve(targetPath ?? ".");
67
+ // Validate the target directory up front for a friendly error.
68
+ let stat;
69
+ try {
70
+ stat = fs.statSync(root);
71
+ }
72
+ catch {
73
+ process.stderr.write(`arol: path not found: ${root}\n`);
74
+ process.exitCode = 2;
75
+ return;
76
+ }
77
+ if (!stat.isDirectory()) {
78
+ process.stderr.write(`arol: not a directory: ${root}\n`);
79
+ process.exitCode = 2;
80
+ return;
81
+ }
82
+ let deprecations;
83
+ try {
84
+ deprecations = (0, data_1.loadDeprecations)(opts.data);
85
+ }
86
+ catch (err) {
87
+ process.stderr.write(`arol: ${err.message}\n`);
88
+ process.exitCode = 2;
89
+ return;
90
+ }
91
+ const result = (0, scanner_1.scanRepo)(root, deprecations);
92
+ if (opts.json) {
93
+ const counts = { high: 0, medium: 0, low: 0 };
94
+ for (const f of result.findings)
95
+ counts[f.deprecation.severity]++;
96
+ const payload = {
97
+ scannedFiles: result.scannedFiles,
98
+ manifestsScanned: result.manifestsScanned,
99
+ detected: result.findings.length,
100
+ counts,
101
+ findings: result.findings.map((f) => ({
102
+ id: f.deprecation.id,
103
+ vendor: f.deprecation.vendor,
104
+ title: f.deprecation.title,
105
+ severity: f.deprecation.severity,
106
+ sunset_date: f.deprecation.sunset_date,
107
+ migration_url: f.deprecation.migration_url,
108
+ summary: f.deprecation.summary,
109
+ manifestMatches: f.manifestMatches,
110
+ patternMatches: f.patternMatches,
111
+ })),
112
+ };
113
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
114
+ }
115
+ else {
116
+ const report = (0, report_1.renderReport)(result, { color: shouldUseColor(opts.color) });
117
+ process.stdout.write(report + "\n");
118
+ }
119
+ // Optional CI gate: exit non-zero if a finding meets/exceeds the threshold.
120
+ const failOn = opts.failOn?.toLowerCase();
121
+ if (failOn && failOn !== "none") {
122
+ const threshold = failOn === "any"
123
+ ? 1
124
+ : SEVERITY_RANK[failOn] ?? Number.POSITIVE_INFINITY;
125
+ const tripped = result.findings.some((f) => SEVERITY_RANK[f.deprecation.severity] >= threshold);
126
+ if (tripped)
127
+ process.exitCode = 1;
128
+ }
129
+ }
130
+ function main(argv) {
131
+ const program = new commander_1.Command();
132
+ program
133
+ .name("arol-ai")
134
+ .description("Scan a local repo for upcoming third-party API/SDK deprecations.\n" +
135
+ "Everything runs locally — no network, no telemetry, your code never leaves the machine.")
136
+ .version(readVersion(), "-v, --version", "print the arol-ai version");
137
+ program
138
+ .command("scan", { isDefault: true })
139
+ .argument("[path]", "directory to scan", ".")
140
+ .description("scan a repository and print a deprecation report")
141
+ .option("--json", "output machine-readable JSON instead of the report")
142
+ .option("--no-color", "disable colored output")
143
+ .option("--data <file>", "use a custom deprecations.json dataset instead of the bundled one")
144
+ .option("--fail-on <severity>", "exit non-zero if findings meet this level: high | medium | low | any | none", "none")
145
+ .action((pathArg, options) => {
146
+ runScan(pathArg, options);
147
+ });
148
+ program.parse(argv);
149
+ }
150
+ main(process.argv);
package/dist/data.js ADDED
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadDeprecations = loadDeprecations;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const SEVERITIES = ["high", "medium", "low"];
40
+ /**
41
+ * Locate the bundled deprecations.json. Tries several candidate locations so the
42
+ * tool works both when running the compiled output (dist/) from a published
43
+ * package and when running straight from source.
44
+ */
45
+ function defaultDataPath() {
46
+ const candidates = [
47
+ // Copied alongside compiled output (if a build step does so).
48
+ path.join(__dirname, "data", "deprecations.json"),
49
+ // Published layout: dist/data.js resolving back up to src/data/.
50
+ path.join(__dirname, "..", "src", "data", "deprecations.json"),
51
+ // Running directly from src/ (e.g. ts-node).
52
+ path.join(__dirname, "..", "data", "deprecations.json"),
53
+ ];
54
+ for (const candidate of candidates) {
55
+ if (fs.existsSync(candidate))
56
+ return candidate;
57
+ }
58
+ // Fall back to the first candidate so the error message is meaningful.
59
+ return candidates[0];
60
+ }
61
+ function isNonEmptyString(value) {
62
+ return typeof value === "string" && value.length > 0;
63
+ }
64
+ function isStringArray(value) {
65
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
66
+ }
67
+ /** Validate and normalize a single raw entry, or return null if it is malformed. */
68
+ function coerceDeprecation(raw) {
69
+ if (typeof raw !== "object" || raw === null)
70
+ return null;
71
+ const r = raw;
72
+ if (!isNonEmptyString(r.id))
73
+ return null;
74
+ if (!isNonEmptyString(r.vendor))
75
+ return null;
76
+ if (!isNonEmptyString(r.title))
77
+ return null;
78
+ if (!SEVERITIES.includes(r.severity))
79
+ return null;
80
+ const detect = r.detect;
81
+ const sdk = detect && isStringArray(detect.sdk) ? detect.sdk : [];
82
+ const patterns = detect && isStringArray(detect.patterns) ? detect.patterns : [];
83
+ // An entry with neither SDKs nor patterns can never match — drop it.
84
+ if (sdk.length === 0 && patterns.length === 0)
85
+ return null;
86
+ return {
87
+ id: r.id,
88
+ vendor: r.vendor,
89
+ title: r.title,
90
+ severity: r.severity,
91
+ sunset_date: typeof r.sunset_date === "string" ? r.sunset_date : "",
92
+ detect: { sdk, patterns },
93
+ migration_url: typeof r.migration_url === "string" ? r.migration_url : "",
94
+ summary: typeof r.summary === "string" ? r.summary : "",
95
+ };
96
+ }
97
+ /**
98
+ * Load and validate the deprecations dataset.
99
+ * @param customPath optional path to an alternative dataset file.
100
+ */
101
+ function loadDeprecations(customPath) {
102
+ const file = customPath ? path.resolve(customPath) : defaultDataPath();
103
+ let contents;
104
+ try {
105
+ contents = fs.readFileSync(file, "utf8");
106
+ }
107
+ catch {
108
+ throw new Error(`Could not read deprecations dataset at: ${file}`);
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(contents);
113
+ }
114
+ catch (err) {
115
+ throw new Error(`deprecations dataset is not valid JSON (${file}): ${err.message}`);
116
+ }
117
+ // Accept either the full { deprecations: [...] } shape or a bare array.
118
+ const rawList = Array.isArray(parsed)
119
+ ? parsed
120
+ : parsed?.deprecations;
121
+ if (!Array.isArray(rawList)) {
122
+ throw new Error(`deprecations dataset must contain a "deprecations" array (${file}).`);
123
+ }
124
+ const deprecations = [];
125
+ for (const raw of rawList) {
126
+ const entry = coerceDeprecation(raw);
127
+ if (entry)
128
+ deprecations.push(entry);
129
+ }
130
+ return deprecations;
131
+ }
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.normalizeName = normalizeName;
40
+ exports.nameMatches = nameMatches;
41
+ exports.parsePackageJson = parsePackageJson;
42
+ exports.packageJsonWorkspaces = packageJsonWorkspaces;
43
+ exports.parseRequirements = parseRequirements;
44
+ exports.parseGoMod = parseGoMod;
45
+ exports.collectManifestDeps = collectManifestDeps;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const fast_glob_1 = __importDefault(require("fast-glob"));
49
+ /**
50
+ * Normalize a name for tolerant comparison. PyPI treats runs of `_ . -` as
51
+ * equivalent and is case-insensitive; npm names are lowercase; Go module paths
52
+ * keep their slashes (untouched here) so identical paths still compare equal.
53
+ */
54
+ function normalizeName(name) {
55
+ return name.toLowerCase().replace(/[_.-]+/g, "-");
56
+ }
57
+ /** True if a manifest dependency name matches one of a deprecation's SDK names. */
58
+ function nameMatches(sdk, depName) {
59
+ if (sdk.toLowerCase() === depName.toLowerCase())
60
+ return true;
61
+ return normalizeName(sdk) === normalizeName(depName);
62
+ }
63
+ function readFileSafe(file) {
64
+ try {
65
+ return fs.readFileSync(file, "utf8");
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ const PACKAGE_JSON_DEP_FIELDS = [
72
+ "dependencies",
73
+ "devDependencies",
74
+ "peerDependencies",
75
+ "optionalDependencies",
76
+ ];
77
+ /** Parse a package.json's dependency maps into PkgRefs. */
78
+ function parsePackageJson(content, source) {
79
+ const refs = [];
80
+ let json;
81
+ try {
82
+ json = JSON.parse(content);
83
+ }
84
+ catch {
85
+ return refs;
86
+ }
87
+ for (const field of PACKAGE_JSON_DEP_FIELDS) {
88
+ const deps = json[field];
89
+ if (typeof deps !== "object" || deps === null)
90
+ continue;
91
+ for (const [name, version] of Object.entries(deps)) {
92
+ refs.push({
93
+ name,
94
+ version: typeof version === "string" ? version : null,
95
+ source,
96
+ });
97
+ }
98
+ }
99
+ return refs;
100
+ }
101
+ /** Extract workspace glob patterns from a root package.json, if any. */
102
+ function packageJsonWorkspaces(content) {
103
+ try {
104
+ const json = JSON.parse(content);
105
+ const ws = json.workspaces;
106
+ if (Array.isArray(ws))
107
+ return ws.filter((w) => typeof w === "string");
108
+ if (ws && typeof ws === "object") {
109
+ const packages = ws.packages;
110
+ if (Array.isArray(packages)) {
111
+ return packages.filter((w) => typeof w === "string");
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ /* ignore */
117
+ }
118
+ return [];
119
+ }
120
+ /** Parse a requirements.txt into PkgRefs. */
121
+ function parseRequirements(content, source) {
122
+ const refs = [];
123
+ // name, optional [extras], optional (operator + version).
124
+ const lineRe = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*((?:==|===|>=|<=|~=|!=|<|>)\s*[^\s;#]+)?/;
125
+ for (const rawLine of content.split(/\r?\n/)) {
126
+ // Strip inline comments and surrounding whitespace.
127
+ let line = rawLine.replace(/\s+#.*$/, "").trim();
128
+ if (!line || line.startsWith("#"))
129
+ continue;
130
+ // Skip pip options (-r, -e, --hash, ...) and direct URLs / VCS installs.
131
+ if (line.startsWith("-"))
132
+ continue;
133
+ if (/^[a-z+]+:\/\//i.test(line) || line.includes("@ "))
134
+ continue;
135
+ const m = lineRe.exec(line);
136
+ if (!m)
137
+ continue;
138
+ const name = m[1];
139
+ const version = m[2] ? m[2].replace(/\s+/g, "") : null;
140
+ refs.push({ name, version, source });
141
+ }
142
+ return refs;
143
+ }
144
+ /** Parse a go.mod file's require directives into PkgRefs. */
145
+ function parseGoMod(content, source) {
146
+ const refs = [];
147
+ const lines = content.split(/\r?\n/);
148
+ let inBlock = false;
149
+ const pushModule = (modPart) => {
150
+ // modPart looks like: "github.com/foo/bar v1.2.3 // indirect"
151
+ const cleaned = modPart.replace(/\/\/.*$/, "").trim();
152
+ if (!cleaned)
153
+ return;
154
+ const parts = cleaned.split(/\s+/);
155
+ if (parts.length < 2)
156
+ return;
157
+ refs.push({ name: parts[0], version: parts[1], source });
158
+ };
159
+ for (const rawLine of lines) {
160
+ const line = rawLine.trim();
161
+ if (!line)
162
+ continue;
163
+ if (inBlock) {
164
+ if (line.startsWith(")")) {
165
+ inBlock = false;
166
+ continue;
167
+ }
168
+ pushModule(line);
169
+ continue;
170
+ }
171
+ if (/^require\s*\(\s*$/.test(line)) {
172
+ inBlock = true;
173
+ continue;
174
+ }
175
+ const single = /^require\s+(.+)$/.exec(line);
176
+ if (single)
177
+ pushModule(single[1]);
178
+ }
179
+ return refs;
180
+ }
181
+ /**
182
+ * Collect every dependency declared in the repo's root manifests
183
+ * (package.json, requirements.txt, go.mod) plus simple npm workspaces.
184
+ * Returns the dependency refs and the list of manifest files that were read.
185
+ */
186
+ function collectManifestDeps(root) {
187
+ const refs = [];
188
+ const manifests = [];
189
+ const addManifest = (absPath, parse) => {
190
+ const content = readFileSafe(absPath);
191
+ if (content === null)
192
+ return content;
193
+ const rel = path.relative(root, absPath) || path.basename(absPath);
194
+ manifests.push(rel);
195
+ refs.push(...parse(content, rel));
196
+ return content;
197
+ };
198
+ // Root package.json (+ workspaces).
199
+ const rootPkgPath = path.join(root, "package.json");
200
+ const rootPkgContent = addManifest(rootPkgPath, parsePackageJson);
201
+ if (rootPkgContent) {
202
+ const patterns = packageJsonWorkspaces(rootPkgContent);
203
+ if (patterns.length > 0) {
204
+ let wsPkgPaths = [];
205
+ try {
206
+ wsPkgPaths = fast_glob_1.default.sync(patterns.map((p) => `${p.replace(/\/+$/, "")}/package.json`), {
207
+ cwd: root,
208
+ absolute: true,
209
+ ignore: ["**/node_modules/**"],
210
+ suppressErrors: true,
211
+ });
212
+ }
213
+ catch {
214
+ wsPkgPaths = [];
215
+ }
216
+ for (const wsPath of wsPkgPaths) {
217
+ if (path.resolve(wsPath) === path.resolve(rootPkgPath))
218
+ continue;
219
+ addManifest(wsPath, parsePackageJson);
220
+ }
221
+ }
222
+ }
223
+ // Python and Go root manifests.
224
+ addManifest(path.join(root, "requirements.txt"), parseRequirements);
225
+ addManifest(path.join(root, "go.mod"), parseGoMod);
226
+ return { refs, manifests };
227
+ }
package/dist/report.js ADDED
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderReport = renderReport;
4
+ function makeStyler(enabled) {
5
+ const wrap = (open, close) => (s) => enabled ? `\x1b[${open}m${s}\x1b[${close}m` : s;
6
+ return {
7
+ enabled,
8
+ bold: wrap(1, 22),
9
+ dim: wrap(2, 22),
10
+ underline: wrap(4, 24),
11
+ red: wrap(31, 39),
12
+ green: wrap(32, 39),
13
+ yellow: wrap(33, 39),
14
+ blue: wrap(34, 39),
15
+ cyan: wrap(36, 39),
16
+ gray: wrap(90, 39),
17
+ white: wrap(97, 39),
18
+ black: wrap(30, 39),
19
+ bgRed: wrap(41, 49),
20
+ bgYellow: wrap(43, 49),
21
+ bgBlue: wrap(44, 49),
22
+ };
23
+ }
24
+ const SEVERITY_ORDER = { high: 0, medium: 1, low: 2 };
25
+ function severityColor(s, sev) {
26
+ if (sev === "high")
27
+ return s.red;
28
+ if (sev === "medium")
29
+ return s.yellow;
30
+ return s.blue;
31
+ }
32
+ /** A colored pill like ` HIGH `. */
33
+ function severityPill(s, sev) {
34
+ const label = ` ${sev.toUpperCase()} `;
35
+ if (!s.enabled)
36
+ return `[${sev.toUpperCase()}]`;
37
+ if (sev === "high")
38
+ return s.bgRed(s.white(s.bold(label)));
39
+ if (sev === "medium")
40
+ return s.bgYellow(s.black(s.bold(label)));
41
+ return s.bgBlue(s.white(s.bold(label)));
42
+ }
43
+ /** Render the sunset date with a relative hint, or a note when there is no date. */
44
+ function sunsetPhrase(s, sunsetDate, now) {
45
+ if (!sunsetDate) {
46
+ return s.gray("no fixed sunset date — already deprecated / unmaintained");
47
+ }
48
+ const parsed = new Date(`${sunsetDate}T00:00:00Z`);
49
+ if (Number.isNaN(parsed.getTime())) {
50
+ return s.gray(`sunsets ${sunsetDate}`);
51
+ }
52
+ const msPerDay = 24 * 60 * 60 * 1000;
53
+ const days = Math.round((parsed.getTime() - now.getTime()) / msPerDay);
54
+ let rel;
55
+ if (days > 1)
56
+ rel = `in ${days} days`;
57
+ else if (days === 1)
58
+ rel = "in 1 day";
59
+ else if (days === 0)
60
+ rel = "today";
61
+ else if (days === -1)
62
+ rel = "1 day ago";
63
+ else
64
+ rel = `${Math.abs(days)} days ago`;
65
+ const base = `sunsets ${sunsetDate}`;
66
+ const hint = days <= 0 ? ` (passed ${rel})` : ` (${rel})`;
67
+ // Past or imminent sunsets are the urgent ones.
68
+ return days <= 30 ? s.red(base + hint) : s.yellow(base + hint);
69
+ }
70
+ /** Group pattern matches by file into "path:line, line" summaries. */
71
+ function formatPatternMatches(s, finding) {
72
+ const byFile = new Map();
73
+ for (const pm of finding.patternMatches) {
74
+ const arr = byFile.get(pm.file) ?? [];
75
+ arr.push(pm.line);
76
+ byFile.set(pm.file, arr);
77
+ }
78
+ const lines = [];
79
+ for (const [file, nums] of byFile) {
80
+ const uniqueSorted = Array.from(new Set(nums)).sort((a, b) => a - b);
81
+ const shown = uniqueSorted.slice(0, 8);
82
+ const more = uniqueSorted.length > shown.length
83
+ ? s.gray(` +${uniqueSorted.length - shown.length} more`)
84
+ : "";
85
+ lines.push(`${s.cyan(file)}${s.gray(":")}${shown.join(s.gray(", "))}${more}`);
86
+ }
87
+ return lines;
88
+ }
89
+ /** Render the full human-readable terminal report. */
90
+ function renderReport(result, opts) {
91
+ const s = makeStyler(opts.color);
92
+ const now = opts.now ?? new Date();
93
+ const out = [];
94
+ const findings = [...result.findings].sort((a, b) => {
95
+ const sevDiff = SEVERITY_ORDER[a.deprecation.severity] -
96
+ SEVERITY_ORDER[b.deprecation.severity];
97
+ if (sevDiff !== 0)
98
+ return sevDiff;
99
+ // Within a severity, soonest/earliest sunset first; undated last.
100
+ const da = a.deprecation.sunset_date || "9999-99-99";
101
+ const db = b.deprecation.sunset_date || "9999-99-99";
102
+ return da.localeCompare(db);
103
+ });
104
+ // Header.
105
+ out.push("");
106
+ out.push(s.bold("arol") +
107
+ s.dim(" · local deprecation scan"));
108
+ const fileWord = result.scannedFiles === 1 ? "file" : "files";
109
+ const apiWord = findings.length === 1 ? "API" : "APIs";
110
+ out.push(s.gray(`Scanned ${result.scannedFiles} ${fileWord} · ${findings.length} ${apiWord} detected`));
111
+ out.push("");
112
+ if (findings.length === 0) {
113
+ out.push(s.green(s.bold("✓ No upcoming deprecations detected in your stack.")));
114
+ out.push("");
115
+ out.push(footer(s));
116
+ return out.join("\n");
117
+ }
118
+ // Severity summary.
119
+ const counts = { high: 0, medium: 0, low: 0 };
120
+ for (const f of findings)
121
+ counts[f.deprecation.severity]++;
122
+ const summaryParts = [
123
+ s.red(`${counts.high} high`),
124
+ s.yellow(`${counts.medium} medium`),
125
+ s.blue(`${counts.low} low`),
126
+ ];
127
+ const noun = findings.length === 1 ? "deprecation" : "deprecations";
128
+ out.push(s.bold(s.yellow("⚠ ")) +
129
+ s.bold(`${findings.length} ${noun} found`) +
130
+ s.gray(` (${summaryParts.join(s.gray(", "))})`));
131
+ out.push("");
132
+ // Per finding.
133
+ for (const finding of findings) {
134
+ const d = finding.deprecation;
135
+ const sevColor = severityColor(s, d.severity);
136
+ out.push(`${sevColor("●")} ${s.bold(d.vendor)} ${s.gray("·")} ${d.title} ${severityPill(s, d.severity)}`);
137
+ out.push(` ${sunsetPhrase(s, d.sunset_date, now)}`);
138
+ if (d.summary) {
139
+ out.push(` ${s.dim(wrapText(d.summary, 76, " ").trimStart())}`);
140
+ }
141
+ // Evidence.
142
+ out.push(` ${s.gray("found in:")}`);
143
+ for (const mm of finding.manifestMatches) {
144
+ const ver = mm.version
145
+ ? s.gray("@") + mm.version
146
+ : s.gray(" (declared, no version)");
147
+ out.push(` ${s.cyan(mm.manifest)} ${s.gray("→")} ${mm.sdk}${ver}`);
148
+ }
149
+ for (const line of formatPatternMatches(s, finding)) {
150
+ out.push(` ${line}`);
151
+ }
152
+ if (d.migration_url) {
153
+ out.push(` ${s.gray("→ migrate:")} ${s.underline(s.cyan(d.migration_url))}`);
154
+ }
155
+ out.push("");
156
+ }
157
+ out.push(footer(s));
158
+ return out.join("\n");
159
+ }
160
+ function footer(s) {
161
+ return [
162
+ s.dim("─".repeat(60)),
163
+ s.dim("These are today's deprecations. New ones land constantly — get"),
164
+ s.dim("alerted before the next one breaks you → ") + s.cyan(s.bold("arol.ai")),
165
+ ].join("\n");
166
+ }
167
+ /** Soft-wrap text to a width, indenting continuation lines. */
168
+ function wrapText(text, width, indent) {
169
+ const words = text.split(/\s+/);
170
+ const lines = [];
171
+ let current = "";
172
+ for (const word of words) {
173
+ if (current.length + word.length + 1 > width && current.length > 0) {
174
+ lines.push(current);
175
+ current = word;
176
+ }
177
+ else {
178
+ current = current ? `${current} ${word}` : word;
179
+ }
180
+ }
181
+ if (current)
182
+ lines.push(current);
183
+ return lines.map((l, i) => (i === 0 ? l : indent + l)).join("\n");
184
+ }
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.scanRepo = scanRepo;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const fast_glob_1 = __importDefault(require("fast-glob"));
43
+ const manifests_1 = require("./manifests");
44
+ /** Source file extensions that get the inline regex scan. */
45
+ const SOURCE_EXTENSIONS = [
46
+ "js",
47
+ "mjs",
48
+ "cjs",
49
+ "jsx",
50
+ "ts",
51
+ "mts",
52
+ "cts",
53
+ "tsx",
54
+ "py",
55
+ "go",
56
+ ];
57
+ /** Directories never worth walking. */
58
+ const IGNORED_DIRS = [
59
+ "node_modules",
60
+ ".git",
61
+ "dist",
62
+ "build",
63
+ ".next",
64
+ "out",
65
+ "coverage",
66
+ ".venv",
67
+ "venv",
68
+ "vendor",
69
+ ];
70
+ /** Skip files larger than this (bytes) to keep the scan fast. */
71
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
72
+ /** Cap matches recorded per pattern per file to avoid pathological output. */
73
+ const MAX_MATCHES_PER_PATTERN_PER_FILE = 50;
74
+ function compileDeprecations(deprecations) {
75
+ return deprecations.map((deprecation) => {
76
+ const regexes = [];
77
+ for (const pattern of deprecation.detect.patterns) {
78
+ try {
79
+ // Global so we can iterate every match and derive line numbers.
80
+ regexes.push(new RegExp(pattern, "g"));
81
+ }
82
+ catch {
83
+ // A malformed pattern in the dataset must not crash the scan.
84
+ }
85
+ }
86
+ return { deprecation, regexes };
87
+ });
88
+ }
89
+ /** Precompute the byte offset at which each line starts. */
90
+ function computeLineStarts(content) {
91
+ const starts = [0];
92
+ for (let i = 0; i < content.length; i++) {
93
+ if (content.charCodeAt(i) === 10 /* \n */)
94
+ starts.push(i + 1);
95
+ }
96
+ return starts;
97
+ }
98
+ /** Map a character offset to a 1-based line number via binary search. */
99
+ function lineNumberAt(lineStarts, offset) {
100
+ let lo = 0;
101
+ let hi = lineStarts.length - 1;
102
+ let ans = 0;
103
+ while (lo <= hi) {
104
+ const mid = (lo + hi) >> 1;
105
+ if (lineStarts[mid] <= offset) {
106
+ ans = mid;
107
+ lo = mid + 1;
108
+ }
109
+ else {
110
+ hi = mid - 1;
111
+ }
112
+ }
113
+ return ans + 1;
114
+ }
115
+ /** Match every compiled deprecation against one file's contents. */
116
+ function scanContent(content, relPath, compiled, sink) {
117
+ const lineStarts = computeLineStarts(content);
118
+ for (const { deprecation, regexes } of compiled) {
119
+ if (regexes.length === 0)
120
+ continue;
121
+ let recorded = sink.get(deprecation.id);
122
+ for (const baseRe of regexes) {
123
+ // Use a fresh regex instance per file so lastIndex never leaks across files.
124
+ const re = new RegExp(baseRe.source, baseRe.flags);
125
+ let count = 0;
126
+ let m;
127
+ const seenLines = new Set();
128
+ while ((m = re.exec(content)) !== null) {
129
+ // Guard against zero-width matches looping forever.
130
+ if (m.index === re.lastIndex)
131
+ re.lastIndex++;
132
+ const line = lineNumberAt(lineStarts, m.index);
133
+ if (seenLines.has(line))
134
+ continue; // one record per line per pattern
135
+ seenLines.add(line);
136
+ const lineStart = lineStarts[line - 1];
137
+ const nextStart = line < lineStarts.length ? lineStarts[line] : content.length;
138
+ const text = content.slice(lineStart, nextStart).replace(/\r?\n$/, "").trim();
139
+ if (!recorded) {
140
+ recorded = [];
141
+ sink.set(deprecation.id, recorded);
142
+ }
143
+ recorded.push({ file: relPath, line, text });
144
+ if (++count >= MAX_MATCHES_PER_PATTERN_PER_FILE)
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ /** Find dependencies in the collected manifest refs that match each deprecation. */
151
+ function matchManifests(deprecations, refs) {
152
+ const byId = new Map();
153
+ for (const deprecation of deprecations) {
154
+ if (deprecation.detect.sdk.length === 0)
155
+ continue;
156
+ const matches = [];
157
+ const seen = new Set();
158
+ for (const sdk of deprecation.detect.sdk) {
159
+ for (const ref of refs) {
160
+ if (!(0, manifests_1.nameMatches)(sdk, ref.name))
161
+ continue;
162
+ const key = `${ref.source}::${ref.name}`;
163
+ if (seen.has(key))
164
+ continue;
165
+ seen.add(key);
166
+ matches.push({ manifest: ref.source, sdk: ref.name, version: ref.version });
167
+ }
168
+ }
169
+ if (matches.length > 0)
170
+ byId.set(deprecation.id, matches);
171
+ }
172
+ return byId;
173
+ }
174
+ /**
175
+ * Scan a repository for deprecation usage.
176
+ * @param root repo root to scan.
177
+ * @param deprecations validated dataset entries.
178
+ */
179
+ function scanRepo(root, deprecations) {
180
+ const absRoot = path.resolve(root);
181
+ // 1. Manifest scan.
182
+ const { refs, manifests } = (0, manifests_1.collectManifestDeps)(absRoot);
183
+ const manifestMatches = matchManifests(deprecations, refs);
184
+ // 2. Inline scan.
185
+ const compiled = compileDeprecations(deprecations);
186
+ const patternSink = new Map();
187
+ const files = fast_glob_1.default.sync([`**/*.{${SOURCE_EXTENSIONS.join(",")}}`], {
188
+ cwd: absRoot,
189
+ absolute: false,
190
+ onlyFiles: true,
191
+ dot: false,
192
+ followSymbolicLinks: false,
193
+ suppressErrors: true,
194
+ ignore: IGNORED_DIRS.map((d) => `**/${d}/**`),
195
+ });
196
+ let scannedFiles = 0;
197
+ for (const rel of files) {
198
+ const abs = path.join(absRoot, rel);
199
+ let stat;
200
+ try {
201
+ stat = fs.statSync(abs);
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
207
+ continue;
208
+ let content;
209
+ try {
210
+ content = fs.readFileSync(abs, "utf8");
211
+ }
212
+ catch {
213
+ continue;
214
+ }
215
+ scannedFiles++;
216
+ scanContent(content, rel, compiled, patternSink);
217
+ }
218
+ // 3. Combine: detected if a manifest match OR a pattern match exists.
219
+ const findings = [];
220
+ for (const deprecation of deprecations) {
221
+ const mm = manifestMatches.get(deprecation.id) ?? [];
222
+ const pm = patternSink.get(deprecation.id) ?? [];
223
+ if (mm.length === 0 && pm.length === 0)
224
+ continue;
225
+ findings.push({ deprecation, manifestMatches: mm, patternMatches: pm });
226
+ }
227
+ return { scannedFiles, manifestsScanned: manifests, findings };
228
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "arol-ai",
3
+ "version": "0.1.0",
4
+ "description": "Scan a local repo for upcoming third-party API/SDK deprecations. Fully local — no network, no telemetry, your code never leaves the machine.",
5
+ "keywords": [
6
+ "deprecation",
7
+ "sdk",
8
+ "api",
9
+ "scanner",
10
+ "cli",
11
+ "migration",
12
+ "dependencies",
13
+ "audit"
14
+ ],
15
+ "homepage": "https://arol.ai",
16
+ "license": "MIT",
17
+ "type": "commonjs",
18
+ "bin": {
19
+ "arol-ai": "dist/cli.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src/data/deprecations.json",
24
+ "README.md"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "scan": "node dist/cli.js scan",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "commander": "^12.1.0",
36
+ "fast-glob": "^3.3.2"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.0",
40
+ "typescript": "^5.7.2"
41
+ }
42
+ }
@@ -0,0 +1,117 @@
1
+ [
2
+ {
3
+ "id": "openai-assistants-api",
4
+ "vendor": "OpenAI",
5
+ "title": "Assistants API (beta)",
6
+ "severity": "high",
7
+ "sunset_date": "2026-08-26",
8
+ "detect": {
9
+ "sdk": ["openai"],
10
+ "patterns": [
11
+ "beta\\.assistants",
12
+ "beta\\.threads",
13
+ "/v1/assistants",
14
+ "/v1/threads"
15
+ ]
16
+ },
17
+ "migration_url": "https://platform.openai.com/docs/assistants/migration",
18
+ "summary": "The Assistants API beta is being removed on Aug 26, 2026; requests to /v1/assistants and /v1/threads will fail. Migrate to the Responses API + Conversations API.",
19
+ "source": "https://developers.openai.com/api/docs/deprecations"
20
+ },
21
+ {
22
+ "id": "anthropic-claude-4-retirement",
23
+ "vendor": "Anthropic",
24
+ "title": "Claude Sonnet 4 & Opus 4",
25
+ "severity": "high",
26
+ "sunset_date": "2026-06-15",
27
+ "detect": {
28
+ "sdk": ["@anthropic-ai/sdk", "anthropic"],
29
+ "patterns": [
30
+ "claude-sonnet-4-20250514",
31
+ "claude-opus-4-20250514",
32
+ "claude-sonnet-4-0",
33
+ "claude-opus-4-0"
34
+ ]
35
+ },
36
+ "migration_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations",
37
+ "summary": "Claude Sonnet 4 and Opus 4 retire June 15, 2026; calls to these model IDs will fail. Migrate to Sonnet 4.6 / Opus 4.6.",
38
+ "source": "https://platform.claude.com/docs/en/release-notes/overview"
39
+ },
40
+ {
41
+ "id": "anthropic-retired-legacy-claude",
42
+ "vendor": "Anthropic",
43
+ "title": "Retired Claude models (Haiku 3, 3.5/3.7 Sonnet, Claude 2.x)",
44
+ "severity": "high",
45
+ "sunset_date": "2026-04-20",
46
+ "detect": {
47
+ "sdk": ["@anthropic-ai/sdk", "anthropic"],
48
+ "patterns": [
49
+ "claude-3-haiku-20240307",
50
+ "claude-3-5-sonnet",
51
+ "claude-3-7-sonnet",
52
+ "claude-2\\.1",
53
+ "claude-2\\.0",
54
+ "claude-instant"
55
+ ]
56
+ },
57
+ "migration_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations",
58
+ "summary": "These Claude model IDs are already retired and return errors. Migrate to current models (Haiku 4.5 / Sonnet 4.6 / Opus 4.6).",
59
+ "source": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
60
+ },
61
+ {
62
+ "id": "google-gemini-2-5-retirement",
63
+ "vendor": "Google (Gemini)",
64
+ "title": "Gemini 2.5 Pro / Flash / Flash-Lite",
65
+ "severity": "high",
66
+ "sunset_date": "2026-10-16",
67
+ "detect": {
68
+ "sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
69
+ "patterns": [
70
+ "gemini-2\\.5-pro",
71
+ "gemini-2\\.5-flash",
72
+ "gemini-2\\.5-flash-lite"
73
+ ]
74
+ },
75
+ "migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
76
+ "summary": "Gemini 2.5 models scheduled for shutdown Oct 16, 2026 (Google states this is the earliest possible date). Migrate to Gemini 3.x.",
77
+ "source": "https://ai.google.dev/gemini-api/docs/deprecations"
78
+ },
79
+ {
80
+ "id": "google-gemini-2-0-shutdown",
81
+ "vendor": "Google (Gemini)",
82
+ "title": "Gemini 2.0 Flash family",
83
+ "severity": "high",
84
+ "sunset_date": "2026-06-01",
85
+ "detect": {
86
+ "sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
87
+ "patterns": [
88
+ "gemini-2\\.0-flash",
89
+ "gemini-2\\.0-flash-001",
90
+ "gemini-2\\.0-flash-lite",
91
+ "gemini-2\\.0-flash-lite-001"
92
+ ]
93
+ },
94
+ "migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
95
+ "summary": "Gemini 2.0 Flash and Flash-Lite shut down June 1, 2026 — already past, these calls now fail. Migrate to gemini-2.5-flash-lite or Gemini 3.x.",
96
+ "source": "https://firebase.google.com/docs/ai-logic/models"
97
+ },
98
+ {
99
+ "id": "google-gemini-1x-shutdown",
100
+ "vendor": "Google (Gemini)",
101
+ "title": "Gemini 1.0 / 1.5 models",
102
+ "severity": "high",
103
+ "sunset_date": "2026-04-29",
104
+ "detect": {
105
+ "sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
106
+ "patterns": [
107
+ "gemini-1\\.5-pro",
108
+ "gemini-1\\.5-flash",
109
+ "gemini-1\\.0-pro",
110
+ "gemini-pro"
111
+ ]
112
+ },
113
+ "migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
114
+ "summary": "All Gemini 1.0 and 1.5 models are shut down and return 404. Migrate to Gemini 2.5+ or 3.x.",
115
+ "source": "https://firebase.google.com/docs/ai-logic/models"
116
+ }
117
+ ]