@readystack/mssql-container-licence-lint 1.0.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/LICENSE.txt ADDED
@@ -0,0 +1,15 @@
1
+ SQL Server Docker Licence Lint (MSSQL_PID) — Licence
2
+
3
+ Free scope
4
+ Every finding and the per-container list-price exposure, in the editor and in the web page, free with no key 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
+ Workspace-wide licence inventory: one CSV/Markdown table of every SQL Server container, its edition, its core count and its list-price cost, ready to hand to a licence auditor 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
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # SQL Server Docker Licence Lint (MSSQL_PID)
2
+
3
+ ![SQL Server Docker Licence Lint (MSSQL_PID)](https://getreadystack.com/img/promo/sku322126_result_card.jpg)
4
+
5
+ mssql docker licence check for compose, Kubernetes and Dockerfiles
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npx @readystack/mssql-container-licence-lint file
11
+ ```
12
+
13
+ Node 18+. The same 13 rules as the VS Code extension, from a terminal or CI.
14
+
15
+ ## Free
16
+
17
+ - Every finding and the per-container list-price exposure for the open file, in the editor and in the free web page, with no key. Measured on the bundled sample: an 8-core production container with MSSQL_PID unset: $60,492 Enterprise or $15,780 Standard; a 2-CPU Standard container is billed 4 core licences: $7,890. 13 rules.
18
+ - `--rules` lists every rule
19
+
20
+ ## With a licence ($29 once)
21
+
22
+ - Workspace sweep: every compose, Kubernetes and Dockerfile in the workspace checked at once, with a written Markdown report of each SQL Server container finding and its list-price exposure, ready to hand to a licence auditor.
23
+
24
+ ```
25
+ @readystack/mssql-container-licence-lint --dir ./templates --report html --out report.html
26
+ ```
27
+
28
+ SQL Server 2022 list price, Enterprise $15,123 and Standard $3,945 per 2-core pack. an 8-core production container with MSSQL_PID unset: $60,492 Enterprise or $15,780 Standard.
29
+
30
+ ## Use from an AI agent (MCP)
31
+
32
+ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
33
+
34
+ ```json
35
+ { "mcpServers": { "mssql-container-licence-lint": { "command": "npx", "args": ["-y", "@readystack/mssql-container-licence-lint", "--mcp"] } } }
36
+ ```
37
+
38
+ Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
39
+
40
+ ## Use in CI
41
+
42
+ ```yaml
43
+ - name: SQL Server Docker Licence Lint (MSSQL_PID)
44
+ run: npx -y @readystack/mssql-container-licence-lint --dir . --ci
45
+ ```
46
+
47
+ (container: `docker run --rm -v "$PWD:/work" getreadystack/mssql-container-licence-lint --dir /work --ci`)
48
+
49
+ The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
50
+
51
+ [Get a licence](https://buy.polar.sh/polar_cl_DzCbdny3aDGMKFdMkOdguhRSGbgiNeZeNnCSh0b7UhV)
52
+
53
+
54
+ <!-- mssql docker -->
package/cli.js ADDED
@@ -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); });
package/engine.js ADDED
@@ -0,0 +1,126 @@
1
+ // SQL Server container licence lint - same file runs in VS Code (node) and in the browser.
2
+ (function () {
3
+ var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.MSQL_RULES;
4
+ var BY = {}; RULES.forEach(function (r) { BY[r.id] = r; });
5
+ // SQL Server 2022 open no-level list price (USD) per 2-core pack, microsoft.com/sql-server/sql-server-2022-pricing
6
+ var PACK = { Enterprise: 15123, Standard: 3945 };
7
+ var MIN_CORES = 4;
8
+ var IMAGE = /(?:mcr\.microsoft\.com\/)?mssql\/server(?::([\w.-]+))?/i;
9
+ var KEY = /\b[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\b/;
10
+ var PROD = /\b(prod|production|live)\b/i;
11
+
12
+ function money(n) { return '$' + Math.round(n).toLocaleString('en-US'); }
13
+ function cost(edition, cores) { return PACK[edition] * Math.ceil(Math.max(MIN_CORES, cores) / 2); }
14
+ function indent(s) { return s.match(/^\s*/)[0].length; }
15
+ function cpuVal(v) {
16
+ v = String(v).replace(/["']/g, '').trim();
17
+ if (/m$/.test(v)) return parseFloat(v) / 1000;
18
+ return parseFloat(v);
19
+ }
20
+
21
+ // A block = the compose service / k8s container / Dockerfile that holds the mssql image.
22
+ function blocks(lines) {
23
+ var out = [];
24
+ var isDockerfile = lines.some(function (l) { return /^\s*FROM\s+\S*mssql\/server/i.test(l); });
25
+ if (isDockerfile) return [{ start: 0, end: lines.length - 1, img: lines.findIndex(function (l) { return /^\s*FROM\s/i.test(l) && IMAGE.test(l); }) }];
26
+ lines.forEach(function (l, i) {
27
+ if (!/^\s*-?\s*image\s*:/.test(l) || !IMAGE.test(l)) return;
28
+ var k = indent(l.replace('-', ' ')), s = i;
29
+ for (var j = i - 1; j >= 0; j--) {
30
+ if (!lines[j].trim() || /^\s*#/.test(lines[j])) continue;
31
+ var dj = indent(lines[j]);
32
+ if (/^\s*-\s/.test(lines[j]) && dj < k) { s = j; break; }
33
+ if (dj < k) { s = j; break; }
34
+ }
35
+ var base = indent(lines[s]), e = lines.length - 1;
36
+ for (var m = i + 1; m < lines.length; m++) {
37
+ if (!lines[m].trim() || /^\s*#/.test(lines[m])) continue;
38
+ if (/^---/.test(lines[m]) || indent(lines[m]) <= base) { e = m - 1; break; }
39
+ }
40
+ out.push({ start: s, end: e, img: i });
41
+ });
42
+ return out;
43
+ }
44
+
45
+ function scan(lines, b) {
46
+ var r = { pid: null, pidLine: -1, eula: false, cpus: null, cpuLine: -1, tag: null, prod: false };
47
+ var m = lines[b.img].match(IMAGE); r.tag = m && m[1] ? m[1] : null;
48
+ var inLimits = false, limInd = -1;
49
+ for (var i = b.start; i <= b.end; i++) {
50
+ var l = lines[i];
51
+ if (/^\s*#/.test(l)) continue;
52
+ if (PROD.test(l)) r.prod = true;
53
+ if (/ACCEPT_EULA/.test(l)) r.eula = true;
54
+ var p = l.match(/MSSQL_PID\s*[=:]\s*["']?([\w-]+)/);
55
+ if (p) { r.pid = p[1]; r.pidLine = i; }
56
+ if (/name\s*:\s*["']?MSSQL_PID\b/.test(l)) {
57
+ for (var n = i + 1; n <= Math.min(i + 3, b.end); n++) {
58
+ var v = lines[n].match(/^\s*value\s*:\s*["']?([\w-]+)/);
59
+ if (v) { r.pid = v[1]; r.pidLine = n; break; }
60
+ if (/valueFrom/.test(lines[n])) { r.pid = '(secret)'; r.pidLine = n; break; }
61
+ }
62
+ }
63
+ if (/^\s*limits\s*:/.test(l)) { inLimits = true; limInd = indent(l); continue; }
64
+ if (inLimits && l.trim() && indent(l) <= limInd) inLimits = false;
65
+ var c = l.match(/^\s*cpus?\s*:\s*(["']?[\d.]+m?["']?)/);
66
+ if (c && (inLimits || r.cpus === null)) { r.cpus = cpuVal(c[1]); r.cpuLine = i; }
67
+ var dc = l.match(/--cpus[= ]["']?([\d.]+)/);
68
+ if (dc) { r.cpus = parseFloat(dc[1]); r.cpuLine = i; }
69
+ }
70
+ return r;
71
+ }
72
+
73
+ function check(text, opts) {
74
+ opts = opts || {};
75
+ var lines = String(text).replace(/\r/g, '').split('\n');
76
+ var findings = [];
77
+ function add(id, line, msg) { findings.push({ check: id, sev: BY[id].sev, msg: BY[id].title + ': ' + msg + ' [' + BY[id].source + ']', line: line + 1 }); }
78
+ var fileProd = PROD.test(opts.filename || '');
79
+
80
+ lines.forEach(function (l, i) {
81
+ if (/^\s*#/.test(l)) return;
82
+ if (/MSSQL_PID|product.?key|PID\s*[=:]/i.test(l) && KEY.test(l)) add('product_key_committed', i, 'a licence key in source control is readable by anyone with clone access - move it to a secret and rotate it with your Microsoft reseller.');
83
+ });
84
+
85
+ blocks(lines).forEach(function (b) {
86
+ var s = scan(lines, b), prod = s.prod || fileProd;
87
+ var pid = s.pid, cores = s.cpus, ver = s.tag && /^(\d{4})/.test(s.tag) ? +s.tag.slice(0, 4) : null;
88
+ var coreTxt = cores === null ? 'at least ' + MIN_CORES + ' cores' : Math.max(MIN_CORES, Math.ceil(cores)) + ' cores';
89
+ var ce = cores === null ? MIN_CORES : Math.ceil(cores);
90
+ var exposure = ' List-price exposure for ' + coreTxt + ': Standard ' + money(cost('Standard', ce)) + ' or Enterprise ' + money(cost('Enterprise', ce)) + ' (SQL Server 2022 2-core packs).';
91
+ if (!s.eula) add('eula_missing', b.img, 'add ACCEPT_EULA=Y - without it the container exits on start.');
92
+ if (!s.tag || s.tag === 'latest') add('tag_unpinned', b.img, 'pin a version tag such as 2022-latest or 2025-latest so the edition limits you licensed for do not change under you.');
93
+ if (!pid) {
94
+ add('pid_unset', b.img, (prod ? 'this production container' : 'this container') + ' starts as Developer edition, which is licensed for development and test only.' + (prod ? exposure : ' Set MSSQL_PID explicitly.'));
95
+ return;
96
+ }
97
+ if (pid === '(secret)') return;
98
+ var P = pid.toLowerCase(), L = s.pidLine;
99
+ if (/developer$/.test(P)) {
100
+ if (prod) add('pid_developer_prod', L, pid + ' may not serve production workloads.' + exposure);
101
+ if (P !== 'developer' && ver && ver < 2025) add('pid_2025_only', L, pid + ' on image tag ' + s.tag + ' - use Developer before SQL Server 2025.');
102
+ return;
103
+ }
104
+ if (P === 'evaluation') { add('pid_evaluation', L, 'the evaluation period ends 180 days after install.' + (prod ? exposure : '')); return; }
105
+ if (P === 'web') { add('pid_web', L, 'Web edition is sold only through a hosting (SPLA) partner; outside a hosting agreement choose Standard or EnterpriseCore.'); return; }
106
+ if (P === 'express') {
107
+ if (cores !== null && cores > 4) add('express_core_cap', s.cpuLine, cores + ' CPUs allocated, Express uses at most 4 cores and 1,410 MB of buffer pool.');
108
+ return;
109
+ }
110
+ var ed = P === 'standard' ? 'Standard' : (P === 'enterprisecore' || P === 'enterprise') ? 'Enterprise' : null;
111
+ if (!ed) return;
112
+ if (P === 'enterprise') add('pid_enterprise_legacy', L, 'use EnterpriseCore for core-based licensing' + (cores !== null && cores > 20 ? '; ' + cores + ' CPUs exceed the 20-core cap of this PID.' : '.'));
113
+ if (cores === null) add('no_cpu_limit', b.img, ed + ' with no CPU limit sees every host core, and every virtual core must be licensed. Set deploy.resources.limits.cpus (compose) or resources.limits.cpu (Kubernetes).');
114
+ else if (cores < MIN_CORES) add('core_minimum', s.cpuLine, cores + ' CPUs allocated, ' + MIN_CORES + ' core licences billed: ' + money(cost(ed, cores)) + ' at ' + ed + ' list price.');
115
+ if (ed === 'Standard' && cores !== null) {
116
+ var cap = ver && ver >= 2025 ? 32 : 24;
117
+ if (cores > cap) add('standard_core_cap', s.cpuLine, cores + ' CPUs allocated, Standard ' + (ver || '2022') + ' uses ' + cap + '; the extra ' + (Math.ceil(cores) - cap) + ' cores cost ' + money(cost('Standard', Math.ceil(cores)) - cost('Standard', cap)) + ' in licences that do no work.');
118
+ }
119
+ });
120
+ return { findings: findings };
121
+ }
122
+
123
+ var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length, cost: cost };
124
+ if (typeof module !== 'undefined') module.exports = api;
125
+ if (typeof window !== 'undefined') window.MSQLENGINE = api;
126
+ })();
package/license.js ADDED
@@ -0,0 +1,83 @@
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 = 'fb75b651-aaa9-48b1-b41c-4bb7024827d7'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
6
+ const BUY_URL = 'https://buy.polar.sh/polar_cl_DzCbdny3aDGMKFdMkOdguhRSGbgiNeZeNnCSh0b7UhV';
7
+ const SLUG = 'mssql-container-licence-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
+ function validate(key) {
15
+ 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); });
16
+ }
17
+ function validate1(key, ben) {
18
+ return new Promise(function (resolve) {
19
+ if (!ORG_ID) return resolve({ ok: false, offline: false });
20
+ 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 });
21
+ const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
22
+ headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
23
+ let buf = ''; res.on('data', function (d) { buf += d; });
24
+ res.on('end', function () {
25
+ if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
26
+ try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
27
+ catch (e) { resolve({ ok: false, offline: false }); }
28
+ });
29
+ });
30
+ req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
31
+ req.on('error', function () { resolve({ ok: false, offline: true }); });
32
+ req.write(body); req.end();
33
+ });
34
+ }
35
+ // ★s151 2026-09-19 — 키 판(키가 없거나 거절된 순간)을 익명으로 센다 (슬러그·출처·이유만) · DO_NOT_TRACK=1 · READYSTACK_NO_TELEMETRY 면 안 보낸다 · 실패는 조용히 · 프로세스당 한 번.
36
+ let _pinged = false;
37
+ function pingPaywall(why) {
38
+ try {
39
+ if (_pinged) return; _pinged = true;
40
+ if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return; // s151: CI(깃허브 액션 등)와 우리 빌드는 손님이 아니다
41
+ if (!(process.stdout.isTTY || process.stdin.isTTY || process.env.READYSTACK_MCP === '1')) return; // s152: 사람 자리(터미널·MCP 세션)에서만 센다 - 발행 1분 뒤 남의 실행기(JP · 우리 3대는 US)가 돌린 no_key 13건은 손님이 아니다
42
+ const body = JSON.stringify({ t: 'paywall', slug: SLUG, src: process.env.READYSTACK_MCP === '1' ? 'mcp' : 'cli', why: why || 'no_key' });
43
+ const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
44
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
45
+ req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
46
+ req.write(body); req.end();
47
+ } catch (e) { /* 세는 것이 실패해도 상품은 돈다 */ }
48
+ }
49
+ async function ensure(explicitKey) {
50
+ const st = load();
51
+ const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
52
+ if (!key) { pingPaywall('no_key'); return { ok: false, why: 'no_key' }; } // s151
53
+ const age = Date.now() - (st.okAt || 0);
54
+ if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
55
+ const r = await validate(String(key).trim());
56
+ if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
57
+ if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
58
+ if (!r.offline) pingPaywall('invalid'); // s151
59
+ return { ok: false, why: r.offline ? 'offline' : 'invalid' };
60
+ }
61
+ // ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
62
+ const FEED_URL = 'https://getreadystack.com/api/rules/';
63
+ function pullFeed() {
64
+ const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
65
+ if (!key) return Promise.resolve(cached);
66
+ if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
67
+ return new Promise(function (resolve) {
68
+ let req;
69
+ try {
70
+ req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
71
+ let buf = ''; res.on('data', function (d) { buf += d; });
72
+ res.on('end', function () {
73
+ if (res.statusCode !== 200) return resolve(cached);
74
+ 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); }
75
+ catch (e) { resolve(cached); }
76
+ });
77
+ });
78
+ } catch (e) { return resolve(cached); }
79
+ req.on('timeout', function () { req.destroy(); resolve(cached); });
80
+ req.on('error', function () { resolve(cached); });
81
+ });
82
+ }
83
+ module.exports = { ensure, BUY_URL, storePath, pullFeed };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@readystack/mssql-container-licence-lint",
3
+ "version": "1.0.0",
4
+ "description": "mssql docker licence check for compose, Kubernetes and Dockerfiles",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "keywords": [
10
+ "mssql",
11
+ "sql server",
12
+ "docker",
13
+ "MSSQL_PID",
14
+ "licence audit",
15
+ "kubernetes",
16
+ "docker-compose",
17
+ "licensing"
18
+ ],
19
+ "homepage": "https://getreadystack.com",
20
+ "funding": "https://buy.polar.sh/polar_cl_DzCbdny3aDGMKFdMkOdguhRSGbgiNeZeNnCSh0b7UhV",
21
+ "bin": {
22
+ "mssql-container-licence-lint": "cli.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "mcpName": "io.github.jmshinhwa/mssql-container-licence-lint",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/jmshinhwa/readystack-themes.git",
31
+ "directory": "mssql-container-licence-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
+ }
package/rules.json ADDED
@@ -0,0 +1,80 @@
1
+ [
2
+ {
3
+ "id": "pid_unset",
4
+ "sev": "error",
5
+ "title": "MSSQL_PID not set: the container runs Developer edition",
6
+ "source": "Docker Hub mssql/server: MSSQL_PID (default: Developer)"
7
+ },
8
+ {
9
+ "id": "pid_developer_prod",
10
+ "sev": "error",
11
+ "title": "Developer edition in a production file",
12
+ "source": "Microsoft: Developer editions are licensed for use as a development and test system, not as a production server"
13
+ },
14
+ {
15
+ "id": "pid_evaluation",
16
+ "sev": "error",
17
+ "title": "Evaluation edition expires",
18
+ "source": "Microsoft: an evaluation deployment is available for 180 days"
19
+ },
20
+ {
21
+ "id": "pid_web",
22
+ "sev": "warning",
23
+ "title": "Web edition is hosting only",
24
+ "source": "Microsoft SQL Server 2022 pricing: Web = Hosting only; not in the SQL Server 2025 MSSQL_PID table"
25
+ },
26
+ {
27
+ "id": "pid_enterprise_legacy",
28
+ "sev": "warning",
29
+ "title": "Legacy Enterprise (Server + CAL) PID",
30
+ "source": "Microsoft: Enterprise PID is limited to 20 cores per instance and isn't available for new agreements; choose EnterpriseCore"
31
+ },
32
+ {
33
+ "id": "product_key_committed",
34
+ "sev": "error",
35
+ "title": "SQL Server product key committed to the repository",
36
+ "source": "Microsoft: a product key has the form #####-#####-#####-#####-#####"
37
+ },
38
+ {
39
+ "id": "core_minimum",
40
+ "sev": "warning",
41
+ "title": "Fewer than 4 cores still costs 4 core licences",
42
+ "source": "SQL Server licensing: minimum of four core licences per virtual OSE (VM or container)"
43
+ },
44
+ {
45
+ "id": "no_cpu_limit",
46
+ "sev": "warning",
47
+ "title": "Paid edition with no CPU limit",
48
+ "source": "Microsoft SQL Server 2022 pricing: license all the virtual cores on the virtual OSE"
49
+ },
50
+ {
51
+ "id": "standard_core_cap",
52
+ "sev": "warning",
53
+ "title": "Standard edition cannot use these cores",
54
+ "source": "Microsoft: Standard is limited to lesser of 4 sockets or 32 cores (2025); 24 cores in SQL Server 2022 and earlier"
55
+ },
56
+ {
57
+ "id": "express_core_cap",
58
+ "sev": "info",
59
+ "title": "Express edition cannot use these cores",
60
+ "source": "Microsoft: Express is limited to lesser of 1 socket or 4 cores, 1,410 MB buffer pool, 50 GB database (2025)"
61
+ },
62
+ {
63
+ "id": "eula_missing",
64
+ "sev": "error",
65
+ "title": "ACCEPT_EULA missing",
66
+ "source": "Microsoft: ACCEPT_EULA is a required setting for the SQL Server image"
67
+ },
68
+ {
69
+ "id": "tag_unpinned",
70
+ "sev": "warning",
71
+ "title": "Image tag not pinned to a SQL Server version",
72
+ "source": "Edition limits and MSSQL_PID values differ between SQL Server 2022 and 2025"
73
+ },
74
+ {
75
+ "id": "pid_2025_only",
76
+ "sev": "warning",
77
+ "title": "StandardDeveloper / EnterpriseDeveloper on a pre-2025 image",
78
+ "source": "Microsoft: StandardDeveloper and EnterpriseDeveloper are listed in the SQL Server 2025 MSSQL_PID table only"
79
+ }
80
+ ]
package/strings.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "SQL Server Docker Licence Lint (MSSQL_PID)",
3
+ "subtitle": "mssql docker licence check for compose, Kubernetes and Dockerfiles",
4
+ "bin": "mssql-container-licence-lint",
5
+ "price": 29,
6
+ "free": "Every finding and the per-container list-price exposure for the open file, in the editor and in the free web page, with no key. Measured on the bundled sample: an 8-core production container with MSSQL_PID unset: $60,492 Enterprise or $15,780 Standard; a 2-CPU Standard container is billed 4 core licences: $7,890. 13 rules.",
7
+ "paid": "Workspace sweep: every compose, Kubernetes and Dockerfile in the workspace checked at once, with a written Markdown report of each SQL Server container finding and its list-price exposure, ready to hand to a licence auditor.",
8
+ "need_key": "This option needs a licence (SQL Server Docker Licence Lint (MSSQL_PID)).",
9
+ "exts": []
10
+ }