rulestamp-wp-plugin-review-gate 1.0.7__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_wp_plugin_review_gate-1.0.7/LICENSE.txt +15 -0
- rulestamp_wp_plugin_review_gate-1.0.7/PKG-INFO +83 -0
- rulestamp_wp_plugin_review_gate-1.0.7/README.md +63 -0
- rulestamp_wp_plugin_review_gate-1.0.7/pyproject.toml +29 -0
- rulestamp_wp_plugin_review_gate-1.0.7/setup.cfg +4 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/__init__.py +1 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/__main__.py +12 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/js/cli.js +161 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/js/engine.js +154 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/js/license.js +103 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/js/package.json +41 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/js/rules.json +149 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate/js/strings.json +10 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/PKG-INFO +83 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/SOURCES.txt +17 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/dependency_links.txt +1 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/entry_points.txt +2 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/requires.txt +1 -0
- rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
WP Plugin Review Gate — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Flags every review blocker in the readme.txt or plugin header file you have open, with the line number and the exact header line to write instead. 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
|
+
Audits the whole plugin folder in one pass - readme.txt against the plugin header across files, Stable tag against Version - and exports a dated submission report you keep. 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,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rulestamp-wp-plugin-review-gate
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit (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/wp-plugin-review-gate?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/wp-plugin-review-gate?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: wordpress,plugin,readme.txt,stable tag,plugin review,wordpress.org,linter
|
|
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
|
+
# WP Plugin Review Gate
|
|
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
|
+
The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
wp-plugin-review-gate file
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Node 18+. The same 21 rules as the VS Code extension, from a terminal or CI.
|
|
36
|
+
|
|
37
|
+
## Free
|
|
38
|
+
|
|
39
|
+
- Checks the readme.txt or main plugin PHP file you have open against 21 rules and prints every blocker with its line number and the exact replacement line. On the sample readme.txt shipped with it, 6 blockers: Stable tag: trunk, no Requires PHP, Tested up to: 6.8.2 carrying a patch number, Tested up to 6.8 older than Requires at least 6.9, a short description of 179 characters against the 150-character cut, and a missing License URI.
|
|
40
|
+
- `--rules` lists every rule
|
|
41
|
+
|
|
42
|
+
## With a licence ($29 once)
|
|
43
|
+
|
|
44
|
+
- Sweeps the whole plugin folder in one pass - readme.txt read against the plugin header, Stable tag against Version across files - and exports a dated submission report you keep. $29 once, one licence key per person or team seat.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
wp-plugin-review-gate --dir ./templates --report html --out report.html
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A freelance WordPress developer bills $30 to $90 an hour; a pre-submission read is an hour or two.
|
|
51
|
+
|
|
52
|
+
## Use from an AI agent (MCP)
|
|
53
|
+
|
|
54
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "mcpServers": { "wp-plugin-review-gate": { "command": "wp-plugin-review-gate", "args": ["--mcp"] } } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
61
|
+
|
|
62
|
+
## Use in CI
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- name: WP Plugin Review Gate
|
|
66
|
+
run: pip install rulestamp-wp-plugin-review-gate && wp-plugin-review-gate --dir . --ci
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
71
|
+
|
|
72
|
+
[Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_qArF3mNB1PwK3Pdmu8u0XW9Y9cxzGW2ytS3iq05dr5h)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
<!-- wp plugin review gate -->
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Install (PyPI)
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install rulestamp-wp-plugin-review-gate
|
|
82
|
+
wp-plugin-review-gate --help
|
|
83
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# WP Plugin Review Gate
|
|
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
|
+
The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
wp-plugin-review-gate file
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node 18+. The same 21 rules as the VS Code extension, from a terminal or CI.
|
|
16
|
+
|
|
17
|
+
## Free
|
|
18
|
+
|
|
19
|
+
- Checks the readme.txt or main plugin PHP file you have open against 21 rules and prints every blocker with its line number and the exact replacement line. On the sample readme.txt shipped with it, 6 blockers: Stable tag: trunk, no Requires PHP, Tested up to: 6.8.2 carrying a patch number, Tested up to 6.8 older than Requires at least 6.9, a short description of 179 characters against the 150-character cut, and a missing License URI.
|
|
20
|
+
- `--rules` lists every rule
|
|
21
|
+
|
|
22
|
+
## With a licence ($29 once)
|
|
23
|
+
|
|
24
|
+
- Sweeps the whole plugin folder in one pass - readme.txt read against the plugin header, Stable tag against Version across files - and exports a dated submission report you keep. $29 once, one licence key per person or team seat.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
wp-plugin-review-gate --dir ./templates --report html --out report.html
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A freelance WordPress developer bills $30 to $90 an hour; a pre-submission read is an hour or two.
|
|
31
|
+
|
|
32
|
+
## Use from an AI agent (MCP)
|
|
33
|
+
|
|
34
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "mcpServers": { "wp-plugin-review-gate": { "command": "wp-plugin-review-gate", "args": ["--mcp"] } } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
41
|
+
|
|
42
|
+
## Use in CI
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
- name: WP Plugin Review Gate
|
|
46
|
+
run: pip install rulestamp-wp-plugin-review-gate && wp-plugin-review-gate --dir . --ci
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
51
|
+
|
|
52
|
+
[Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_qArF3mNB1PwK3Pdmu8u0XW9Y9cxzGW2ytS3iq05dr5h)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
<!-- wp plugin review gate -->
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Install (PyPI)
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
pip install rulestamp-wp-plugin-review-gate
|
|
62
|
+
wp-plugin-review-gate --help
|
|
63
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rulestamp-wp-plugin-review-gate"
|
|
7
|
+
version = "1.0.7"
|
|
8
|
+
description = "The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit (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 = ["wordpress", "plugin", "readme.txt", "stable tag", "plugin review", "wordpress.org", "linter"]
|
|
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/wp-plugin-review-gate?ref=pypi"
|
|
19
|
+
Documentation = "https://getreadystack.com/tools/wp-plugin-review-gate?ref=pypi"
|
|
20
|
+
Support = "https://getreadystack.com/support"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
"wp-plugin-review-gate" = "rulestamp_wp_plugin_review_gate.__main__:main"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
"rulestamp_wp_plugin_review_gate" = ["js/*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.7'
|
|
@@ -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); });
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// WP Plugin Review Gate — one engine, used by the VS Code extension and by the free web page.
|
|
2
|
+
var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.WPRG_RULES;
|
|
3
|
+
|
|
4
|
+
var BY_ID = {};
|
|
5
|
+
for (var i = 0; i < RULES.length; i++) BY_ID[RULES[i].id] = RULES[i];
|
|
6
|
+
|
|
7
|
+
function lines(text) { return String(text == null ? '' : text).split(/\r\n|\r|\n/); }
|
|
8
|
+
|
|
9
|
+
function headerBlock(ls, isPhp) {
|
|
10
|
+
// readme.txt: the lines between the === title === line and the first blank line after it.
|
|
11
|
+
// plugin php: the lines of the first block comment that carries Plugin Name:.
|
|
12
|
+
var out = [], started = false;
|
|
13
|
+
for (var i = 0; i < ls.length && i < 80; i++) {
|
|
14
|
+
var l = ls[i];
|
|
15
|
+
if (isPhp) {
|
|
16
|
+
if (/^\s*\/\*/.test(l)) started = true;
|
|
17
|
+
if (started) out.push({ n: i + 1, t: l.replace(/^\s*\*+\s?/, '').replace(/^\s*\/\*+\s?/, '') });
|
|
18
|
+
if (started && /\*\//.test(l)) break;
|
|
19
|
+
} else {
|
|
20
|
+
if (/^\s*===\s*.+?\s*===\s*$/.test(l)) { started = true; continue; }
|
|
21
|
+
if (started) {
|
|
22
|
+
if (/^\s*$/.test(l)) break;
|
|
23
|
+
out.push({ n: i + 1, t: l });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function headerValue(block, key) {
|
|
31
|
+
var re = new RegExp('^\\s*' + key.replace(/ /g, '\\s+') + '\\s*:\\s*(.*)$', 'i');
|
|
32
|
+
for (var i = 0; i < block.length; i++) {
|
|
33
|
+
var m = re.exec(block[i].t);
|
|
34
|
+
if (m) return { value: m[1].trim(), line: block[i].n, raw: block[i].t.trim() };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ver(v) {
|
|
40
|
+
var m = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(v || '').trim());
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
return { major: +m[1], minor: m[2] ? +m[2] : 0, patch: m[3] === undefined ? null : +m[3] };
|
|
43
|
+
}
|
|
44
|
+
function cmp(a, b) { return (a.major - b.major) || (a.minor - b.minor); }
|
|
45
|
+
|
|
46
|
+
function add(out, id, msg, line) {
|
|
47
|
+
var r = BY_ID[id];
|
|
48
|
+
out.push({ check: id, sev: r ? r.sev : 'warn', msg: msg, line: line || 1 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function kindOf(text, ls) {
|
|
52
|
+
var head = ls.slice(0, 80).join('\n');
|
|
53
|
+
if (/^\s*===\s*.+?\s*===\s*$/m.test(head) || /^\s*Stable\s+tag\s*:/im.test(head)) return 'readme';
|
|
54
|
+
if (/Plugin\s+Name\s*:/i.test(head) && (/<\?php/.test(head) || /^\s*\/\*/m.test(head))) return 'php';
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkReadme(text, ls, out) {
|
|
59
|
+
var block = headerBlock(ls, false);
|
|
60
|
+
var firstLine = 1;
|
|
61
|
+
for (var i = 0; i < ls.length; i++) { if (ls[i].trim() !== '') { firstLine = i + 1; break; } }
|
|
62
|
+
if (!/^\s*===\s*.+?\s*===\s*$/.test(ls[firstLine - 1] || '')) {
|
|
63
|
+
add(out, 'readme_title_line', 'First line is "' + (ls[firstLine - 1] || '').trim().slice(0, 40) + '" — the directory reads the plugin name from === Your Plugin Name ===.', firstLine);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var stable = headerValue(block, 'Stable tag');
|
|
67
|
+
if (!stable) add(out, 'stable_tag_missing', 'No "Stable tag:" line — the directory has no version to ship, so the update never reaches installs. Write Stable tag: <the version you tagged>.', firstLine);
|
|
68
|
+
else if (/^trunk$/i.test(stable.value)) add(out, 'stable_tag_trunk', '"Stable tag: trunk" ships whatever sits in trunk to every install. Write the tagged version instead, e.g. Stable tag: 2.4.1.', stable.line);
|
|
69
|
+
|
|
70
|
+
var reqWp = headerValue(block, 'Requires at least');
|
|
71
|
+
if (!reqWp || !reqWp.value) add(out, 'requires_at_least_missing', 'No "Requires at least:" line — WordPress will offer your plugin to installs that cannot run it. Write Requires at least: 6.5.', firstLine);
|
|
72
|
+
|
|
73
|
+
var reqPhp = headerValue(block, 'Requires PHP');
|
|
74
|
+
if (!reqPhp || !reqPhp.value) add(out, 'requires_php_missing', 'No "Requires PHP:" line — every PHP version, including ones you never tested, is offered the plugin. Write Requires PHP: 7.4.', firstLine);
|
|
75
|
+
|
|
76
|
+
var tested = headerValue(block, 'Tested up to');
|
|
77
|
+
if (!tested || !tested.value) {
|
|
78
|
+
add(out, 'tested_up_to_missing', 'No "Tested up to:" line — the directory shows your plugin as untested. Write Tested up to: <the WordPress major you tested>.', firstLine);
|
|
79
|
+
} else {
|
|
80
|
+
var tv = ver(tested.value);
|
|
81
|
+
if (tv && tv.patch !== null) add(out, 'tested_up_to_patch', '"Tested up to: ' + tested.value + '" carries a patch number. The directory wants the major only: Tested up to: ' + tv.major + '.' + tv.minor + '.', tested.line);
|
|
82
|
+
if (tv && reqWp && reqWp.value) {
|
|
83
|
+
var rv = ver(reqWp.value);
|
|
84
|
+
if (rv && cmp(tv, rv) < 0) add(out, 'tested_below_requires', 'Tested up to ' + tv.major + '.' + tv.minor + ' is older than Requires at least ' + rv.major + '.' + rv.minor + ' — the two lines contradict each other and no install matches both.', tested.line);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var tags = headerValue(block, 'Tags');
|
|
89
|
+
if (tags && tags.value) {
|
|
90
|
+
var list = tags.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
91
|
+
if (list.length > 5) add(out, 'tags_over_five', list.length + ' tags listed; only the first five are used, so "' + list.slice(5).join(', ') + '" does nothing.', tags.line);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var lic = headerValue(block, 'License');
|
|
95
|
+
var licUri = headerValue(block, 'License URI');
|
|
96
|
+
if (!lic || !licUri) {
|
|
97
|
+
var which = !lic && !licUri ? 'License and License URI are' : (!lic ? 'License is' : 'License URI is');
|
|
98
|
+
add(out, 'license_line_missing', which + ' missing from the header block. Add License: GPLv2 or later and License URI: https://www.gnu.org/licenses/gpl-2.0.html.', firstLine);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// short description = first non-empty line after the header block
|
|
102
|
+
var end = block.length ? block[block.length - 1].n : firstLine;
|
|
103
|
+
for (var j = end; j < ls.length; j++) {
|
|
104
|
+
var s = ls[j];
|
|
105
|
+
if (s.trim() === '') continue;
|
|
106
|
+
if (/^\s*==/.test(s)) break;
|
|
107
|
+
if (s.trim().length > 150) add(out, 'short_description_long', 'Short description is ' + s.trim().length + ' characters; the directory cuts it at 150.', j + 1);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!/^\s*==\s*Changelog\s*==/im.test(text)) add(out, 'changelog_section_missing', 'No "== Changelog ==" section — reviewers and users read it to see what this version changed.', firstLine);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function checkPhp(text, ls, out) {
|
|
115
|
+
var block = headerBlock(ls, true);
|
|
116
|
+
var firstLine = block.length ? block[0].n : 1;
|
|
117
|
+
|
|
118
|
+
if (!headerValue(block, 'Version')) add(out, 'php_version_header_missing', 'The plugin header has no "Version:" line — WordPress cannot tell an update from the installed copy.', firstLine);
|
|
119
|
+
if (!headerValue(block, 'Requires PHP')) add(out, 'php_requires_php_header_missing', 'The plugin header has no "Requires PHP:" line — add Requires PHP: 7.4 so old installs are not offered the update.', firstLine);
|
|
120
|
+
if (!headerValue(block, 'License')) add(out, 'php_license_header_missing', 'The plugin header has no "License:" line — the directory only accepts GPL-compatible plugins. Add License: GPLv2 or later.', firstLine);
|
|
121
|
+
if (!headerValue(block, 'Text Domain')) add(out, 'php_text_domain_missing', 'The plugin header has no "Text Domain:" line — translations will not load. It must equal your plugin slug.', firstLine);
|
|
122
|
+
|
|
123
|
+
if (!/defined\s*\(\s*['"]ABSPATH['"]\s*\)/.test(text)) {
|
|
124
|
+
add(out, 'php_direct_access_guard', 'No ABSPATH guard in this file — the file runs when it is requested directly. Add defined( \'ABSPATH\' ) || exit; under the header.', firstLine);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (var i = 0; i < ls.length; i++) {
|
|
128
|
+
var l = ls[i];
|
|
129
|
+
if (/wp_enqueue_(script|style)\s*\(/.test(l) && /https?:\/\/(?!localhost)/.test(l)) {
|
|
130
|
+
add(out, 'php_remote_asset', 'Line loads an asset from a remote URL: ' + l.trim().slice(0, 70) + ' — plugins in the directory must ship their own files.', i + 1);
|
|
131
|
+
}
|
|
132
|
+
var ob = /\b(eval|base64_decode|gzinflate|str_rot13)\s*\(/.exec(l);
|
|
133
|
+
if (ob) add(out, 'php_obfuscated_code', ob[1] + '() on this line reads as obfuscated code to a reviewer; ship readable PHP.', i + 1);
|
|
134
|
+
if (/['"][^'"]*wp-content\/plugins[^'"]*['"]/.test(l)) {
|
|
135
|
+
add(out, 'php_hardcoded_plugin_path', 'Hard-coded wp-content/plugins path — it breaks on every install that moved the plugins folder. Use plugin_dir_path().', i + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function check(text, opts) {
|
|
141
|
+
opts = opts || {};
|
|
142
|
+
var ls = lines(text);
|
|
143
|
+
var out = [];
|
|
144
|
+
var kind = kindOf(text, ls);
|
|
145
|
+
if (kind === 'readme') checkReadme(text, ls, out);
|
|
146
|
+
else if (kind === 'php') checkPhp(text, ls, out);
|
|
147
|
+
else add(out, 'file_kind', 'This is neither a readme.txt (first line === Plugin Name ===) nor a main plugin file (header comment with Plugin Name:). Open one of those two files.', 1);
|
|
148
|
+
out.sort(function (a, b) { return a.line - b.line; });
|
|
149
|
+
return { findings: out, kind: kind, today: opts.today || '', rule_count: RULES.length };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
|
|
153
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = API;
|
|
154
|
+
if (typeof window !== 'undefined') window.WPRGENGINE = API;
|
|
@@ -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 = 'bbd4a519-b762-483f-8c0b-46325093c344'; // 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_qArF3mNB1PwK3Pdmu8u0XW9Y9cxzGW2ytS3iq05dr5h';
|
|
7
|
+
const SLUG = 'wp-plugin-review-gate';
|
|
8
|
+
const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
|
|
9
|
+
const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
|
|
10
|
+
function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
|
|
11
|
+
function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
|
|
12
|
+
function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
|
|
13
|
+
const ALL_BENEFIT_ID = '22692551-5203-4467-b1a3-e33cdba6589d'; // s149 2026-09-17 — 팀 키(전 린터 한 키 · Polar benefit) · 상품 benefit 다음에 한 번 더 묻는다
|
|
14
|
+
// s170 2026-09-29 — ask OUR worker first (/api/lic): keys bought on Whop open here (a price-tier key binds to the first tool it opens · the team key opens every tool). Then the old Polar check.
|
|
15
|
+
function validate(key) {
|
|
16
|
+
return validateHub(key).then(function (h) {
|
|
17
|
+
if (h.ok) return h;
|
|
18
|
+
return validate1(key, BENEFIT_ID).then(function (r) { return (r.ok || r.offline || !/^[0-9a-f-]{36}$/.test(ALL_BENEFIT_ID)) ? r : validate1(key, ALL_BENEFIT_ID); })
|
|
19
|
+
.then(function (r) { return r.ok ? r : { ok: false, offline: !!(h.offline || r.offline) }; });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function validateHub(key) {
|
|
23
|
+
return new Promise(function (resolve) {
|
|
24
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/lic?key=' + encodeURIComponent(key) + '&slug=' + encodeURIComponent(SLUG),
|
|
25
|
+
method: 'GET', timeout: 8000, headers: { 'accept': 'application/json', 'user-agent': 'readystack-cli/' + SLUG } }, function (res) {
|
|
26
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
27
|
+
res.on('end', function () {
|
|
28
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: res.statusCode >= 500 });
|
|
29
|
+
try { const j = JSON.parse(buf); resolve({ ok: !!(j && j.ok), offline: false }); } catch (e) { resolve({ ok: false, offline: false }); }
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
33
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
34
|
+
req.end();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function validate1(key, ben) {
|
|
38
|
+
return new Promise(function (resolve) {
|
|
39
|
+
if (!ORG_ID) return resolve({ ok: false, offline: false });
|
|
40
|
+
const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(ben) ? { key: key, organization_id: ORG_ID, benefit_id: ben } : { key: key, organization_id: ORG_ID });
|
|
41
|
+
const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
|
|
42
|
+
headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
|
|
43
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
44
|
+
res.on('end', function () {
|
|
45
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
|
|
46
|
+
try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
|
|
47
|
+
catch (e) { resolve({ ok: false, offline: false }); }
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
51
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
52
|
+
req.write(body); req.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// ★s151 2026-09-19 — 키 판(키가 없거나 거절된 순간)을 익명으로 센다 (슬러그·출처·이유만) · DO_NOT_TRACK=1 · READYSTACK_NO_TELEMETRY 면 안 보낸다 · 실패는 조용히 · 프로세스당 한 번.
|
|
56
|
+
let _pinged = false;
|
|
57
|
+
function pingPaywall(why) {
|
|
58
|
+
try {
|
|
59
|
+
if (_pinged) return; _pinged = true;
|
|
60
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return; // s151: CI(깃허브 액션 등)와 우리 빌드는 손님이 아니다
|
|
61
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY || process.env.READYSTACK_MCP === '1')) return; // s152: 사람 자리(터미널·MCP 세션)에서만 센다 - 발행 1분 뒤 남의 실행기(JP · 우리 3대는 US)가 돌린 no_key 13건은 손님이 아니다
|
|
62
|
+
const body = JSON.stringify({ t: 'paywall', slug: SLUG, src: process.env.READYSTACK_MCP === '1' ? 'mcp' : 'cli', why: why || 'no_key' });
|
|
63
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
64
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
|
|
65
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
66
|
+
req.write(body); req.end();
|
|
67
|
+
} catch (e) { /* 세는 것이 실패해도 상품은 돈다 */ }
|
|
68
|
+
}
|
|
69
|
+
async function ensure(explicitKey) {
|
|
70
|
+
const st = load();
|
|
71
|
+
const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
|
|
72
|
+
if (!key) { pingPaywall('no_key'); return { ok: false, why: 'no_key' }; } // s151
|
|
73
|
+
const age = Date.now() - (st.okAt || 0);
|
|
74
|
+
if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
|
|
75
|
+
const r = await validate(String(key).trim());
|
|
76
|
+
if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
|
|
77
|
+
if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
|
|
78
|
+
if (!r.offline) pingPaywall('invalid'); // s151
|
|
79
|
+
return { ok: false, why: r.offline ? 'offline' : 'invalid' };
|
|
80
|
+
}
|
|
81
|
+
// ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
|
|
82
|
+
const FEED_URL = 'https://getreadystack.com/api/rules/';
|
|
83
|
+
function pullFeed() {
|
|
84
|
+
const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
|
|
85
|
+
if (!key) return Promise.resolve(cached);
|
|
86
|
+
if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
|
|
87
|
+
return new Promise(function (resolve) {
|
|
88
|
+
let req;
|
|
89
|
+
try {
|
|
90
|
+
req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
|
|
91
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
92
|
+
res.on('end', function () {
|
|
93
|
+
if (res.statusCode !== 200) return resolve(cached);
|
|
94
|
+
try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
|
|
95
|
+
catch (e) { resolve(cached); }
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
} catch (e) { return resolve(cached); }
|
|
99
|
+
req.on('timeout', function () { req.destroy(); resolve(cached); });
|
|
100
|
+
req.on('error', function () { resolve(cached); });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
module.exports = { ensure, BUY_URL, storePath, pullFeed };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/wp-plugin-review-gate",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"wordpress",
|
|
11
|
+
"plugin",
|
|
12
|
+
"readme.txt",
|
|
13
|
+
"stable tag",
|
|
14
|
+
"plugin review",
|
|
15
|
+
"wordpress.org",
|
|
16
|
+
"linter"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://getreadystack.com",
|
|
19
|
+
"funding": "https://getreadystack.com/api/buy/cl/polar_cl_qArF3mNB1PwK3Pdmu8u0XW9Y9cxzGW2ytS3iq05dr5h",
|
|
20
|
+
"bin": {
|
|
21
|
+
"wp-plugin-review-gate": "cli.js"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"mcpName": "io.github.jmshinhwa/wp-plugin-review-gate",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
30
|
+
"directory": "wp-plugin-review-gate"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"cli.js",
|
|
34
|
+
"license.js",
|
|
35
|
+
"rules.json",
|
|
36
|
+
"strings.json",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE.txt",
|
|
39
|
+
"engine.js"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "file_kind",
|
|
4
|
+
"scope": "any",
|
|
5
|
+
"sev": "info",
|
|
6
|
+
"title": "File not recognised",
|
|
7
|
+
"fix": "Open a readme.txt (starts with === Plugin Name ===) or the main plugin PHP file (header comment with Plugin Name:)."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "readme_title_line",
|
|
11
|
+
"scope": "readme",
|
|
12
|
+
"sev": "error",
|
|
13
|
+
"title": "readme.txt does not start with === Plugin Name ===",
|
|
14
|
+
"fix": "Make the first non-empty line === Your Plugin Name ==="
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "stable_tag_missing",
|
|
18
|
+
"scope": "readme",
|
|
19
|
+
"sev": "error",
|
|
20
|
+
"title": "No Stable tag line",
|
|
21
|
+
"fix": "Stable tag: 1.0.0"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "stable_tag_trunk",
|
|
25
|
+
"scope": "readme",
|
|
26
|
+
"sev": "error",
|
|
27
|
+
"title": "Stable tag: trunk ships trunk to every install",
|
|
28
|
+
"fix": "Stable tag: <the version you tagged>"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "requires_at_least_missing",
|
|
32
|
+
"scope": "readme",
|
|
33
|
+
"sev": "error",
|
|
34
|
+
"title": "No Requires at least line",
|
|
35
|
+
"fix": "Requires at least: 6.5"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "requires_php_missing",
|
|
39
|
+
"scope": "readme",
|
|
40
|
+
"sev": "error",
|
|
41
|
+
"title": "No Requires PHP line",
|
|
42
|
+
"fix": "Requires PHP: 7.4"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "tested_up_to_missing",
|
|
46
|
+
"scope": "readme",
|
|
47
|
+
"sev": "error",
|
|
48
|
+
"title": "No Tested up to line",
|
|
49
|
+
"fix": "Tested up to: 6.8"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "tested_up_to_patch",
|
|
53
|
+
"scope": "readme",
|
|
54
|
+
"sev": "error",
|
|
55
|
+
"title": "Tested up to carries a patch number",
|
|
56
|
+
"fix": "Tested up to: <major.minor only>"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "tested_below_requires",
|
|
60
|
+
"scope": "readme",
|
|
61
|
+
"sev": "error",
|
|
62
|
+
"title": "Tested up to is older than Requires at least",
|
|
63
|
+
"fix": "Raise Tested up to, or lower Requires at least"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "short_description_long",
|
|
67
|
+
"scope": "readme",
|
|
68
|
+
"sev": "error",
|
|
69
|
+
"title": "Short description is over 150 characters",
|
|
70
|
+
"fix": "Cut the line under the header block to 150 characters"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "tags_over_five",
|
|
74
|
+
"scope": "readme",
|
|
75
|
+
"sev": "warn",
|
|
76
|
+
"title": "More than five tags; only the first five are used",
|
|
77
|
+
"fix": "Keep five tags, the five you want to rank for"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "license_line_missing",
|
|
81
|
+
"scope": "readme",
|
|
82
|
+
"sev": "error",
|
|
83
|
+
"title": "License or License URI missing",
|
|
84
|
+
"fix": "License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "changelog_section_missing",
|
|
88
|
+
"scope": "readme",
|
|
89
|
+
"sev": "warn",
|
|
90
|
+
"title": "No == Changelog == section",
|
|
91
|
+
"fix": "Add == Changelog == with an entry for this version"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "php_version_header_missing",
|
|
95
|
+
"scope": "php",
|
|
96
|
+
"sev": "error",
|
|
97
|
+
"title": "Plugin header has no Version",
|
|
98
|
+
"fix": "Version: 1.0.0 in the header comment"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "php_requires_php_header_missing",
|
|
102
|
+
"scope": "php",
|
|
103
|
+
"sev": "error",
|
|
104
|
+
"title": "Plugin header has no Requires PHP",
|
|
105
|
+
"fix": "Requires PHP: 7.4 in the header comment"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "php_license_header_missing",
|
|
109
|
+
"scope": "php",
|
|
110
|
+
"sev": "error",
|
|
111
|
+
"title": "Plugin header has no License line",
|
|
112
|
+
"fix": "License: GPLv2 or later in the header comment"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "php_text_domain_missing",
|
|
116
|
+
"scope": "php",
|
|
117
|
+
"sev": "warn",
|
|
118
|
+
"title": "Plugin header has no Text Domain",
|
|
119
|
+
"fix": "Text Domain: <your plugin slug>"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "php_direct_access_guard",
|
|
123
|
+
"scope": "php",
|
|
124
|
+
"sev": "error",
|
|
125
|
+
"title": "No direct-access guard",
|
|
126
|
+
"fix": "defined( 'ABSPATH' ) || exit; under the header"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "php_remote_asset",
|
|
130
|
+
"scope": "php",
|
|
131
|
+
"sev": "error",
|
|
132
|
+
"title": "Script or style enqueued from a remote URL",
|
|
133
|
+
"fix": "Ship the file inside the plugin and enqueue it with plugins_url()"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "php_obfuscated_code",
|
|
137
|
+
"scope": "php",
|
|
138
|
+
"sev": "error",
|
|
139
|
+
"title": "eval, base64_decode or gzinflate in plugin code",
|
|
140
|
+
"fix": "Ship readable PHP; the review team reads every line"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "php_hardcoded_plugin_path",
|
|
144
|
+
"scope": "php",
|
|
145
|
+
"sev": "warn",
|
|
146
|
+
"title": "Hard-coded wp-content/plugins path",
|
|
147
|
+
"fix": "plugin_dir_path() instead of the literal path"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "WP Plugin Review Gate",
|
|
3
|
+
"subtitle": "The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit",
|
|
4
|
+
"bin": "wp-plugin-review-gate",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Checks the readme.txt or main plugin PHP file you have open against 21 rules and prints every blocker with its line number and the exact replacement line. On the sample readme.txt shipped with it, 6 blockers: Stable tag: trunk, no Requires PHP, Tested up to: 6.8.2 carrying a patch number, Tested up to 6.8 older than Requires at least 6.9, a short description of 179 characters against the 150-character cut, and a missing License URI.",
|
|
7
|
+
"paid": "Sweeps the whole plugin folder in one pass - readme.txt read against the plugin header, Stable tag against Version across files - and exports a dated submission report you keep. $29 once, one licence key per person or team seat.",
|
|
8
|
+
"need_key": "This option needs a licence (WP Plugin Review Gate).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rulestamp-wp-plugin-review-gate
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit (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/wp-plugin-review-gate?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/wp-plugin-review-gate?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: wordpress,plugin,readme.txt,stable tag,plugin review,wordpress.org,linter
|
|
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
|
+
# WP Plugin Review Gate
|
|
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
|
+
The readme.txt and plugin-header lines that send a wordpress.org submission back, found before you submit
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
wp-plugin-review-gate file
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Node 18+. The same 21 rules as the VS Code extension, from a terminal or CI.
|
|
36
|
+
|
|
37
|
+
## Free
|
|
38
|
+
|
|
39
|
+
- Checks the readme.txt or main plugin PHP file you have open against 21 rules and prints every blocker with its line number and the exact replacement line. On the sample readme.txt shipped with it, 6 blockers: Stable tag: trunk, no Requires PHP, Tested up to: 6.8.2 carrying a patch number, Tested up to 6.8 older than Requires at least 6.9, a short description of 179 characters against the 150-character cut, and a missing License URI.
|
|
40
|
+
- `--rules` lists every rule
|
|
41
|
+
|
|
42
|
+
## With a licence ($29 once)
|
|
43
|
+
|
|
44
|
+
- Sweeps the whole plugin folder in one pass - readme.txt read against the plugin header, Stable tag against Version across files - and exports a dated submission report you keep. $29 once, one licence key per person or team seat.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
wp-plugin-review-gate --dir ./templates --report html --out report.html
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A freelance WordPress developer bills $30 to $90 an hour; a pre-submission read is an hour or two.
|
|
51
|
+
|
|
52
|
+
## Use from an AI agent (MCP)
|
|
53
|
+
|
|
54
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "mcpServers": { "wp-plugin-review-gate": { "command": "wp-plugin-review-gate", "args": ["--mcp"] } } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
61
|
+
|
|
62
|
+
## Use in CI
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- name: WP Plugin Review Gate
|
|
66
|
+
run: pip install rulestamp-wp-plugin-review-gate && wp-plugin-review-gate --dir . --ci
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
71
|
+
|
|
72
|
+
[Get a licence](https://getreadystack.com/api/buy/cl/polar_cl_qArF3mNB1PwK3Pdmu8u0XW9Y9cxzGW2ytS3iq05dr5h)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
<!-- wp plugin review gate -->
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Install (PyPI)
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install rulestamp-wp-plugin-review-gate
|
|
82
|
+
wp-plugin-review-gate --help
|
|
83
|
+
```
|
rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/rulestamp_wp_plugin_review_gate/__init__.py
|
|
5
|
+
src/rulestamp_wp_plugin_review_gate/__main__.py
|
|
6
|
+
src/rulestamp_wp_plugin_review_gate.egg-info/PKG-INFO
|
|
7
|
+
src/rulestamp_wp_plugin_review_gate.egg-info/SOURCES.txt
|
|
8
|
+
src/rulestamp_wp_plugin_review_gate.egg-info/dependency_links.txt
|
|
9
|
+
src/rulestamp_wp_plugin_review_gate.egg-info/entry_points.txt
|
|
10
|
+
src/rulestamp_wp_plugin_review_gate.egg-info/requires.txt
|
|
11
|
+
src/rulestamp_wp_plugin_review_gate.egg-info/top_level.txt
|
|
12
|
+
src/rulestamp_wp_plugin_review_gate/js/cli.js
|
|
13
|
+
src/rulestamp_wp_plugin_review_gate/js/engine.js
|
|
14
|
+
src/rulestamp_wp_plugin_review_gate/js/license.js
|
|
15
|
+
src/rulestamp_wp_plugin_review_gate/js/package.json
|
|
16
|
+
src/rulestamp_wp_plugin_review_gate/js/rules.json
|
|
17
|
+
src/rulestamp_wp_plugin_review_gate/js/strings.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/requires.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs-wheel-binaries>=20
|
rulestamp_wp_plugin_review_gate-1.0.7/src/rulestamp_wp_plugin_review_gate.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rulestamp_wp_plugin_review_gate
|