rulestamp-docker-hub-pull-limit-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.
- rulestamp_docker_hub_pull_limit_lint-1.0.2/LICENSE.txt +15 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/PKG-INFO +85 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/README.md +65 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/pyproject.toml +29 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/setup.cfg +4 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/__init__.py +1 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/__main__.py +12 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/cli.js +161 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/engine.js +161 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/license.js +103 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/package.json +42 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/rules.json +90 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/strings.json +10 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint.egg-info/PKG-INFO +85 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint.egg-info/SOURCES.txt +17 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint.egg-info/dependency_links.txt +1 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint.egg-info/entry_points.txt +2 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint.egg-info/requires.txt +1 -0
- rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Docker Hub Pull Limit Lint — CI rate limit guard — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Lint the open workflow, compose, GitLab CI, Kubernetes or Dockerfile: every anonymous Docker Hub pull, with line and fix. 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 in one run and export a pull inventory report (Markdown) to attach to the CI ticket. 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,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rulestamp-docker-hub-pull-limit-lint
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per (bundled Node.js runtime)
|
|
5
|
+
Author-email: Rulestamp <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/docker-hub-pull-limit-lint?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/docker-hub-pull-limit-lint?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: docker,docker hub,rate limit,toomanyrequests,github actions,docker-compose,ci,kubernetes
|
|
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
|
+
# Docker Hub Pull Limit Lint
|
|
22
|
+
|
|
23
|
+
> Runs the same checker as our npm package 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
|
+

|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet (docs.docker.com, checked 2026-09-27).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
docker-hub-pull-limit-lint file
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Node 18+. The same 10 rules as the VS Code extension, from a terminal or CI.
|
|
38
|
+
|
|
39
|
+
## Free
|
|
40
|
+
|
|
41
|
+
- Lint the open workflow, compose, GitLab CI, Kubernetes or Dockerfile and see every anonymous Docker Hub pull with line and fix, using all 10 rules.
|
|
42
|
+
- `--rules` lists every rule
|
|
43
|
+
|
|
44
|
+
## With a licence ($29 once)
|
|
45
|
+
|
|
46
|
+
- Scan the whole workspace in one run and export a Markdown pull inventory report to attach to the CI ticket.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
docker-hub-pull-limit-lint --dir ./templates --report html --out report.html
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Docker Pro costs $9 per user per month billed yearly ($108 a year), and it only lifts the limit for pulls that log in; this lint costs $29 once.
|
|
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": { "docker-hub-pull-limit-lint": { "command": "docker-hub-pull-limit-lint", "args": ["--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: Docker Hub Pull Limit Lint
|
|
68
|
+
run: pip install rulestamp-docker-hub-pull-limit-lint && docker-hub-pull-limit-lint --dir . --ci
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
73
|
+
|
|
74
|
+
[Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
<!-- docker hub pull limit lint -->
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Install (PyPI)
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install rulestamp-docker-hub-pull-limit-lint
|
|
84
|
+
docker-hub-pull-limit-lint --help
|
|
85
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Docker Hub Pull Limit Lint
|
|
2
|
+
|
|
3
|
+
> Runs the same checker as our npm package 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
|
+

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet (docs.docker.com, checked 2026-09-27).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
docker-hub-pull-limit-lint file
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node 18+. The same 10 rules as the VS Code extension, from a terminal or CI.
|
|
18
|
+
|
|
19
|
+
## Free
|
|
20
|
+
|
|
21
|
+
- Lint the open workflow, compose, GitLab CI, Kubernetes or Dockerfile and see every anonymous Docker Hub pull with line and fix, using all 10 rules.
|
|
22
|
+
- `--rules` lists every rule
|
|
23
|
+
|
|
24
|
+
## With a licence ($29 once)
|
|
25
|
+
|
|
26
|
+
- Scan the whole workspace in one run and export a Markdown pull inventory report to attach to the CI ticket.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
docker-hub-pull-limit-lint --dir ./templates --report html --out report.html
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Docker Pro costs $9 per user per month billed yearly ($108 a year), and it only lifts the limit for pulls that log in; this lint costs $29 once.
|
|
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": { "docker-hub-pull-limit-lint": { "command": "docker-hub-pull-limit-lint", "args": ["--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: Docker Hub Pull Limit Lint
|
|
48
|
+
run: pip install rulestamp-docker-hub-pull-limit-lint && docker-hub-pull-limit-lint --dir . --ci
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
53
|
+
|
|
54
|
+
[Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
<!-- docker hub pull limit lint -->
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Install (PyPI)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
pip install rulestamp-docker-hub-pull-limit-lint
|
|
64
|
+
docker-hub-pull-limit-lint --help
|
|
65
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rulestamp-docker-hub-pull-limit-lint"
|
|
7
|
+
version = "1.0.2"
|
|
8
|
+
description = "Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per (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 = ["docker", "docker hub", "rate limit", "toomanyrequests", "github actions", "docker-compose", "ci", "kubernetes"]
|
|
14
|
+
authors = [{name = "Rulestamp", 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/docker-hub-pull-limit-lint?ref=pypi"
|
|
19
|
+
Documentation = "https://getreadystack.com/tools/docker-hub-pull-limit-lint?ref=pypi"
|
|
20
|
+
Support = "https://getreadystack.com/support"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
"docker-hub-pull-limit-lint" = "rulestamp_docker_hub_pull_limit_lint.__main__:main"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
"rulestamp_docker_hub_pull_limit_lint" = ["js/*"]
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.2'
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/__main__.py
ADDED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
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); });
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/engine.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.DH_RULES;
|
|
3
|
+
const BY = {};
|
|
4
|
+
RULES.forEach(r => { BY[r.id] = r; });
|
|
5
|
+
const HUB_HOSTS = ['docker.io', 'index.docker.io', 'registry-1.docker.io', 'registry.hub.docker.com'];
|
|
6
|
+
const VALUE_OPTS = ['-v', '--volume', '-e', '--env', '-w', '--workdir', '--name', '-p', '--publish', '--network', '--net',
|
|
7
|
+
'--entrypoint', '-u', '--user', '--mount', '--env-file', '--platform', '-l', '--label', '-h', '--hostname', '--add-host', '--pull'];
|
|
8
|
+
|
|
9
|
+
function clean(v) {
|
|
10
|
+
return String(v || '').replace(/\s+#.*$/, '').trim().replace(/^['"]|['"]$/g, '').trim();
|
|
11
|
+
}
|
|
12
|
+
// returns the image reference when it resolves to Docker Hub, else null
|
|
13
|
+
function hubImage(ref) {
|
|
14
|
+
ref = clean(ref).replace(/^docker:\/\//, '');
|
|
15
|
+
if (!ref || /\$/.test(ref) || ref === 'scratch') return null;
|
|
16
|
+
const parts = ref.split('/');
|
|
17
|
+
const first = parts[0].toLowerCase();
|
|
18
|
+
if (parts.length > 1 && (first.includes('.') || first.includes(':') || first === 'localhost')) {
|
|
19
|
+
return HUB_HOSTS.includes(first) ? ref : null;
|
|
20
|
+
}
|
|
21
|
+
return ref;
|
|
22
|
+
}
|
|
23
|
+
function indent(s) { return s.match(/^\s*/)[0].length; }
|
|
24
|
+
function blank(s) { return !s.trim() || /^\s*#/.test(s); }
|
|
25
|
+
// line index of the parent mapping key of line i (-1 if none)
|
|
26
|
+
function parentOf(lines, i) {
|
|
27
|
+
const n = indent(lines[i].replace(/^(\s*)- /, '$1 '));
|
|
28
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
29
|
+
if (blank(lines[j])) continue;
|
|
30
|
+
if (indent(lines[j]) < n) return j;
|
|
31
|
+
}
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
function keyOf(line) { const m = (line || '').match(/^\s*-?\s*([\w.-]+):/); return m ? m[1] : ''; }
|
|
35
|
+
// does the mapping that contains line i have sibling key `key`?
|
|
36
|
+
function siblingHas(lines, i, key) {
|
|
37
|
+
const n = indent(lines[i]);
|
|
38
|
+
const re = new RegExp('^\\s{' + n + '}' + key + ':');
|
|
39
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
40
|
+
if (blank(lines[j])) continue;
|
|
41
|
+
if (indent(lines[j]) < n) break;
|
|
42
|
+
if (re.test(lines[j])) return true;
|
|
43
|
+
}
|
|
44
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
45
|
+
if (blank(lines[j])) continue;
|
|
46
|
+
if (indent(lines[j]) < n) break;
|
|
47
|
+
if (re.test(lines[j])) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function cliImage(cmd) {
|
|
52
|
+
const m = cmd.match(/\bdocker\s+(?:container\s+|image\s+)?(pull|run|create)\s+(.*)$/);
|
|
53
|
+
if (!m) return null;
|
|
54
|
+
const toks = m[2].split(/\s+/).filter(Boolean);
|
|
55
|
+
for (let k = 0; k < toks.length; k++) {
|
|
56
|
+
const t = toks[k];
|
|
57
|
+
if (/^[<>|&;]/.test(t)) return null;
|
|
58
|
+
if (t.startsWith('-')) { if (VALUE_OPTS.includes(t)) k++; continue; }
|
|
59
|
+
return t;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function kindOf(text) {
|
|
64
|
+
if (/^jobs:\s*$/m.test(text) && /runs-on:/.test(text)) return 'workflow';
|
|
65
|
+
if (/^\s*apiVersion:/m.test(text) && /^\s*kind:\s*\w+/m.test(text)) return 'k8s';
|
|
66
|
+
if (/^services:\s*$/m.test(text)) return 'compose';
|
|
67
|
+
if (/^\s+script:/m.test(text) || /^stages:/m.test(text)) return 'gitlab';
|
|
68
|
+
if (/^\s*FROM\s+\S+/im.test(text)) return 'dockerfile';
|
|
69
|
+
return 'unknown';
|
|
70
|
+
}
|
|
71
|
+
function hit(out, id, line, image, extra) {
|
|
72
|
+
const r = BY[id];
|
|
73
|
+
out.push({ check: id, sev: r.sev, msg: r.title + ' — ' + image + '. ' + r.msg + (extra || ''), fix: r.fix, line: line + 1, image });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function check(text, opts) {
|
|
77
|
+
opts = opts || {};
|
|
78
|
+
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
79
|
+
const kind = opts.kind || kindOf(text);
|
|
80
|
+
const out = [];
|
|
81
|
+
|
|
82
|
+
if (kind === 'workflow') {
|
|
83
|
+
let loggedIn = false, jobIndent = -1, nonHubLogin = null, selfHosted = -1;
|
|
84
|
+
lines.forEach((ln, i) => {
|
|
85
|
+
if (blank(ln)) return;
|
|
86
|
+
const n = indent(ln);
|
|
87
|
+
if (/^jobs:\s*$/.test(ln)) { jobIndent = -2; return; }
|
|
88
|
+
if (jobIndent === -2 && n > 0) jobIndent = n;
|
|
89
|
+
if (n === jobIndent && /^\s*[\w-]+:\s*$/.test(ln)) { loggedIn = false; return; }
|
|
90
|
+
if (/runs-on:.*self-hosted/.test(ln) && selfHosted < 0) selfHosted = i;
|
|
91
|
+
let m;
|
|
92
|
+
if ((m = ln.match(/uses:\s*['"]?docker\/login-action/))) {
|
|
93
|
+
let reg = '';
|
|
94
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) {
|
|
95
|
+
if (/^\s*- /.test(lines[j])) break;
|
|
96
|
+
const r = lines[j].match(/^\s*registry:\s*(.+)$/);
|
|
97
|
+
if (r) reg = clean(r[1]);
|
|
98
|
+
}
|
|
99
|
+
if (!reg || HUB_HOSTS.includes(reg.toLowerCase())) loggedIn = true; else if (!nonHubLogin) nonHubLogin = { i, reg };
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if ((m = ln.match(/\bdocker\s+login\b(.*)$/))) {
|
|
103
|
+
const host = (m[1].split(/\s+/).filter(t => t && !t.startsWith('-') && !/^\$|^['"]?\$/.test(t)).pop() || '');
|
|
104
|
+
if (!host || !host.includes('.') || HUB_HOSTS.includes(host.toLowerCase())) loggedIn = true;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if ((m = ln.match(/^\s*-?\s*uses:\s*['"]?docker:\/\/(\S+)/))) {
|
|
108
|
+
const img = hubImage(m[1]); if (img) hit(out, 'gha-docker-uri-action', i, img); return;
|
|
109
|
+
}
|
|
110
|
+
if ((m = ln.match(/^\s*container:\s*(\S.*)$/)) && clean(m[1])) {
|
|
111
|
+
const img = hubImage(m[1]); if (img) hit(out, 'gha-container-no-credentials', i, img); return;
|
|
112
|
+
}
|
|
113
|
+
if ((m = ln.match(/^\s*image:\s*(\S.*)$/))) {
|
|
114
|
+
const img = hubImage(m[1]); if (!img) return;
|
|
115
|
+
if (siblingHas(lines, i, 'credentials')) return;
|
|
116
|
+
const p = parentOf(lines, i);
|
|
117
|
+
if (keyOf(lines[p]) === 'container') { hit(out, 'gha-container-no-credentials', i, img); return; }
|
|
118
|
+
if (keyOf(lines[parentOf(lines, p)]) === 'services') { hit(out, 'gha-service-no-credentials', i, img); return; }
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const ci = cliImage(ln);
|
|
122
|
+
if (ci && !loggedIn) { const img = hubImage(ci); if (img) hit(out, 'cli-pull-before-login', i, img); }
|
|
123
|
+
});
|
|
124
|
+
const hubPulls = out.length;
|
|
125
|
+
if (nonHubLogin && hubPulls) {
|
|
126
|
+
hit(out, 'login-registry-not-hub', nonHubLogin.i, nonHubLogin.reg);
|
|
127
|
+
}
|
|
128
|
+
if (selfHosted >= 0 && hubPulls) hit(out, 'self-hosted-shared-ip', selfHosted, hubPulls + ' anonymous pull line(s) in this file');
|
|
129
|
+
} else if (kind === 'dockerfile') {
|
|
130
|
+
const stages = new Set();
|
|
131
|
+
lines.forEach((ln, i) => {
|
|
132
|
+
const m = ln.match(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?/i);
|
|
133
|
+
if (!m) return;
|
|
134
|
+
const ref = m[1];
|
|
135
|
+
if (!stages.has(ref.toLowerCase())) { const img = hubImage(ref); if (img) hit(out, 'dockerfile-from-hub', i, img); }
|
|
136
|
+
if (m[2]) stages.add(m[2].toLowerCase());
|
|
137
|
+
});
|
|
138
|
+
} else if (kind === 'compose' || kind === 'k8s' || kind === 'gitlab') {
|
|
139
|
+
const id = kind === 'compose' ? 'compose-image-hub' : kind === 'k8s' ? 'k8s-image-hub-no-pull-secret' : 'gitlab-image-hub-no-auth';
|
|
140
|
+
const covered = kind === 'k8s' ? /imagePullSecrets:/.test(text)
|
|
141
|
+
: kind === 'gitlab' ? /DOCKER_AUTH_CONFIG|CI_DEPENDENCY_PROXY/.test(text) : false;
|
|
142
|
+
let loggedIn = false;
|
|
143
|
+
lines.forEach((ln, i) => {
|
|
144
|
+
if (/\bdocker\s+login\b/.test(ln)) loggedIn = true;
|
|
145
|
+
let m = ln.match(/^\s*-?\s*(?:image|name):\s*(\S.*)$/);
|
|
146
|
+
if (m && (/image:/.test(ln) || keyOf(lines[parentOf(lines, i)]) === 'image')) {
|
|
147
|
+
const img = hubImage(m[1]);
|
|
148
|
+
if (img && !covered && !(kind === 'gitlab' && /^\s*-?\s*name:/.test(ln) && !img.includes(':') && !img.includes('/'))) hit(out, id, i, img);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const ci = cliImage(ln);
|
|
152
|
+
if (ci && !loggedIn && !covered) { const img = hubImage(ci); if (img) hit(out, 'cli-pull-before-login', i, img); }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return { kind, findings: out, pulls: out.filter(f => f.check !== 'self-hosted-shared-ip' && f.check !== 'login-registry-not-hub').length };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const api = { engine: { check, hubImage, kindOf }, RULES, RULE_COUNT: RULES.length };
|
|
159
|
+
if (typeof module !== 'undefined') module.exports = api;
|
|
160
|
+
if (typeof window !== 'undefined') window.DHENGINE = api;
|
|
161
|
+
})();
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/license.js
ADDED
|
@@ -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 = 'b9df2e85-507b-4cdd-8b55-c0782968bd8f'; // 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_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky';
|
|
7
|
+
const SLUG = 'docker-hub-pull-limit-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 };
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/docker-hub-pull-limit-lint",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per ",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"docker",
|
|
11
|
+
"docker hub",
|
|
12
|
+
"rate limit",
|
|
13
|
+
"toomanyrequests",
|
|
14
|
+
"github actions",
|
|
15
|
+
"docker-compose",
|
|
16
|
+
"ci",
|
|
17
|
+
"kubernetes"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://getreadystack.com",
|
|
20
|
+
"funding": "https://getreadystack.com/api/buy/cl/polar_cl_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky",
|
|
21
|
+
"bin": {
|
|
22
|
+
"docker-hub-pull-limit-lint": "cli.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"mcpName": "io.github.jmshinhwa/docker-hub-pull-limit-lint",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
31
|
+
"directory": "docker-hub-pull-limit-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
|
+
}
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/rules.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "gha-service-no-credentials",
|
|
4
|
+
"sev": "error",
|
|
5
|
+
"kind": "workflow",
|
|
6
|
+
"title": "Service container pulled from Docker Hub without credentials",
|
|
7
|
+
"msg": "Service containers are pulled in 'Initialize containers', before any step runs — a docker/login-action step cannot authenticate this pull, so it counts against the anonymous per-IP quota.",
|
|
8
|
+
"fix": "Add credentials: {username, password} to this service, or pull it from a mirror registry.",
|
|
9
|
+
"limits": {
|
|
10
|
+
"unauthenticated": "100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet",
|
|
11
|
+
"personal": "200 pulls per 6 hours",
|
|
12
|
+
"paid": "unlimited (Pro, Team, Business)",
|
|
13
|
+
"source": "https://docs.docker.com/docker-hub/usage/pulls/",
|
|
14
|
+
"checked": "2026-09-27",
|
|
15
|
+
"multi_arch": "a multi-arch image counts one pull per architecture"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "gha-container-no-credentials",
|
|
20
|
+
"sev": "error",
|
|
21
|
+
"kind": "workflow",
|
|
22
|
+
"title": "Job container pulled from Docker Hub without credentials",
|
|
23
|
+
"msg": "The job container is pulled before the first step — a docker/login-action step cannot authenticate it, so it counts against the anonymous per-IP quota.",
|
|
24
|
+
"fix": "Use the long form container: {image, credentials: {username, password}}, or a mirror registry."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "gha-docker-uri-action",
|
|
28
|
+
"sev": "error",
|
|
29
|
+
"kind": "workflow",
|
|
30
|
+
"title": "uses: docker:// image pulled from Docker Hub at job setup",
|
|
31
|
+
"msg": "docker:// action images are pulled while the job is set up, before your login step, so the pull is anonymous.",
|
|
32
|
+
"fix": "Replace with a run: docker run step placed after docker/login-action, or reference the image on a mirror registry."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "cli-pull-before-login",
|
|
36
|
+
"sev": "error",
|
|
37
|
+
"kind": "any",
|
|
38
|
+
"title": "docker pull / run / create of a Docker Hub image with no Docker Hub login before it",
|
|
39
|
+
"msg": "No docker login (or docker/login-action for Docker Hub) runs earlier in this job, so this pull is anonymous.",
|
|
40
|
+
"fix": "Add docker/login-action (no registry: = Docker Hub) with a Docker Hub access token before this step."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "login-registry-not-hub",
|
|
44
|
+
"sev": "warn",
|
|
45
|
+
"kind": "workflow",
|
|
46
|
+
"title": "Login goes to another registry while the job pulls from Docker Hub",
|
|
47
|
+
"msg": "This login sets registry: to a non-Docker-Hub host; Docker Hub pulls in the job stay anonymous.",
|
|
48
|
+
"fix": "Add a second docker/login-action step without registry: for Docker Hub."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "self-hosted-shared-ip",
|
|
52
|
+
"sev": "warn",
|
|
53
|
+
"kind": "workflow",
|
|
54
|
+
"title": "Self-hosted runner: anonymous Docker Hub pulls share one NAT IP",
|
|
55
|
+
"msg": "Self-hosted runners behind one NAT or proxy share one public IP, so every anonymous Docker Hub pull from the fleet draws on one per-IP quota.",
|
|
56
|
+
"fix": "Log every runner in to Docker Hub or put a pull-through cache registry in front of the fleet."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "dockerfile-from-hub",
|
|
60
|
+
"sev": "warn",
|
|
61
|
+
"kind": "dockerfile",
|
|
62
|
+
"title": "FROM pulls a Docker Hub base image",
|
|
63
|
+
"msg": "This base image resolves to docker.io; the build pulls it anonymously unless the build host is logged in to Docker Hub.",
|
|
64
|
+
"fix": "Make sure the build step logs in to Docker Hub, or point FROM at a mirror registry."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "compose-image-hub",
|
|
68
|
+
"sev": "warn",
|
|
69
|
+
"kind": "compose",
|
|
70
|
+
"title": "Compose service image resolves to Docker Hub",
|
|
71
|
+
"msg": "docker compose pull / up fetches this from docker.io with the host's credentials — anonymous on a fresh CI runner.",
|
|
72
|
+
"fix": "Run docker login before compose in CI, or use a mirror registry for this image."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "k8s-image-hub-no-pull-secret",
|
|
76
|
+
"sev": "warn",
|
|
77
|
+
"kind": "k8s",
|
|
78
|
+
"title": "Kubernetes image from Docker Hub with no imagePullSecrets",
|
|
79
|
+
"msg": "Nodes pull this from docker.io anonymously; every node that starts the pod counts against its own IP's quota.",
|
|
80
|
+
"fix": "Add imagePullSecrets with a Docker Hub token, or use a pull-through cache registry."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "gitlab-image-hub-no-auth",
|
|
84
|
+
"sev": "warn",
|
|
85
|
+
"kind": "gitlab",
|
|
86
|
+
"title": "GitLab CI image from Docker Hub without DOCKER_AUTH_CONFIG or Dependency Proxy",
|
|
87
|
+
"msg": "The runner pulls this image before the script runs; with no DOCKER_AUTH_CONFIG and no Dependency Proxy prefix the pull is anonymous.",
|
|
88
|
+
"fix": "Prefix with ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ or set DOCKER_AUTH_CONFIG for the runner."
|
|
89
|
+
}
|
|
90
|
+
]
|
rulestamp_docker_hub_pull_limit_lint-1.0.2/src/rulestamp_docker_hub_pull_limit_lint/js/strings.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Docker Hub Pull Limit Lint",
|
|
3
|
+
"subtitle": "Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet (docs.docker.com, checked 2026-09-27).",
|
|
4
|
+
"bin": "docker-hub-pull-limit-lint",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Lint the open workflow, compose, GitLab CI, Kubernetes or Dockerfile and see every anonymous Docker Hub pull with line and fix, using all 10 rules.",
|
|
7
|
+
"paid": "Scan the whole workspace in one run and export a Markdown pull inventory report to attach to the CI ticket.",
|
|
8
|
+
"need_key": "This option needs a licence (Docker Hub Pull Limit Lint).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rulestamp-docker-hub-pull-limit-lint
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per (bundled Node.js runtime)
|
|
5
|
+
Author-email: Rulestamp <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/docker-hub-pull-limit-lint?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/docker-hub-pull-limit-lint?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: docker,docker hub,rate limit,toomanyrequests,github actions,docker-compose,ci,kubernetes
|
|
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
|
+
# Docker Hub Pull Limit Lint
|
|
22
|
+
|
|
23
|
+
> Runs the same checker as our npm package 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
|
+

|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Finds every anonymous Docker Hub pull in GitHub Actions, compose, GitLab CI, Kubernetes and Dockerfiles, including the service containers your login step never covers. Docker Hub limit: 100 pulls per 6 hours per IPv4 address or IPv6 /64 subnet (docs.docker.com, checked 2026-09-27).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
docker-hub-pull-limit-lint file
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Node 18+. The same 10 rules as the VS Code extension, from a terminal or CI.
|
|
38
|
+
|
|
39
|
+
## Free
|
|
40
|
+
|
|
41
|
+
- Lint the open workflow, compose, GitLab CI, Kubernetes or Dockerfile and see every anonymous Docker Hub pull with line and fix, using all 10 rules.
|
|
42
|
+
- `--rules` lists every rule
|
|
43
|
+
|
|
44
|
+
## With a licence ($29 once)
|
|
45
|
+
|
|
46
|
+
- Scan the whole workspace in one run and export a Markdown pull inventory report to attach to the CI ticket.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
docker-hub-pull-limit-lint --dir ./templates --report html --out report.html
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Docker Pro costs $9 per user per month billed yearly ($108 a year), and it only lifts the limit for pulls that log in; this lint costs $29 once.
|
|
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": { "docker-hub-pull-limit-lint": { "command": "docker-hub-pull-limit-lint", "args": ["--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: Docker Hub Pull Limit Lint
|
|
68
|
+
run: pip install rulestamp-docker-hub-pull-limit-lint && docker-hub-pull-limit-lint --dir . --ci
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
73
|
+
|
|
74
|
+
[Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_xEh5IeTeNYGk8do3WVOUYZxBSCx2BseojEiBQ38O9Ky)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
<!-- docker hub pull limit lint -->
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Install (PyPI)
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install rulestamp-docker-hub-pull-limit-lint
|
|
84
|
+
docker-hub-pull-limit-lint --help
|
|
85
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/rulestamp_docker_hub_pull_limit_lint/__init__.py
|
|
5
|
+
src/rulestamp_docker_hub_pull_limit_lint/__main__.py
|
|
6
|
+
src/rulestamp_docker_hub_pull_limit_lint.egg-info/PKG-INFO
|
|
7
|
+
src/rulestamp_docker_hub_pull_limit_lint.egg-info/SOURCES.txt
|
|
8
|
+
src/rulestamp_docker_hub_pull_limit_lint.egg-info/dependency_links.txt
|
|
9
|
+
src/rulestamp_docker_hub_pull_limit_lint.egg-info/entry_points.txt
|
|
10
|
+
src/rulestamp_docker_hub_pull_limit_lint.egg-info/requires.txt
|
|
11
|
+
src/rulestamp_docker_hub_pull_limit_lint.egg-info/top_level.txt
|
|
12
|
+
src/rulestamp_docker_hub_pull_limit_lint/js/cli.js
|
|
13
|
+
src/rulestamp_docker_hub_pull_limit_lint/js/engine.js
|
|
14
|
+
src/rulestamp_docker_hub_pull_limit_lint/js/license.js
|
|
15
|
+
src/rulestamp_docker_hub_pull_limit_lint/js/package.json
|
|
16
|
+
src/rulestamp_docker_hub_pull_limit_lint/js/rules.json
|
|
17
|
+
src/rulestamp_docker_hub_pull_limit_lint/js/strings.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs-wheel-binaries>=20
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rulestamp_docker_hub_pull_limit_lint
|