readystack-aks-eks-support-cliff-lint 1.0.2__tar.gz

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.
Files changed (19) hide show
  1. readystack_aks_eks_support_cliff_lint-1.0.2/LICENSE.txt +15 -0
  2. readystack_aks_eks_support_cliff_lint-1.0.2/PKG-INFO +86 -0
  3. readystack_aks_eks_support_cliff_lint-1.0.2/README.md +66 -0
  4. readystack_aks_eks_support_cliff_lint-1.0.2/pyproject.toml +29 -0
  5. readystack_aks_eks_support_cliff_lint-1.0.2/setup.cfg +4 -0
  6. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/__init__.py +1 -0
  7. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/__main__.py +12 -0
  8. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/js/cli.js +161 -0
  9. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/js/engine.js +120 -0
  10. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/js/license.js +103 -0
  11. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/js/package.json +42 -0
  12. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/js/rules.json +122 -0
  13. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint/js/strings.json +10 -0
  14. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint.egg-info/PKG-INFO +86 -0
  15. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint.egg-info/SOURCES.txt +17 -0
  16. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint.egg-info/dependency_links.txt +1 -0
  17. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint.egg-info/entry_points.txt +2 -0
  18. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint.egg-info/requires.txt +1 -0
  19. readystack_aks_eks_support_cliff_lint-1.0.2/src/readystack_aks_eks_support_cliff_lint.egg-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
1
+ AKS LTS & EKS Extended Support Lint for Terraform — Licence
2
+
3
+ Free scope
4
+ Lint the open .tf file: every EKS and AKS cluster block checked against the dated support calendars, with the extra dollars already billed. is free for any use, personal or commercial, for as long as you keep the
5
+ extension installed, and the online check at the product page is free without registration.
6
+
7
+ Paid scope
8
+ Scan the whole workspace and export a per-cluster CSV of fees, cliff dates and fixes for your platform team and FinOps review. is the paid part. It asks for a licence key issued at purchase (one seat, one key). The key is checked against the payment provider and
9
+ cached locally for 30 days so it keeps working offline.
10
+
11
+ Redistribution
12
+ You may not resell, re-host or bundle this extension or its rule set. The rule set and the
13
+ engine are provided as-is; check results are advisory and do not constitute legal advice.
14
+
15
+ (c) ReadyStack — https://getreadystack.com
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: readystack-aks-eks-support-cliff-lint
3
+ Version: 1.0.2
4
+ Summary: Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules (bundled Node.js runtime)
5
+ Author-email: ReadyStack <support@getreadystack.com>
6
+ License: Proprietary - see LICENSE.txt
7
+ Project-URL: Homepage, https://getreadystack.com/tools/aks-eks-support-cliff-lint?ref=pypi
8
+ Project-URL: Documentation, https://getreadystack.com/tools/aks-eks-support-cliff-lint?ref=pypi
9
+ Project-URL: Support, https://getreadystack.com/support
10
+ Keywords: aks,eks,terraform,kubernetes version,aks long term support,eks extended support,azure kubernetes service,premium tier
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Environment :: Console
13
+ Classifier: Topic :: Software Development :: Quality Assurance
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: nodejs-wheel-binaries>=20
19
+ Dynamic: license-file
20
+
21
+ # AKS LTS & EKS Extended Support Lint for Terraform
22
+
23
+ > Runs the same checker as the npm package `@readystack/aks-eks-support-cliff-lint` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
24
+
25
+ ![AKS LTS & EKS Extended Support Lint for Terraform — finds the line](https://getreadystack.com/img/promo/aks-eks-support-cliff-lint_demo.gif)
26
+
27
+ ![AKS LTS & EKS Extended Support Lint for Terraform](https://getreadystack.com/img/promo/sku326983_result_card.jpg)
28
+
29
+ Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules
30
+
31
+ ## Install
32
+
33
+ ```
34
+ aks-eks-support-cliff-lint file
35
+ ```
36
+
37
+ Node 18+. The same 9 rules as the VS Code extension, from a terminal or CI.
38
+
39
+ ## Free
40
+
41
+ - Lint the open .tf file with all 9 rules: every EKS and AKS cluster block checked against the dated calendars, with the dollars already billed.
42
+ - `--rules` lists every rule
43
+
44
+ ## With a licence ($29 once)
45
+
46
+ - Scan the whole workspace and export a per-cluster CSV of fees, cliff dates and fixes for a platform or FinOps review.
47
+
48
+ ```
49
+ @readystack/aks-eks-support-cliff-lint --dir ./templates --report html --out report.html
50
+ ```
51
+
52
+ Amazon EKS bills $0.60 per cluster-hour in extended support against $0.10 in standard support, so one cluster left on an old version costs $365 a month or $4,380 a year extra; AKS Premium tier for LTS is $0.60 against $0.10 for Standard.
53
+
54
+ ## Use from an AI agent (MCP)
55
+
56
+ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
57
+
58
+ ```json
59
+ { "mcpServers": { "aks-eks-support-cliff-lint": { "command": "npx", "args": ["-y", "@readystack/aks-eks-support-cliff-lint", "--mcp"] } } }
60
+ ```
61
+
62
+ Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
63
+
64
+ ## Use in CI
65
+
66
+ ```yaml
67
+ - name: AKS LTS & EKS Extended Support Lint for Terraform
68
+ run: npx -y @readystack/aks-eks-support-cliff-lint --dir . --ci
69
+ ```
70
+
71
+ (container: `docker run --rm -v "$PWD:/work" getreadystack/aks-eks-support-cliff-lint --dir /work --ci`)
72
+
73
+ The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
74
+
75
+ [Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_h4hStvanMbFQkOvKD4EQ6BWS54rfLlOpDYCBB0UJnAI)
76
+
77
+
78
+ <!-- aks lts premium tier -->
79
+
80
+
81
+ ## Install (PyPI)
82
+
83
+ ```
84
+ pip install readystack-aks-eks-support-cliff-lint
85
+ aks-eks-support-cliff-lint --help
86
+ ```
@@ -0,0 +1,66 @@
1
+ # AKS LTS & EKS Extended Support Lint for Terraform
2
+
3
+ > Runs the same checker as the npm package `@readystack/aks-eks-support-cliff-lint` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
4
+
5
+ ![AKS LTS & EKS Extended Support Lint for Terraform — finds the line](https://getreadystack.com/img/promo/aks-eks-support-cliff-lint_demo.gif)
6
+
7
+ ![AKS LTS & EKS Extended Support Lint for Terraform](https://getreadystack.com/img/promo/sku326983_result_card.jpg)
8
+
9
+ Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules
10
+
11
+ ## Install
12
+
13
+ ```
14
+ aks-eks-support-cliff-lint file
15
+ ```
16
+
17
+ Node 18+. The same 9 rules as the VS Code extension, from a terminal or CI.
18
+
19
+ ## Free
20
+
21
+ - Lint the open .tf file with all 9 rules: every EKS and AKS cluster block checked against the dated calendars, with the dollars already billed.
22
+ - `--rules` lists every rule
23
+
24
+ ## With a licence ($29 once)
25
+
26
+ - Scan the whole workspace and export a per-cluster CSV of fees, cliff dates and fixes for a platform or FinOps review.
27
+
28
+ ```
29
+ @readystack/aks-eks-support-cliff-lint --dir ./templates --report html --out report.html
30
+ ```
31
+
32
+ Amazon EKS bills $0.60 per cluster-hour in extended support against $0.10 in standard support, so one cluster left on an old version costs $365 a month or $4,380 a year extra; AKS Premium tier for LTS is $0.60 against $0.10 for Standard.
33
+
34
+ ## Use from an AI agent (MCP)
35
+
36
+ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
37
+
38
+ ```json
39
+ { "mcpServers": { "aks-eks-support-cliff-lint": { "command": "npx", "args": ["-y", "@readystack/aks-eks-support-cliff-lint", "--mcp"] } } }
40
+ ```
41
+
42
+ Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
43
+
44
+ ## Use in CI
45
+
46
+ ```yaml
47
+ - name: AKS LTS & EKS Extended Support Lint for Terraform
48
+ run: npx -y @readystack/aks-eks-support-cliff-lint --dir . --ci
49
+ ```
50
+
51
+ (container: `docker run --rm -v "$PWD:/work" getreadystack/aks-eks-support-cliff-lint --dir /work --ci`)
52
+
53
+ The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
54
+
55
+ [Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_h4hStvanMbFQkOvKD4EQ6BWS54rfLlOpDYCBB0UJnAI)
56
+
57
+
58
+ <!-- aks lts premium tier -->
59
+
60
+
61
+ ## Install (PyPI)
62
+
63
+ ```
64
+ pip install readystack-aks-eks-support-cliff-lint
65
+ aks-eks-support-cliff-lint --help
66
+ ```
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "readystack-aks-eks-support-cliff-lint"
7
+ version = "1.0.2"
8
+ description = "Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 \u2014 $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules (bundled Node.js runtime)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = ["nodejs-wheel-binaries>=20"]
12
+ license = {text = "Proprietary - see LICENSE.txt"}
13
+ keywords = ["aks", "eks", "terraform", "kubernetes version", "aks long term support", "eks extended support", "azure kubernetes service", "premium tier"]
14
+ authors = [{name = "ReadyStack", email = "support@getreadystack.com"}]
15
+ classifiers = ["Programming Language :: Python :: 3", "Environment :: Console", "Topic :: Software Development :: Quality Assurance", "Operating System :: OS Independent"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://getreadystack.com/tools/aks-eks-support-cliff-lint?ref=pypi"
19
+ Documentation = "https://getreadystack.com/tools/aks-eks-support-cliff-lint?ref=pypi"
20
+ Support = "https://getreadystack.com/support"
21
+
22
+ [project.scripts]
23
+ "aks-eks-support-cliff-lint" = "readystack_aks_eks_support_cliff_lint.__main__:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ "readystack_aks_eks_support_cliff_lint" = ["js/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ import os, sys
2
+
3
+
4
+ def main():
5
+ # Runs the same checker as the npm package on the Node.js runtime installed with this package (nodejs-wheel-binaries).
6
+ from nodejs_wheel import node
7
+ here = os.path.dirname(os.path.abspath(__file__))
8
+ return node([os.path.join(here, "js", "cli.js")] + sys.argv[1:])
9
+
10
+
11
+ if __name__ == "__main__":
12
+ sys.exit(main())
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ // cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
3
+ 'use strict';
4
+ const fs = require('fs'), path = require('path');
5
+ const RULES = require('./rules.json');
6
+ const S = require('./strings.json');
7
+ const lic = require('./license.js');
8
+ const ENGINE = require('./engine.js'); // s144 — scaffold products: the same engine the VS Code extension runs
9
+ const RULE_LIST = Array.isArray(ENGINE.RULES) ? ENGINE.RULES : (Array.isArray(RULES) ? RULES : []);
10
+ const RULE_N = ENGINE.RULE_COUNT || RULE_LIST.length;
11
+ function scan(text, file) {
12
+ const r = ENGINE.engine.check(String(text), { today: new Date().toISOString().slice(0, 10), path: file || '' });
13
+ return (r && r.findings || []).map((f) => ({ line: parseInt(f.line, 10) || 1, msg: String(f.msg || f.message || f.check || ''), fix: f.fix || null, sev: (String(f.sev || 'error').toLowerCase().startsWith('err') ? 'error' : 'warn') }));
14
+ }
15
+ function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
16
+ function walk(dir, exts, out) {
17
+ let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
18
+ for (const e of ents) {
19
+ if (['node_modules', '.git', '.hg', '.svn', '.venv', 'venv', '.tox', '.cache', '.next', '.nuxt', '.terraform', '__pycache__'].includes(e.name)) continue; // s158 — .github · .gitlab-ci.yml · .env · .well-known 은 본다 (옛 줄은 점으로 시작하는 것을 전부 건너뛰어 워크플로 검사기가 .github/workflows 를 못 봤다)
20
+ const p = path.join(dir, e.name);
21
+ if (e.isDirectory()) walk(p, exts, out);
22
+ else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
23
+ }
24
+ return out;
25
+ }
26
+ function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
27
+ function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
28
+ function render(rows, fmt) {
29
+ if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULE_N, files: rows }, null, 1);
30
+ if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
31
+ if (fmt === 'html') {
32
+ const n = rows.reduce((a, r) => a + r.hits.length, 0);
33
+ let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
34
+ o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULE_N + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
35
+ for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
36
+ return o + '</table>';
37
+ }
38
+ let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
39
+ return o;
40
+ }
41
+ function trialState() {
42
+ // s158 — ⚑7일 무료 없음: 새 체험은 ⛔열지 않는다 · 이미 시작된 체험(trial.json)만 끝까지 지킨다 (읽기 전용)
43
+ if (process.env.READYSTACK_NO_TRIAL) return { active: false };
44
+ try {
45
+ const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
46
+ const t = JSON.parse(fs.readFileSync(p, 'utf8'));
47
+ return { active: !!(t && t.until) && new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t && t.until };
48
+ } catch (e) { return { active: false }; }
49
+ }
50
+ // ★s158 2026-09-23 — the free run SPEAKS about the rest of the folder (vsix: auto.js · crx: welcome tab).
51
+ // The answer for the named files is complete and free; the next step is shown with the user's OWN numbers:
52
+ // "this folder has N more matching files - M issues in K of them" + the one command that sweeps them.
53
+ // Never blocks, never starts a trial (s158: no free trial - the free answer on the named files is the "try it"), human seat only, 400 files / 1.5 s cap.
54
+ function folderHint(done) {
55
+ if (!process.stderr.isTTY || process.env.CI || process.env.READYSTACK_NO_HINT || !done.length) return;
56
+ const base = path.dirname(path.resolve(done[0]));
57
+ const seen = new Set(done.map((f) => path.resolve(f)));
58
+ const all = walk(base, S.exts || [], []).filter((f) => !seen.has(path.resolve(f)));
59
+ if (!all.length) return;
60
+ const t0 = Date.now(); let issues = 0, hitFiles = 0, scanned = 0;
61
+ for (const f of all.slice(0, 400)) { if (Date.now() - t0 > 1500) break; let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } scanned++; const h = scan(t, f); if (h.length) { issues += h.length; hitFiles++; } }
62
+ const rel = path.relative(process.cwd(), base) || '.';
63
+ const tail = trialState().active ? 'free during your trial' : '$' + S.price + ' once';
64
+ const more = all.length + ' more matching file' + (all.length > 1 ? 's' : '');
65
+ const found = issues ? ' - ' + (scanned < all.length ? 'at least ' : '') + issues + ' issue' + (issues > 1 ? 's' : '') + ' in ' + hitFiles + ' of them' : ' - no issues found in them';
66
+ process.stderr.write('\n' + (rel === '.' ? 'This folder' : rel) + ' has ' + more + found + '.\n' + (issues ? 'Sweep them all: ' + S.bin + ' --dir ' + (/\s/.test(rel) ? JSON.stringify(rel) : rel) + ' (' + tail + ')\n' : ''));
67
+ }
68
+ let _usePinged = false;
69
+ function pingUse() {
70
+ try {
71
+ if (_usePinged) return; _usePinged = true;
72
+ if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return;
73
+ if (!(process.stdout.isTTY || process.stdin.isTTY)) return; // human seat only (s152)
74
+ const https = require('https');
75
+ const body = JSON.stringify({ t: 'use', slug: S.bin, src: 'cli', why: 'free' });
76
+ const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
77
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + S.bin } }, function (res) { res.resume(); });
78
+ req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
79
+ req.write(body); req.end();
80
+ } catch (e) {}
81
+ }
82
+ function mcpServe() {
83
+ process.env.READYSTACK_MCP = '1'; // s152 - 에이전트(Claude Code·Cursor)가 부른 세션은 사람 자리다 · 키 판 핑 src=mcp
84
+ // s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (s158: no free trial): check_dir.
85
+ const rl = require('readline').createInterface({ input: process.stdin });
86
+ const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
87
+ const tools = [
88
+ { name: 'check_text', description: S.name + ' - run all ' + RULE_N + ' checks on a text (free)', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'file contents' }, path: { type: 'string', description: 'optional file name for context' } }, required: ['text'] } },
89
+ { name: 'check_file', description: S.name + ' - run all checks on one file by path (free)', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
90
+ { name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; $' + S.price + ' once)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
91
+ ];
92
+ const result = (id, rows) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: render(rows, 'text') }], structuredContent: { tool: S.name, rules: RULE_N, files: rows } } });
93
+ const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
94
+ rl.on('line', async (line) => {
95
+ let m; try { m = JSON.parse(line); } catch (e) { return; }
96
+ const id = m.id, method = m.method;
97
+ if (method === 'initialize') return send({ jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: '@readystack/' + S.bin, version: '1.0.0' } } });
98
+ if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
99
+ if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
100
+ if (method === 'tools/call') {
101
+ const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
102
+ try {
103
+ if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
104
+ if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
105
+ if (name === 'check_dir') {
106
+ const r = await lic.ensure();
107
+ if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once): ' + lic.BUY_URL);
108
+ const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
109
+ return result(id, files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t, f) }; }));
110
+ }
111
+ return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
112
+ } catch (e) { return fail(id, String(e && e.message || e)); }
113
+ }
114
+ if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
115
+ });
116
+ }
117
+ function help() {
118
+ return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
119
+ ' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
120
+ ' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
121
+ ' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
122
+ ' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
123
+ ' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
124
+ ' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
125
+ 'Free: ' + S.free, 'Licence ($' + S.price + ', once): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
126
+ }
127
+ (async function main() {
128
+ try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { if (Array.isArray(ENGINE.RULES)) for (const r of _feed.rules) ENGINE.RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
129
+ const a = process.argv.slice(2);
130
+ const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
131
+ if (a.includes('--mcp')) { mcpServe(); return; }
132
+ if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
133
+ if (a.includes('--rules')) { process.stdout.write((RULE_LIST.length ? RULE_LIST.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + (r.message || r.msg || r.id || '')) : Array.from({ length: RULE_N }, (_, i) => String(i + 1).padStart(3) + ' [engine] check ' + (i + 1))).join('\n') + '\n'); return; }
134
+ if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
135
+ const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
136
+ const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
137
+ const paid = !!(dir || fmt || ci);
138
+ if (paid) {
139
+ const r = await lic.ensure();
140
+ if (!r.ok) {
141
+ const t = trialState(); // s158 — ⚑7일 무료 없음 · 이미 시작된 체험만 지킨다
142
+ if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once. Get a licence: ' + lic.BUY_URL + '\n');
143
+ else {
144
+ // s158 — the key is asked WITH the customer's own count (endowment · open loop): how many issues this folder holds.
145
+ let own = '';
146
+ if (dir) { try { const all = walk(dir, exts, []); let n = 0, k = 0, sc = 0; const t0 = Date.now();
147
+ for (const f of all.slice(0, 2000)) { if (Date.now() - t0 > 4000) break; let tx = ''; try { tx = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } sc++; const h = scan(tx, f); if (h.length) { n += h.length; k++; } }
148
+ if (n) own = dir + ': ' + (sc < all.length ? 'at least ' : '') + n + ' issue' + (n > 1 ? 's' : '') + ' in ' + k + ' of ' + all.length + ' files.\n'; } catch (e) {} }
149
+ process.stderr.write(own + S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ' once): ' + lic.BUY_URL + '\n'); process.exit(2);
150
+ }
151
+ }
152
+ }
153
+ const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
154
+ if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
155
+ const rows = files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t) }; });
156
+ const text = render(rows, fmt || 'text');
157
+ if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
158
+ if (!paid) { try { folderHint(files); } catch (e) {} pingUse(); } // s158 — a free run ends with the user's own folder count + one anonymous 'used' count (human seat only)
159
+ const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
160
+ if (ci && errors) process.exit(1);
161
+ })().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
@@ -0,0 +1,120 @@
1
+ /* aks-eks-support-cliff-lint engine: Terraform aws_eks_cluster / azurerm_kubernetes_cluster vs the EKS and AKS version calendars. Runs in Node and the browser. */
2
+ (function () {
3
+ const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.SCL_RULES;
4
+ const R = {}; RULES.forEach(r => { R[r.id] = r; });
5
+ const EKS = R['eks-extended-billing'], AKS = R['aks-lts-needs-premium'];
6
+ const DAY = 86400000;
7
+
8
+ function d(s) { return new Date(s + 'T00:00:00Z'); }
9
+ function days(a, b) { return Math.round((d(b) - d(a)) / DAY); }
10
+ function usd(n) { return '$' + Math.round(n).toLocaleString('en-US'); }
11
+ function minor(v) { const m = /^(\d+)\.(\d+)/.exec(v || ''); return m ? m[1] + '.' + m[2] : null; }
12
+ function older(a, b) { const x = a.split('.').map(Number), y = b.split('.').map(Number); return x[0] < y[0] || (x[0] === y[0] && x[1] < y[1]); }
13
+ function oldestKey(cal) { return Object.keys(cal).sort((a, b) => older(a, b) ? -1 : 1)[0]; }
14
+
15
+ // Find resource blocks by brace matching; return {type, name, line, body, bodyLine}
16
+ function blocks(text) {
17
+ const out = [], re = /resource\s+"(aws_eks_cluster|azurerm_kubernetes_cluster)"\s+"([^"]+)"\s*\{/g;
18
+ let m;
19
+ while ((m = re.exec(text))) {
20
+ let depth = 1, i = re.lastIndex;
21
+ while (i < text.length && depth > 0) { const c = text[i]; if (c === '{') depth++; else if (c === '}') depth--; i++; }
22
+ const start = re.lastIndex;
23
+ out.push({ type: m[1], name: m[2], line: text.slice(0, m.index).split('\n').length, body: text.slice(start, i - 1), bodyLine: text.slice(0, start).split('\n').length });
24
+ }
25
+ return out;
26
+ }
27
+ function attr(b, key) {
28
+ const m = new RegExp('^[ \\t]*' + key + '[ \\t]*=[ \\t]*"([^"]*)"', 'm').exec(b.body);
29
+ if (!m) return null;
30
+ return { value: m[1], line: b.bodyLine + b.body.slice(0, m.index).split('\n').length - 1 };
31
+ }
32
+
33
+ function eks(b, today, F) {
34
+ const v = attr(b, 'version'); if (!v) return;
35
+ const k = minor(v.value); if (!k) return;
36
+ const st = attr(b, 'support_type');
37
+ const policy = st ? st.value.toUpperCase() : 'EXTENDED';
38
+ const how = st ? 'support_type = "' + policy + '"' : 'no upgrade_policy block, so AWS defaults to EXTENDED';
39
+ const cal = EKS.eks_calendar;
40
+ let row = cal[k];
41
+ if (!row) {
42
+ if (older(k, oldestKey(cal))) F.push({ check: 'eks-past-extended', sev: R['eks-past-extended'].sev, line: v.line,
43
+ msg: 'aws_eks_cluster.' + b.name + ' pins Kubernetes ' + k + ', older than every version EKS still supports (oldest: ' + oldestKey(cal) + '). The control plane is auto-upgraded; managed node groups stay behind.' });
44
+ return;
45
+ }
46
+ const diff = EKS.price_extended_hour - EKS.price_standard_hour;
47
+ if (today >= row.ext_end) {
48
+ F.push({ check: 'eks-past-extended', sev: R['eks-past-extended'].sev, line: v.line,
49
+ msg: 'aws_eks_cluster.' + b.name + ' pins ' + k + '; extended support ended ' + row.ext_end + '. The control plane is auto-upgraded; node groups stay on ' + k + '.' });
50
+ return;
51
+ }
52
+ if (today >= row.std_end && policy !== 'STANDARD') {
53
+ const billed = days(row.std_end, today) * 24 * diff;
54
+ F.push({ check: 'eks-extended-billing', sev: EKS.sev, line: v.line, extra_usd: Math.round(billed),
55
+ msg: 'aws_eks_cluster.' + b.name + ' pins ' + k + ' (' + how + '). Standard support ended ' + row.std_end + '; billed $0.60 instead of $0.10 per cluster-hour since then: ' + usd(billed) + ' extra by ' + today + ', ' + usd(diff * EKS.hours_per_month) + ' more each month.' });
56
+ const left = days(today, row.ext_end);
57
+ if (left <= R['eks-forced-upgrade-soon'].window_days)
58
+ F.push({ check: 'eks-forced-upgrade-soon', sev: R['eks-forced-upgrade-soon'].sev, line: v.line,
59
+ msg: 'aws_eks_cluster.' + b.name + ': extended support for ' + k + ' ends ' + row.ext_end + ' (' + left + ' days). After that AWS upgrades the control plane at any time with no notice.' });
60
+ return;
61
+ }
62
+ if (today >= row.std_end && policy === 'STANDARD') {
63
+ F.push({ check: 'eks-past-extended', sev: R['eks-past-extended'].sev, line: v.line,
64
+ msg: 'aws_eks_cluster.' + b.name + ' pins ' + k + ' with support_type = "STANDARD"; standard support ended ' + row.std_end + ', so AWS auto-upgrades this control plane and the pinned version drifts from state.' });
65
+ return;
66
+ }
67
+ const left = days(today, row.std_end);
68
+ if (left <= R['eks-cliff-soon'].window_days)
69
+ F.push({ check: 'eks-cliff-soon', sev: R['eks-cliff-soon'].sev, line: v.line,
70
+ msg: 'aws_eks_cluster.' + b.name + ': standard support for ' + k + ' ends ' + row.std_end + ' (' + left + ' days). ' +
71
+ (policy === 'STANDARD' ? 'support_type = "STANDARD": AWS auto-upgrades the control plane on that date.' : how + ': billing jumps from $0.10 to $0.60 per cluster-hour (' + usd(diff * EKS.hours_per_month) + ' a month).') });
72
+ }
73
+
74
+ function aks(b, today, F) {
75
+ const v = attr(b, 'kubernetes_version'); if (!v) return;
76
+ const k = minor(v.value); if (!k) return;
77
+ const tierA = attr(b, 'sku_tier'), planA = attr(b, 'support_plan');
78
+ const tier = tierA ? tierA.value : 'Free', lts = planA && planA.value === 'AKSLongTermSupport';
79
+ const cal = AKS.aks_calendar, row = cal[k];
80
+ const diff = AKS.price_premium_hour - AKS.price_standard_hour;
81
+ if (lts && tier !== 'Premium') {
82
+ F.push({ check: 'aks-lts-needs-premium', sev: AKS.sev, line: planA.line,
83
+ msg: 'azurerm_kubernetes_cluster.' + b.name + ': support_plan = "AKSLongTermSupport" with sku_tier = "' + tier + '". LTS is only available on the Premium tier ($0.60 vs $0.10 per cluster-hour).' });
84
+ return;
85
+ }
86
+ if (!row) {
87
+ if (older(k, oldestKey(cal))) F.push({ check: 'aks-lts-ended', sev: R['aks-lts-ended'].sev, line: v.line,
88
+ msg: 'azurerm_kubernetes_cluster.' + b.name + ' pins ' + k + ', older than every AKS version still supported, LTS included (oldest: ' + oldestKey(cal) + ').' });
89
+ return;
90
+ }
91
+ if (lts) {
92
+ if (today > row.lts_eol) F.push({ check: 'aks-lts-ended', sev: R['aks-lts-ended'].sev, line: v.line,
93
+ msg: 'azurerm_kubernetes_cluster.' + b.name + ' pins ' + k + '; its LTS end of life was ' + row.lts_eol + '. Premium tier no longer covers this version.' });
94
+ else if (today <= row.eol) F.push({ check: 'aks-premium-unused', sev: R['aks-premium-unused'].sev, line: tierA.line,
95
+ msg: 'azurerm_kubernetes_cluster.' + b.name + ': Premium tier for LTS, but ' + k + ' is in community support until ' + row.eol + '. Standard tier until then saves ' + usd(diff * AKS.hours_per_month) + ' a month.' });
96
+ return;
97
+ }
98
+ if (today > row.eol) {
99
+ F.push({ check: 'aks-out-of-support', sev: R['aks-out-of-support'].sev, line: v.line,
100
+ msg: 'azurerm_kubernetes_cluster.' + b.name + ' pins ' + k + '; community end of life was ' + row.eol + ' and LTS is off. No security patches, no new clusters at this version. Upgrade, or LTS (Premium) runs to ' + row.lts_eol + '.' });
101
+ return;
102
+ }
103
+ const left = days(today, row.eol);
104
+ if (left <= R['aks-eol-soon'].window_days)
105
+ F.push({ check: 'aks-eol-soon', sev: R['aks-eol-soon'].sev, line: v.line,
106
+ msg: 'azurerm_kubernetes_cluster.' + b.name + ': ' + k + ' leaves community support ' + row.eol + ' (' + left + ' days) and LTS is off.' });
107
+ }
108
+
109
+ function check(text, opts) {
110
+ const today = (opts && opts.today) || new Date().toISOString().slice(0, 10);
111
+ const findings = [];
112
+ blocks(String(text || '')).forEach(b => (b.type === 'aws_eks_cluster' ? eks : aks)(b, today, findings));
113
+ findings.sort((a, b) => a.line - b.line);
114
+ return { findings };
115
+ }
116
+
117
+ const api = { engine: { check }, RULES, RULE_COUNT: RULES.length };
118
+ if (typeof module !== 'undefined') module.exports = api;
119
+ if (typeof window !== 'undefined') window.SCLENGINE = api;
120
+ })();
@@ -0,0 +1,103 @@
1
+ // license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
2
+ 'use strict';
3
+ const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
4
+ const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
5
+ const BENEFIT_ID = '41644950-8524-409c-9150-2ff86f649cda'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
6
+ const BUY_URL = 'https://getreadystack.com/api/buy/cl/polar_cl_h4hStvanMbFQkOvKD4EQ6BWS54rfLlOpDYCBB0UJnAI';
7
+ const SLUG = 'aks-eks-support-cliff-lint';
8
+ const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
9
+ const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
10
+ function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
11
+ function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
12
+ function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
13
+ const ALL_BENEFIT_ID = '22692551-5203-4467-b1a3-e33cdba6589d'; // s149 2026-09-17 — 팀 키(전 린터 한 키 · Polar benefit) · 상품 benefit 다음에 한 번 더 묻는다
14
+ // s170 2026-09-29 — ask OUR worker first (/api/lic): keys bought on Whop open here (a price-tier key binds to the first tool it opens · the team key opens every tool). Then the old Polar check.
15
+ function validate(key) {
16
+ return validateHub(key).then(function (h) {
17
+ if (h.ok) return h;
18
+ return validate1(key, BENEFIT_ID).then(function (r) { return (r.ok || r.offline || !/^[0-9a-f-]{36}$/.test(ALL_BENEFIT_ID)) ? r : validate1(key, ALL_BENEFIT_ID); })
19
+ .then(function (r) { return r.ok ? r : { ok: false, offline: !!(h.offline || r.offline) }; });
20
+ });
21
+ }
22
+ function validateHub(key) {
23
+ return new Promise(function (resolve) {
24
+ const req = https.request({ hostname: 'getreadystack.com', path: '/api/lic?key=' + encodeURIComponent(key) + '&slug=' + encodeURIComponent(SLUG),
25
+ method: 'GET', timeout: 8000, headers: { 'accept': 'application/json', 'user-agent': 'readystack-cli/' + SLUG } }, function (res) {
26
+ let buf = ''; res.on('data', function (d) { buf += d; });
27
+ res.on('end', function () {
28
+ if (res.statusCode !== 200) return resolve({ ok: false, offline: res.statusCode >= 500 });
29
+ try { const j = JSON.parse(buf); resolve({ ok: !!(j && j.ok), offline: false }); } catch (e) { resolve({ ok: false, offline: false }); }
30
+ });
31
+ });
32
+ req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
33
+ req.on('error', function () { resolve({ ok: false, offline: true }); });
34
+ req.end();
35
+ });
36
+ }
37
+ function validate1(key, ben) {
38
+ return new Promise(function (resolve) {
39
+ if (!ORG_ID) return resolve({ ok: false, offline: false });
40
+ const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(ben) ? { key: key, organization_id: ORG_ID, benefit_id: ben } : { key: key, organization_id: ORG_ID });
41
+ const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
42
+ headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
43
+ let buf = ''; res.on('data', function (d) { buf += d; });
44
+ res.on('end', function () {
45
+ if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
46
+ try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
47
+ catch (e) { resolve({ ok: false, offline: false }); }
48
+ });
49
+ });
50
+ req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
51
+ req.on('error', function () { resolve({ ok: false, offline: true }); });
52
+ req.write(body); req.end();
53
+ });
54
+ }
55
+ // ★s151 2026-09-19 — 키 판(키가 없거나 거절된 순간)을 익명으로 센다 (슬러그·출처·이유만) · DO_NOT_TRACK=1 · READYSTACK_NO_TELEMETRY 면 안 보낸다 · 실패는 조용히 · 프로세스당 한 번.
56
+ let _pinged = false;
57
+ function pingPaywall(why) {
58
+ try {
59
+ if (_pinged) return; _pinged = true;
60
+ if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return; // s151: CI(깃허브 액션 등)와 우리 빌드는 손님이 아니다
61
+ if (!(process.stdout.isTTY || process.stdin.isTTY || process.env.READYSTACK_MCP === '1')) return; // s152: 사람 자리(터미널·MCP 세션)에서만 센다 - 발행 1분 뒤 남의 실행기(JP · 우리 3대는 US)가 돌린 no_key 13건은 손님이 아니다
62
+ const body = JSON.stringify({ t: 'paywall', slug: SLUG, src: process.env.READYSTACK_MCP === '1' ? 'mcp' : 'cli', why: why || 'no_key' });
63
+ const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
64
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
65
+ req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
66
+ req.write(body); req.end();
67
+ } catch (e) { /* 세는 것이 실패해도 상품은 돈다 */ }
68
+ }
69
+ async function ensure(explicitKey) {
70
+ const st = load();
71
+ const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
72
+ if (!key) { pingPaywall('no_key'); return { ok: false, why: 'no_key' }; } // s151
73
+ const age = Date.now() - (st.okAt || 0);
74
+ if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
75
+ const r = await validate(String(key).trim());
76
+ if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
77
+ if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
78
+ if (!r.offline) pingPaywall('invalid'); // s151
79
+ return { ok: false, why: r.offline ? 'offline' : 'invalid' };
80
+ }
81
+ // ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
82
+ const FEED_URL = 'https://getreadystack.com/api/rules/';
83
+ function pullFeed() {
84
+ const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
85
+ if (!key) return Promise.resolve(cached);
86
+ if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
87
+ return new Promise(function (resolve) {
88
+ let req;
89
+ try {
90
+ req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
91
+ let buf = ''; res.on('data', function (d) { buf += d; });
92
+ res.on('end', function () {
93
+ if (res.statusCode !== 200) return resolve(cached);
94
+ try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
95
+ catch (e) { resolve(cached); }
96
+ });
97
+ });
98
+ } catch (e) { return resolve(cached); }
99
+ req.on('timeout', function () { req.destroy(); resolve(cached); });
100
+ req.on('error', function () { resolve(cached); });
101
+ });
102
+ }
103
+ module.exports = { ensure, BUY_URL, storePath, pullFeed };
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@readystack/aks-eks-support-cliff-lint",
3
+ "version": "1.0.2",
4
+ "description": "Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "keywords": [
10
+ "aks",
11
+ "eks",
12
+ "terraform",
13
+ "kubernetes version",
14
+ "aks long term support",
15
+ "eks extended support",
16
+ "azure kubernetes service",
17
+ "premium tier"
18
+ ],
19
+ "homepage": "https://getreadystack.com",
20
+ "funding": "https://getreadystack.com/api/buy/cl/polar_cl_h4hStvanMbFQkOvKD4EQ6BWS54rfLlOpDYCBB0UJnAI",
21
+ "bin": {
22
+ "aks-eks-support-cliff-lint": "cli.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "mcpName": "io.github.jmshinhwa/aks-eks-support-cliff-lint",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/jmshinhwa/readystack-themes.git",
31
+ "directory": "aks-eks-support-cliff-lint"
32
+ },
33
+ "files": [
34
+ "cli.js",
35
+ "license.js",
36
+ "rules.json",
37
+ "strings.json",
38
+ "README.md",
39
+ "LICENSE.txt",
40
+ "engine.js"
41
+ ]
42
+ }
@@ -0,0 +1,122 @@
1
+ [
2
+ {
3
+ "id": "eks-extended-billing",
4
+ "sev": "error",
5
+ "title": "EKS version is in extended support and billed at $0.60 per cluster-hour instead of $0.10",
6
+ "source": "https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html",
7
+ "price_standard_hour": 0.1,
8
+ "price_extended_hour": 0.6,
9
+ "hours_per_month": 730,
10
+ "eks_calendar": {
11
+ "1.31": {
12
+ "std_end": "2025-11-26",
13
+ "ext_end": "2026-11-26"
14
+ },
15
+ "1.32": {
16
+ "std_end": "2026-03-23",
17
+ "ext_end": "2027-03-23"
18
+ },
19
+ "1.33": {
20
+ "std_end": "2026-07-29",
21
+ "ext_end": "2027-07-29"
22
+ },
23
+ "1.34": {
24
+ "std_end": "2026-12-02",
25
+ "ext_end": "2027-12-02"
26
+ },
27
+ "1.35": {
28
+ "std_end": "2027-03-27",
29
+ "ext_end": "2028-03-27"
30
+ },
31
+ "1.36": {
32
+ "std_end": "2027-08-02",
33
+ "ext_end": "2028-08-02"
34
+ }
35
+ }
36
+ },
37
+ {
38
+ "id": "eks-forced-upgrade-soon",
39
+ "sev": "error",
40
+ "title": "EKS extended support ends within the warning window; AWS then upgrades the control plane with no notice",
41
+ "window_days": 90
42
+ },
43
+ {
44
+ "id": "eks-past-extended",
45
+ "sev": "error",
46
+ "title": "EKS version is past end of extended support; the control plane is auto-upgraded and node groups are left behind"
47
+ },
48
+ {
49
+ "id": "eks-cliff-soon",
50
+ "sev": "warning",
51
+ "title": "EKS standard support ends within the warning window (auto-upgrade under STANDARD, 6x fee under EXTENDED)",
52
+ "window_days": 90
53
+ },
54
+ {
55
+ "id": "aks-lts-needs-premium",
56
+ "sev": "error",
57
+ "title": "support_plan AKSLongTermSupport is only available on sku_tier Premium",
58
+ "source": "https://learn.microsoft.com/en-us/azure/aks/supported-kubernetes-versions",
59
+ "price_standard_hour": 0.1,
60
+ "price_premium_hour": 0.6,
61
+ "hours_per_month": 730,
62
+ "aks_calendar": {
63
+ "1.29": {
64
+ "eol": "2025-03-31",
65
+ "lts_eol": "2026-04-30"
66
+ },
67
+ "1.30": {
68
+ "eol": "2025-08-22",
69
+ "lts_eol": "2026-07-31"
70
+ },
71
+ "1.31": {
72
+ "eol": "2025-11-01",
73
+ "lts_eol": "2026-11-30"
74
+ },
75
+ "1.32": {
76
+ "eol": "2026-03-31",
77
+ "lts_eol": "2027-03-31"
78
+ },
79
+ "1.33": {
80
+ "eol": "2026-07-31",
81
+ "lts_eol": "2027-07-31"
82
+ },
83
+ "1.34": {
84
+ "eol": "2026-11-30",
85
+ "lts_eol": "2027-11-30"
86
+ },
87
+ "1.35": {
88
+ "eol": "2027-03-31",
89
+ "lts_eol": "2028-03-31"
90
+ },
91
+ "1.36": {
92
+ "eol": "2027-06-30",
93
+ "lts_eol": "2028-06-30"
94
+ },
95
+ "1.37": {
96
+ "eol": "2027-10-31",
97
+ "lts_eol": "2028-10-31"
98
+ }
99
+ }
100
+ },
101
+ {
102
+ "id": "aks-out-of-support",
103
+ "sev": "error",
104
+ "title": "AKS version is past community end of life without LTS: no security patches, no new clusters"
105
+ },
106
+ {
107
+ "id": "aks-lts-ended",
108
+ "sev": "error",
109
+ "title": "AKS version is past its LTS end of life"
110
+ },
111
+ {
112
+ "id": "aks-eol-soon",
113
+ "sev": "warning",
114
+ "title": "AKS community support ends within the warning window and LTS is not enabled",
115
+ "window_days": 90
116
+ },
117
+ {
118
+ "id": "aks-premium-unused",
119
+ "sev": "info",
120
+ "title": "Premium tier bought for LTS while the version is still in community support"
121
+ }
122
+ ]
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "AKS LTS & EKS Extended Support Lint for Terraform",
3
+ "subtitle": "Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules",
4
+ "bin": "aks-eks-support-cliff-lint",
5
+ "price": 29,
6
+ "free": "Lint the open .tf file with all 9 rules: every EKS and AKS cluster block checked against the dated calendars, with the dollars already billed.",
7
+ "paid": "Scan the whole workspace and export a per-cluster CSV of fees, cliff dates and fixes for a platform or FinOps review.",
8
+ "need_key": "This option needs a licence (AKS LTS & EKS Extended Support Lint for Terraform).",
9
+ "exts": []
10
+ }
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: readystack-aks-eks-support-cliff-lint
3
+ Version: 1.0.2
4
+ Summary: Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules (bundled Node.js runtime)
5
+ Author-email: ReadyStack <support@getreadystack.com>
6
+ License: Proprietary - see LICENSE.txt
7
+ Project-URL: Homepage, https://getreadystack.com/tools/aks-eks-support-cliff-lint?ref=pypi
8
+ Project-URL: Documentation, https://getreadystack.com/tools/aks-eks-support-cliff-lint?ref=pypi
9
+ Project-URL: Support, https://getreadystack.com/support
10
+ Keywords: aks,eks,terraform,kubernetes version,aks long term support,eks extended support,azure kubernetes service,premium tier
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Environment :: Console
13
+ Classifier: Topic :: Software Development :: Quality Assurance
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: nodejs-wheel-binaries>=20
19
+ Dynamic: license-file
20
+
21
+ # AKS LTS & EKS Extended Support Lint for Terraform
22
+
23
+ > Runs the same checker as the npm package `@readystack/aks-eks-support-cliff-lint` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
24
+
25
+ ![AKS LTS & EKS Extended Support Lint for Terraform — finds the line](https://getreadystack.com/img/promo/aks-eks-support-cliff-lint_demo.gif)
26
+
27
+ ![AKS LTS & EKS Extended Support Lint for Terraform](https://getreadystack.com/img/promo/sku326983_result_card.jpg)
28
+
29
+ Terraform EKS and AKS version check: sample clusters.tf on 2026-09-27 — $2,256 extra on EKS 1.32 since 2026-03-23, $3,660 on 1.31 since 2025-11-26, $5,916 total; 9 rules
30
+
31
+ ## Install
32
+
33
+ ```
34
+ aks-eks-support-cliff-lint file
35
+ ```
36
+
37
+ Node 18+. The same 9 rules as the VS Code extension, from a terminal or CI.
38
+
39
+ ## Free
40
+
41
+ - Lint the open .tf file with all 9 rules: every EKS and AKS cluster block checked against the dated calendars, with the dollars already billed.
42
+ - `--rules` lists every rule
43
+
44
+ ## With a licence ($29 once)
45
+
46
+ - Scan the whole workspace and export a per-cluster CSV of fees, cliff dates and fixes for a platform or FinOps review.
47
+
48
+ ```
49
+ @readystack/aks-eks-support-cliff-lint --dir ./templates --report html --out report.html
50
+ ```
51
+
52
+ Amazon EKS bills $0.60 per cluster-hour in extended support against $0.10 in standard support, so one cluster left on an old version costs $365 a month or $4,380 a year extra; AKS Premium tier for LTS is $0.60 against $0.10 for Standard.
53
+
54
+ ## Use from an AI agent (MCP)
55
+
56
+ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
57
+
58
+ ```json
59
+ { "mcpServers": { "aks-eks-support-cliff-lint": { "command": "npx", "args": ["-y", "@readystack/aks-eks-support-cliff-lint", "--mcp"] } } }
60
+ ```
61
+
62
+ Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
63
+
64
+ ## Use in CI
65
+
66
+ ```yaml
67
+ - name: AKS LTS & EKS Extended Support Lint for Terraform
68
+ run: npx -y @readystack/aks-eks-support-cliff-lint --dir . --ci
69
+ ```
70
+
71
+ (container: `docker run --rm -v "$PWD:/work" getreadystack/aks-eks-support-cliff-lint --dir /work --ci`)
72
+
73
+ The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
74
+
75
+ [Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_h4hStvanMbFQkOvKD4EQ6BWS54rfLlOpDYCBB0UJnAI)
76
+
77
+
78
+ <!-- aks lts premium tier -->
79
+
80
+
81
+ ## Install (PyPI)
82
+
83
+ ```
84
+ pip install readystack-aks-eks-support-cliff-lint
85
+ aks-eks-support-cliff-lint --help
86
+ ```
@@ -0,0 +1,17 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ src/readystack_aks_eks_support_cliff_lint/__init__.py
5
+ src/readystack_aks_eks_support_cliff_lint/__main__.py
6
+ src/readystack_aks_eks_support_cliff_lint.egg-info/PKG-INFO
7
+ src/readystack_aks_eks_support_cliff_lint.egg-info/SOURCES.txt
8
+ src/readystack_aks_eks_support_cliff_lint.egg-info/dependency_links.txt
9
+ src/readystack_aks_eks_support_cliff_lint.egg-info/entry_points.txt
10
+ src/readystack_aks_eks_support_cliff_lint.egg-info/requires.txt
11
+ src/readystack_aks_eks_support_cliff_lint.egg-info/top_level.txt
12
+ src/readystack_aks_eks_support_cliff_lint/js/cli.js
13
+ src/readystack_aks_eks_support_cliff_lint/js/engine.js
14
+ src/readystack_aks_eks_support_cliff_lint/js/license.js
15
+ src/readystack_aks_eks_support_cliff_lint/js/package.json
16
+ src/readystack_aks_eks_support_cliff_lint/js/rules.json
17
+ src/readystack_aks_eks_support_cliff_lint/js/strings.json
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aks-eks-support-cliff-lint = readystack_aks_eks_support_cliff_lint.__main__:main