toolfunnel 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -231
- package/SECURITY.md +84 -0
- package/bin/toolfunnel.js +451 -372
- package/docs/KNOWN_BUGS.md +80 -99
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +84 -70
- package/docs/packaging.md +115 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +39 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -147
- package/mcp/servers/mock-upstream/server.js +380 -350
- package/package.json +3 -1
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +353 -344
- package/src/core/child-reaper.js +61 -0
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +329 -275
- package/src/core/hook-loader.js +824 -791
- package/src/core/hook-runner.js +566 -346
- package/src/core/logger.js +297 -171
- package/src/core/matcher.js +100 -51
- package/src/core/metrics.js +75 -75
- package/src/core/rename-migration.js +163 -0
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -98
- package/src/extend/add-hook.md +152 -152
- package/src/extend/add-mcp.md +119 -122
- package/src/extend/configure.md +88 -87
- package/src/extend/create-tool.md +168 -168
- package/src/extend/howto.js +102 -102
- package/src/extend/package.md +190 -187
- package/src/extend/wrap.md +99 -99
- package/src/mcp/aggregator.js +1719 -1246
- package/src/mcp/expose-store.js +577 -460
- package/src/mcp/gated-run.js +210 -201
- package/src/mcp/http-transport.js +1563 -1311
- package/src/mcp/mcp-client.js +1070 -907
- package/src/mcp/modern.js +406 -372
- package/src/mcp/protocol.js +468 -468
- package/src/mcp/server.js +3112 -2688
- package/src/tools/registry.js +661 -513
- package/src/tools/tool-state.js +170 -170
- package/src/ui/public/app.js +1187 -1174
- package/src/ui/public/index.html +286 -286
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1616 -1348
- package/tools/scripts/base64.js +139 -139
- package/tools/scripts/danger.js +61 -61
- package/tools/scripts/echo.js +39 -39
- package/tools/scripts/hash.js +101 -101
- package/tools/scripts/json.js +109 -109
- package/tools/scripts/text-stats.js +131 -131
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +120 -112
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +117 -117
- package/tools/scripts/tf-mcp-set.js +121 -121
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -134
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -103
- package/tools/scripts/uuid.js +102 -102
- package/tools/tools.register.json +227 -227
- package/src/core/tool-registry.js +0 -180
- package/src/packages/packager.js +0 -715
- package/src/tools/drift.js +0 -138
package/tools/scripts/hash.js
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* hash-tool.js - a safe, dependency-free local cryptographic-digest tool.
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* Hash UTF-8 text with a chosen algorithm (sha256 default; sha1 / md5 also
|
|
10
|
-
* offered) and return the lowercase hex digest. Useful from the Tool Manager
|
|
11
|
-
* for checksums, fingerprinting, and quick integrity checks. Uses ONLY the
|
|
12
|
-
* Node built-in `node:crypto` - no network, no filesystem, no new deps.
|
|
13
|
-
*
|
|
14
|
-
* Contract with the register (the register's `defaultRunScript`)
|
|
15
|
-
* ----------------------------------------------------------
|
|
16
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
17
|
-
* - Args: { algo?: "sha256" | "sha1" | "md5", text: <string> }.
|
|
18
|
-
* `algo` defaults to "sha256" when omitted.
|
|
19
|
-
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
-
* on bad input (reported as { ok:false, error }), never a thrown exception.
|
|
21
|
-
*
|
|
22
|
-
* Output (stdout), exactly one JSON object:
|
|
23
|
-
* success: { ok:true, algo, hexdigest }
|
|
24
|
-
* failure: { ok:false, error }
|
|
25
|
-
*
|
|
26
|
-
* Safety invariants (mirror the isolation rule):
|
|
27
|
-
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
28
|
-
* - Only an explicit allow-list of algorithms is accepted, so the tool can
|
|
29
|
-
* never be coaxed into an unexpected / unavailable OpenSSL algorithm name.
|
|
30
|
-
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const crypto = require('node:crypto');
|
|
34
|
-
|
|
35
|
-
/** Algorithms this tool will accept (the names the task specifies). */
|
|
36
|
-
const ALLOWED_ALGOS = new Set(['sha256', 'sha1', 'md5']);
|
|
37
|
-
const DEFAULT_ALGO = 'sha256';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
41
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
42
|
-
*/
|
|
43
|
-
function parseStructuredArgs() {
|
|
44
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
45
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
46
|
-
return { value: null };
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
return { value: JSON.parse(raw) };
|
|
50
|
-
} catch (_err) {
|
|
51
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
57
|
-
*
|
|
58
|
-
* @param {any} args
|
|
59
|
-
* @returns {object} the JSON-serialisable result
|
|
60
|
-
*/
|
|
61
|
-
function run(args) {
|
|
62
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
63
|
-
return { ok: false, error: 'args must be an object { algo?, text }' };
|
|
64
|
-
}
|
|
65
|
-
const { text } = args;
|
|
66
|
-
const algo = args.algo === undefined || args.algo === null ? DEFAULT_ALGO : args.algo;
|
|
67
|
-
if (typeof algo !== 'string' || !ALLOWED_ALGOS.has(algo)) {
|
|
68
|
-
return { ok: false, error: `algo must be one of ${[...ALLOWED_ALGOS].join(', ')}` };
|
|
69
|
-
}
|
|
70
|
-
if (typeof text !== 'string') {
|
|
71
|
-
return { ok: false, error: 'text must be a string' };
|
|
72
|
-
}
|
|
73
|
-
// createHash can throw only for an unavailable algorithm; the allow-list above
|
|
74
|
-
// covers the universally-available ones, but wrap defensively just in case a
|
|
75
|
-
// hardened OpenSSL build (e.g. FIPS) disables md5/sha1.
|
|
76
|
-
let hexdigest;
|
|
77
|
-
try {
|
|
78
|
-
hexdigest = crypto.createHash(algo).update(text, 'utf8').digest('hex');
|
|
79
|
-
} catch (err) {
|
|
80
|
-
return { ok: false, error: `hash failed for algo "${algo}": ${(err && err.message) || String(err)}` };
|
|
81
|
-
}
|
|
82
|
-
return { ok: true, algo, hexdigest };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function main() {
|
|
86
|
-
const parsed = parseStructuredArgs();
|
|
87
|
-
let payload;
|
|
88
|
-
if (parsed.parseError) {
|
|
89
|
-
payload = { ok: false, error: parsed.parseError };
|
|
90
|
-
} else {
|
|
91
|
-
try {
|
|
92
|
-
payload = run(parsed.value);
|
|
93
|
-
} catch (err) {
|
|
94
|
-
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
98
|
-
process.exit(0);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* hash-tool.js - a safe, dependency-free local cryptographic-digest tool.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* Hash UTF-8 text with a chosen algorithm (sha256 default; sha1 / md5 also
|
|
10
|
+
* offered) and return the lowercase hex digest. Useful from the Tool Manager
|
|
11
|
+
* for checksums, fingerprinting, and quick integrity checks. Uses ONLY the
|
|
12
|
+
* Node built-in `node:crypto` - no network, no filesystem, no new deps.
|
|
13
|
+
*
|
|
14
|
+
* Contract with the register (the register's `defaultRunScript`)
|
|
15
|
+
* ----------------------------------------------------------
|
|
16
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
17
|
+
* - Args: { algo?: "sha256" | "sha1" | "md5", text: <string> }.
|
|
18
|
+
* `algo` defaults to "sha256" when omitted.
|
|
19
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
+
* on bad input (reported as { ok:false, error }), never a thrown exception.
|
|
21
|
+
*
|
|
22
|
+
* Output (stdout), exactly one JSON object:
|
|
23
|
+
* success: { ok:true, algo, hexdigest }
|
|
24
|
+
* failure: { ok:false, error }
|
|
25
|
+
*
|
|
26
|
+
* Safety invariants (mirror the isolation rule):
|
|
27
|
+
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
28
|
+
* - Only an explicit allow-list of algorithms is accepted, so the tool can
|
|
29
|
+
* never be coaxed into an unexpected / unavailable OpenSSL algorithm name.
|
|
30
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const crypto = require('node:crypto');
|
|
34
|
+
|
|
35
|
+
/** Algorithms this tool will accept (the names the task specifies). */
|
|
36
|
+
const ALLOWED_ALGOS = new Set(['sha256', 'sha1', 'md5']);
|
|
37
|
+
const DEFAULT_ALGO = 'sha256';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
41
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
42
|
+
*/
|
|
43
|
+
function parseStructuredArgs() {
|
|
44
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
45
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
46
|
+
return { value: null };
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return { value: JSON.parse(raw) };
|
|
50
|
+
} catch (_err) {
|
|
51
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
57
|
+
*
|
|
58
|
+
* @param {any} args
|
|
59
|
+
* @returns {object} the JSON-serialisable result
|
|
60
|
+
*/
|
|
61
|
+
function run(args) {
|
|
62
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
63
|
+
return { ok: false, error: 'args must be an object { algo?, text }' };
|
|
64
|
+
}
|
|
65
|
+
const { text } = args;
|
|
66
|
+
const algo = args.algo === undefined || args.algo === null ? DEFAULT_ALGO : args.algo;
|
|
67
|
+
if (typeof algo !== 'string' || !ALLOWED_ALGOS.has(algo)) {
|
|
68
|
+
return { ok: false, error: `algo must be one of ${[...ALLOWED_ALGOS].join(', ')}` };
|
|
69
|
+
}
|
|
70
|
+
if (typeof text !== 'string') {
|
|
71
|
+
return { ok: false, error: 'text must be a string' };
|
|
72
|
+
}
|
|
73
|
+
// createHash can throw only for an unavailable algorithm; the allow-list above
|
|
74
|
+
// covers the universally-available ones, but wrap defensively just in case a
|
|
75
|
+
// hardened OpenSSL build (e.g. FIPS) disables md5/sha1.
|
|
76
|
+
let hexdigest;
|
|
77
|
+
try {
|
|
78
|
+
hexdigest = crypto.createHash(algo).update(text, 'utf8').digest('hex');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return { ok: false, error: `hash failed for algo "${algo}": ${(err && err.message) || String(err)}` };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, algo, hexdigest };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function main() {
|
|
86
|
+
const parsed = parseStructuredArgs();
|
|
87
|
+
let payload;
|
|
88
|
+
if (parsed.parseError) {
|
|
89
|
+
payload = { ok: false, error: parsed.parseError };
|
|
90
|
+
} else {
|
|
91
|
+
try {
|
|
92
|
+
payload = run(parsed.value);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
main();
|
package/tools/scripts/json.js
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* json-tool.js - a safe, dependency-free local JSON formatter / validator.
|
|
6
|
-
*
|
|
7
|
-
* Purpose
|
|
8
|
-
* -------
|
|
9
|
-
* Validate a JSON string and either pretty-print it (default) or minify it.
|
|
10
|
-
* Useful from the Tool Manager for tidying up JSON blobs or confirming a string
|
|
11
|
-
* is valid JSON. Pure JSON.parse / JSON.stringify - no network, no filesystem.
|
|
12
|
-
*
|
|
13
|
-
* Contract with the register (the register's `defaultRunScript`)
|
|
14
|
-
* ----------------------------------------------------------
|
|
15
|
-
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
16
|
-
* - Args: { text: <string>, indent?: <integer 0..10>, minify?: <boolean> }.
|
|
17
|
-
* indent defaults to 2 (used for the pretty form).
|
|
18
|
-
* minify true -> return the compact (no-whitespace) form instead.
|
|
19
|
-
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
-
* for invalid JSON input (reported as { ok:false, error }).
|
|
21
|
-
*
|
|
22
|
-
* Output (stdout), exactly one JSON object:
|
|
23
|
-
* pretty: { ok:true, pretty: "<indented json>" } (minify falsey)
|
|
24
|
-
* minify: { ok:true, minified: "<compact json>" } (minify true)
|
|
25
|
-
* failure: { ok:false, error } (invalid JSON / bad args)
|
|
26
|
-
*
|
|
27
|
-
* Safety invariants (mirror the isolation rule):
|
|
28
|
-
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
29
|
-
* - indent is clamped to a sane [0,10] range so a huge indent cannot blow up.
|
|
30
|
-
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const MAX_INDENT = 10;
|
|
34
|
-
const DEFAULT_INDENT = 2;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
38
|
-
* @returns {{ value: any } | { parseError: string }}
|
|
39
|
-
*/
|
|
40
|
-
function parseStructuredArgs() {
|
|
41
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
42
|
-
if (raw === undefined || raw === null || raw === '') {
|
|
43
|
-
return { value: null };
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
return { value: JSON.parse(raw) };
|
|
47
|
-
} catch (_err) {
|
|
48
|
-
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
54
|
-
*
|
|
55
|
-
* @param {any} args
|
|
56
|
-
* @returns {object} the JSON-serialisable result
|
|
57
|
-
*/
|
|
58
|
-
function run(args) {
|
|
59
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
60
|
-
return { ok: false, error: 'args must be an object { text, indent?, minify? }' };
|
|
61
|
-
}
|
|
62
|
-
const { text } = args;
|
|
63
|
-
if (typeof text !== 'string') {
|
|
64
|
-
return { ok: false, error: 'text must be a string' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Validate indent (only meaningful for the pretty path, but validate eagerly).
|
|
68
|
-
let indent = DEFAULT_INDENT;
|
|
69
|
-
if (args.indent !== undefined && args.indent !== null) {
|
|
70
|
-
if (typeof args.indent !== 'number' || !Number.isInteger(args.indent)) {
|
|
71
|
-
return { ok: false, error: 'indent must be an integer' };
|
|
72
|
-
}
|
|
73
|
-
if (args.indent < 0 || args.indent > MAX_INDENT) {
|
|
74
|
-
return { ok: false, error: `indent must be between 0 and ${MAX_INDENT}` };
|
|
75
|
-
}
|
|
76
|
-
indent = args.indent;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Parse the supplied JSON text - THIS is the validation step.
|
|
80
|
-
let parsed;
|
|
81
|
-
try {
|
|
82
|
-
parsed = JSON.parse(text);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
return { ok: false, error: `invalid JSON: ${(err && err.message) || String(err)}` };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (args.minify === true) {
|
|
88
|
-
return { ok: true, minified: JSON.stringify(parsed) };
|
|
89
|
-
}
|
|
90
|
-
return { ok: true, pretty: JSON.stringify(parsed, null, indent) };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function main() {
|
|
94
|
-
const parsed = parseStructuredArgs();
|
|
95
|
-
let payload;
|
|
96
|
-
if (parsed.parseError) {
|
|
97
|
-
payload = { ok: false, error: parsed.parseError };
|
|
98
|
-
} else {
|
|
99
|
-
try {
|
|
100
|
-
payload = run(parsed.value);
|
|
101
|
-
} catch (err) {
|
|
102
|
-
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
106
|
-
process.exit(0);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* json-tool.js - a safe, dependency-free local JSON formatter / validator.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* Validate a JSON string and either pretty-print it (default) or minify it.
|
|
10
|
+
* Useful from the Tool Manager for tidying up JSON blobs or confirming a string
|
|
11
|
+
* is valid JSON. Pure JSON.parse / JSON.stringify - no network, no filesystem.
|
|
12
|
+
*
|
|
13
|
+
* Contract with the register (the register's `defaultRunScript`)
|
|
14
|
+
* ----------------------------------------------------------
|
|
15
|
+
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
|
|
16
|
+
* - Args: { text: <string>, indent?: <integer 0..10>, minify?: <boolean> }.
|
|
17
|
+
* indent defaults to 2 (used for the pretty form).
|
|
18
|
+
* minify true -> return the compact (no-whitespace) form instead.
|
|
19
|
+
* - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
|
|
20
|
+
* for invalid JSON input (reported as { ok:false, error }).
|
|
21
|
+
*
|
|
22
|
+
* Output (stdout), exactly one JSON object:
|
|
23
|
+
* pretty: { ok:true, pretty: "<indented json>" } (minify falsey)
|
|
24
|
+
* minify: { ok:true, minified: "<compact json>" } (minify true)
|
|
25
|
+
* failure: { ok:false, error } (invalid JSON / bad args)
|
|
26
|
+
*
|
|
27
|
+
* Safety invariants (mirror the isolation rule):
|
|
28
|
+
* - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
|
|
29
|
+
* - indent is clamped to a sane [0,10] range so a huge indent cannot blow up.
|
|
30
|
+
* - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const MAX_INDENT = 10;
|
|
34
|
+
const DEFAULT_INDENT = 2;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
|
|
38
|
+
* @returns {{ value: any } | { parseError: string }}
|
|
39
|
+
*/
|
|
40
|
+
function parseStructuredArgs() {
|
|
41
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
42
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
43
|
+
return { value: null };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return { value: JSON.parse(raw) };
|
|
47
|
+
} catch (_err) {
|
|
48
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure core: given the parsed args, produce the result object. Never throws.
|
|
54
|
+
*
|
|
55
|
+
* @param {any} args
|
|
56
|
+
* @returns {object} the JSON-serialisable result
|
|
57
|
+
*/
|
|
58
|
+
function run(args) {
|
|
59
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
60
|
+
return { ok: false, error: 'args must be an object { text, indent?, minify? }' };
|
|
61
|
+
}
|
|
62
|
+
const { text } = args;
|
|
63
|
+
if (typeof text !== 'string') {
|
|
64
|
+
return { ok: false, error: 'text must be a string' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate indent (only meaningful for the pretty path, but validate eagerly).
|
|
68
|
+
let indent = DEFAULT_INDENT;
|
|
69
|
+
if (args.indent !== undefined && args.indent !== null) {
|
|
70
|
+
if (typeof args.indent !== 'number' || !Number.isInteger(args.indent)) {
|
|
71
|
+
return { ok: false, error: 'indent must be an integer' };
|
|
72
|
+
}
|
|
73
|
+
if (args.indent < 0 || args.indent > MAX_INDENT) {
|
|
74
|
+
return { ok: false, error: `indent must be between 0 and ${MAX_INDENT}` };
|
|
75
|
+
}
|
|
76
|
+
indent = args.indent;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Parse the supplied JSON text - THIS is the validation step.
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(text);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return { ok: false, error: `invalid JSON: ${(err && err.message) || String(err)}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (args.minify === true) {
|
|
88
|
+
return { ok: true, minified: JSON.stringify(parsed) };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, pretty: JSON.stringify(parsed, null, indent) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function main() {
|
|
94
|
+
const parsed = parseStructuredArgs();
|
|
95
|
+
let payload;
|
|
96
|
+
if (parsed.parseError) {
|
|
97
|
+
payload = { ok: false, error: parsed.parseError };
|
|
98
|
+
} else {
|
|
99
|
+
try {
|
|
100
|
+
payload = run(parsed.value);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|