@crediolabs/policy-builder-cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -2
- package/dist/bin/policy-builder.js +42 -8
- package/dist/src/commands/record.js +1 -1
- package/dist/src/commands/synthesize.js +194 -7
- package/dist/src/output.d.ts +8 -1
- package/dist/src/output.js +24 -5
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/src/commands/record.d.ts +3 -0
- package/dist-cjs/src/commands/record.js +45 -0
- package/dist-cjs/src/commands/synthesize.d.ts +3 -0
- package/dist-cjs/src/commands/synthesize.js +253 -0
- package/dist-cjs/src/index.d.ts +3 -0
- package/dist-cjs/src/index.js +12 -0
- package/dist-cjs/src/output.d.ts +56 -0
- package/dist-cjs/src/output.js +166 -0
- package/package.json +36 -4
- package/src/commands/record.ts +1 -1
- package/src/commands/synthesize.ts +220 -7
- package/src/output.ts +22 -5
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// apps/policy-builder-cli/src/commands/synthesize.ts
|
|
3
|
+
//
|
|
4
|
+
// `policy-builder synthesize` subcommand. Dispatches to ONE of the two
|
|
5
|
+
// front-ends (mandate / recording) based on which file flag is supplied:
|
|
6
|
+
// --mandate <path.json> -> synthesizeFromMandate
|
|
7
|
+
// --recorded-tx <path.json> -> synthesizeFromRecording
|
|
8
|
+
//
|
|
9
|
+
// The CLI mirrors the MCP tool's discriminated union: one subcommand, two
|
|
10
|
+
// front-ends, mutually exclusive.
|
|
11
|
+
//
|
|
12
|
+
// Per-field response flags (--window-seconds, --valid-until, --limit-amount,
|
|
13
|
+
// --invocation-limit) merge into `userResponses`. A flag overrides the same
|
|
14
|
+
// field from --responses (CLI flags are explicit; the file is a default bag).
|
|
15
|
+
// Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
|
|
16
|
+
// the interpreter opt-in and are rejected without --smart-account; tighten-only
|
|
17
|
+
// bounds are validated by the core.
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.runSynthesizeCommand = runSynthesizeCommand;
|
|
20
|
+
const run_1 = require("@crediolabs/policy-synth/run");
|
|
21
|
+
const output_ts_1 = require("../output.js");
|
|
22
|
+
// Positive-int flags and i128 amount strings share the same wire shape: a
|
|
23
|
+
// base-10 unsigned decimal, no sign. The i128 stays a string at the boundary
|
|
24
|
+
// because it is wider than Number.MAX_SAFE_INTEGER.
|
|
25
|
+
const POSITIVE_INT_RE = /^[0-9]+$/;
|
|
26
|
+
async function runSynthesizeCommand(argv, flags) {
|
|
27
|
+
const pairs = (0, output_ts_1.parsePairs)(argv);
|
|
28
|
+
const hasMandate = Boolean(pairs.mandate);
|
|
29
|
+
const hasRecorded = Boolean(pairs['recorded-tx']);
|
|
30
|
+
if (hasMandate === hasRecorded) {
|
|
31
|
+
throw new output_ts_1.CliError({
|
|
32
|
+
code: 'CLI_MISSING_ARG',
|
|
33
|
+
message: 'synthesize: exactly one of --mandate <path> or --recorded-tx <path> is required',
|
|
34
|
+
severity: 'error',
|
|
35
|
+
retryable: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (hasMandate) {
|
|
39
|
+
const mandate = (0, output_ts_1.readJsonFile)(pairs.mandate);
|
|
40
|
+
const args = { source: 'mandate', mandate };
|
|
41
|
+
if (pairs['oz-config'] !== undefined) {
|
|
42
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
43
|
+
}
|
|
44
|
+
if (pairs.confidence !== undefined) {
|
|
45
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
46
|
+
}
|
|
47
|
+
const res = await (0, run_1.runSynthesizePolicy)(args);
|
|
48
|
+
return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(mandate)');
|
|
49
|
+
}
|
|
50
|
+
// hasRecorded
|
|
51
|
+
const recordedFile = (0, output_ts_1.readJsonFile)(pairs['recorded-tx']);
|
|
52
|
+
// Accept either a bare RecordedTransaction or the `{ ok, data }` artifact that
|
|
53
|
+
// `record --out` writes (same shape as `--json`), so `record --out X` followed
|
|
54
|
+
// by `synthesize --recorded-tx X` works end to end.
|
|
55
|
+
const recordedTx = recordedFile?.ok === true && typeof recordedFile.data === 'object' && recordedFile.data !== null
|
|
56
|
+
? recordedFile.data
|
|
57
|
+
: recordedFile;
|
|
58
|
+
const network = pairs.network;
|
|
59
|
+
if (!network) {
|
|
60
|
+
throw new output_ts_1.CliError({
|
|
61
|
+
code: 'CLI_MISSING_ARG',
|
|
62
|
+
message: 'synthesize: --network <mainnet|testnet> is required with --recorded-tx',
|
|
63
|
+
severity: 'error',
|
|
64
|
+
retryable: false,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const args = { source: 'recording', recordedTx, network };
|
|
68
|
+
// userResponses precedence: --responses file is the base; per-field flags
|
|
69
|
+
// override the same field. Only the override'd fields are merged in.
|
|
70
|
+
const userResponses = {};
|
|
71
|
+
if (pairs.responses) {
|
|
72
|
+
const file = (0, output_ts_1.readJsonFile)(pairs.responses);
|
|
73
|
+
if (file !== null && typeof file === 'object' && !Array.isArray(file)) {
|
|
74
|
+
Object.assign(userResponses, file);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
throw new output_ts_1.CliError({
|
|
78
|
+
code: 'CLI_INVALID_JSON',
|
|
79
|
+
message: `synthesize: --responses ${pairs.responses} must be a JSON object`,
|
|
80
|
+
severity: 'error',
|
|
81
|
+
retryable: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (pairs['window-seconds'] !== undefined) {
|
|
86
|
+
userResponses.windowSeconds = parsePositiveInt(pairs['window-seconds'], '--window-seconds');
|
|
87
|
+
}
|
|
88
|
+
if (pairs['valid-until'] !== undefined) {
|
|
89
|
+
userResponses.validUntilLedger = parsePositiveInt(pairs['valid-until'], '--valid-until');
|
|
90
|
+
}
|
|
91
|
+
if (pairs['limit-amount'] !== undefined) {
|
|
92
|
+
userResponses.limitAmount = parseI128String(pairs['limit-amount'], '--limit-amount');
|
|
93
|
+
}
|
|
94
|
+
if (pairs['invocation-limit'] !== undefined) {
|
|
95
|
+
userResponses.invocationLimit = parsePositiveInt(pairs['invocation-limit'], '--invocation-limit');
|
|
96
|
+
}
|
|
97
|
+
if (Object.keys(userResponses).length > 0) {
|
|
98
|
+
args.userResponses = userResponses;
|
|
99
|
+
}
|
|
100
|
+
if (pairs['oz-config'] !== undefined) {
|
|
101
|
+
args.ozConfig = readOzConfigFile(pairs['oz-config']);
|
|
102
|
+
}
|
|
103
|
+
if (pairs.confidence !== undefined) {
|
|
104
|
+
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
105
|
+
}
|
|
106
|
+
// --smart-account <C...> opts into the interpreter adapter, so constraints OZ
|
|
107
|
+
// cannot express (per-method scoping, invocation-count windows, oracle bounds,
|
|
108
|
+
// exact hop paths) lower to a real predicate document instead of just warnings.
|
|
109
|
+
// The core validates the address and installNonce; a bad value surfaces there.
|
|
110
|
+
//
|
|
111
|
+
// Use `!== undefined` (not truthy) so `--smart-account ""` and `--install-nonce`
|
|
112
|
+
// without `--smart-account` are rejected up front instead of being silently
|
|
113
|
+
// dropped. The foot-gun: an empty value previously produced an "ok" envelope
|
|
114
|
+
// with 0 policyDocuments, so callers thought the constraint had been enforced
|
|
115
|
+
// when in fact it had been silently skipped.
|
|
116
|
+
const smartAccountRaw = pairs['smart-account'];
|
|
117
|
+
const installNonceRaw = pairs['install-nonce'];
|
|
118
|
+
const oracleStalenessRaw = pairs['oracle-max-staleness'];
|
|
119
|
+
const oracleDeviationRaw = pairs['oracle-max-deviation'];
|
|
120
|
+
if (installNonceRaw !== undefined && smartAccountRaw === undefined) {
|
|
121
|
+
throw new output_ts_1.CliError({
|
|
122
|
+
code: 'CLI_MISSING_ARG',
|
|
123
|
+
message: 'synthesize: --install-nonce requires --smart-account <C...> (interpreter opt-in)',
|
|
124
|
+
severity: 'error',
|
|
125
|
+
retryable: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Oracle params are an interpreter-only knob; reject up front so they cannot
|
|
129
|
+
// be silently dropped when --smart-account is absent.
|
|
130
|
+
if ((oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) &&
|
|
131
|
+
smartAccountRaw === undefined) {
|
|
132
|
+
throw new output_ts_1.CliError({
|
|
133
|
+
code: 'CLI_MISSING_ARG',
|
|
134
|
+
message: 'synthesize: --oracle-max-staleness / --oracle-max-deviation require --smart-account <C...> (interpreter opt-in)',
|
|
135
|
+
severity: 'error',
|
|
136
|
+
retryable: false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (smartAccountRaw !== undefined) {
|
|
140
|
+
const smartAccount = smartAccountRaw.trim();
|
|
141
|
+
if (smartAccount.length === 0) {
|
|
142
|
+
throw new output_ts_1.CliError({
|
|
143
|
+
code: 'CLI_MISSING_ARG',
|
|
144
|
+
message: 'synthesize: --smart-account <C...> was passed empty; provide a 56-character contract strkey or omit the flag',
|
|
145
|
+
severity: 'error',
|
|
146
|
+
retryable: false,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!/^C[2-7A-Z]{55}$/.test(smartAccount)) {
|
|
150
|
+
throw new output_ts_1.CliError({
|
|
151
|
+
code: 'CLI_MISSING_ARG',
|
|
152
|
+
message: `synthesize: --smart-account "${smartAccount}" is not a valid C... contract strkey (expected 56 chars starting with C)`,
|
|
153
|
+
severity: 'error',
|
|
154
|
+
retryable: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const interpreter = { smartAccountAddress: smartAccount };
|
|
158
|
+
if (installNonceRaw !== undefined) {
|
|
159
|
+
const nonce = Number(installNonceRaw);
|
|
160
|
+
if (!Number.isInteger(nonce) || nonce < 0) {
|
|
161
|
+
throw new output_ts_1.CliError({
|
|
162
|
+
code: 'CLI_MISSING_ARG',
|
|
163
|
+
message: `synthesize: --install-nonce "${installNonceRaw}" is not a non-negative integer`,
|
|
164
|
+
severity: 'error',
|
|
165
|
+
retryable: false,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
interpreter.installNonce = nonce;
|
|
169
|
+
}
|
|
170
|
+
// Oracle params only attach when at least one bound was provided. The
|
|
171
|
+
// core validates tighten-only (maxStalenessSeconds <= 600,
|
|
172
|
+
// maxDeviationBps <= 200) - a too-loose value surfaces as SYNTHESIS_ERROR.
|
|
173
|
+
if (oracleStalenessRaw !== undefined || oracleDeviationRaw !== undefined) {
|
|
174
|
+
const oracleParams = {};
|
|
175
|
+
if (oracleStalenessRaw !== undefined) {
|
|
176
|
+
oracleParams.maxStalenessSeconds = parsePositiveInt(oracleStalenessRaw, '--oracle-max-staleness');
|
|
177
|
+
}
|
|
178
|
+
if (oracleDeviationRaw !== undefined) {
|
|
179
|
+
oracleParams.maxDeviationBps = parsePositiveInt(oracleDeviationRaw, '--oracle-max-deviation');
|
|
180
|
+
}
|
|
181
|
+
interpreter.oracleParams = oracleParams;
|
|
182
|
+
}
|
|
183
|
+
args.interpreter = interpreter;
|
|
184
|
+
}
|
|
185
|
+
const res = await (0, run_1.runSynthesizePolicy)(args);
|
|
186
|
+
return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(recording)');
|
|
187
|
+
}
|
|
188
|
+
/** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
|
|
189
|
+
* CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
|
|
190
|
+
* on `ozConfig` catches shape mismatches downstream. */
|
|
191
|
+
function readOzConfigFile(path) {
|
|
192
|
+
const value = (0, output_ts_1.readJsonFile)(path);
|
|
193
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
194
|
+
throw new output_ts_1.CliError({
|
|
195
|
+
code: 'CLI_INVALID_JSON',
|
|
196
|
+
message: `synthesize: --oz-config ${path} must be a JSON object`,
|
|
197
|
+
severity: 'error',
|
|
198
|
+
retryable: false,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
/** Parse and validate `--confidence <n>` as a finite number in [0, 1]. A
|
|
204
|
+
* threshold above 1 would disable the recorder gate; reject it up front. */
|
|
205
|
+
function parseConfidence(raw) {
|
|
206
|
+
const n = Number(raw);
|
|
207
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
208
|
+
throw new output_ts_1.CliError({
|
|
209
|
+
code: 'CLI_MISSING_ARG',
|
|
210
|
+
message: `synthesize: --confidence "${raw}" must be a finite number within [0, 1]`,
|
|
211
|
+
severity: 'error',
|
|
212
|
+
retryable: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return n;
|
|
216
|
+
}
|
|
217
|
+
/** Parse a strictly positive integer (windowSeconds, validUntilLedger,
|
|
218
|
+
* invocationLimit, oracleParams bounds). The core re-validates these with
|
|
219
|
+
* field-specific caps; the CLI just enforces "looks like an integer > 0". */
|
|
220
|
+
function parsePositiveInt(raw, flagName) {
|
|
221
|
+
if (!POSITIVE_INT_RE.test(raw)) {
|
|
222
|
+
throw new output_ts_1.CliError({
|
|
223
|
+
code: 'CLI_MISSING_ARG',
|
|
224
|
+
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
225
|
+
severity: 'error',
|
|
226
|
+
retryable: false,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const n = Number(raw);
|
|
230
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
231
|
+
throw new output_ts_1.CliError({
|
|
232
|
+
code: 'CLI_MISSING_ARG',
|
|
233
|
+
message: `synthesize: ${flagName} "${raw}" must be a positive integer`,
|
|
234
|
+
severity: 'error',
|
|
235
|
+
retryable: false,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return n;
|
|
239
|
+
}
|
|
240
|
+
/** Parse an i128 decimal string (positive, base 10). The synth gate, not the
|
|
241
|
+
* CLI, decides what to do with negatives - real recordings carry positive
|
|
242
|
+
* amounts on the wire for `limitAmount`. */
|
|
243
|
+
function parseI128String(raw, flagName) {
|
|
244
|
+
if (!POSITIVE_INT_RE.test(raw)) {
|
|
245
|
+
throw new output_ts_1.CliError({
|
|
246
|
+
code: 'CLI_MISSING_ARG',
|
|
247
|
+
message: `synthesize: ${flagName} "${raw}" must be a positive decimal integer string (base-10 i128)`,
|
|
248
|
+
severity: 'error',
|
|
249
|
+
retryable: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return raw;
|
|
253
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// apps/policy-builder-cli/src/index.ts - public re-exports for the CLI package.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.writeJsonFile = exports.readJsonFile = exports.formatToolResponse = exports.runSynthesizeCommand = exports.runRecordCommand = void 0;
|
|
5
|
+
var record_ts_1 = require("./commands/record.js");
|
|
6
|
+
Object.defineProperty(exports, "runRecordCommand", { enumerable: true, get: function () { return record_ts_1.runRecordCommand; } });
|
|
7
|
+
var synthesize_ts_1 = require("./commands/synthesize.js");
|
|
8
|
+
Object.defineProperty(exports, "runSynthesizeCommand", { enumerable: true, get: function () { return synthesize_ts_1.runSynthesizeCommand; } });
|
|
9
|
+
var output_ts_1 = require("./output.js");
|
|
10
|
+
Object.defineProperty(exports, "formatToolResponse", { enumerable: true, get: function () { return output_ts_1.formatToolResponse; } });
|
|
11
|
+
Object.defineProperty(exports, "readJsonFile", { enumerable: true, get: function () { return output_ts_1.readJsonFile; } });
|
|
12
|
+
Object.defineProperty(exports, "writeJsonFile", { enumerable: true, get: function () { return output_ts_1.writeJsonFile; } });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ErrorCode, ToolError, ToolResponse } from '@crediolabs/policy-synth';
|
|
2
|
+
/** CLI flags (the only ones shipped in T1). */
|
|
3
|
+
export interface CliFlags {
|
|
4
|
+
json: boolean;
|
|
5
|
+
quiet: boolean;
|
|
6
|
+
out: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** Parse the argv tail for the known flags. Unrecognised flags are ignored
|
|
9
|
+
* (the caller enforces per-subcommand required flags separately). */
|
|
10
|
+
export declare function parseFlags(argv: ReadonlyArray<string>): CliFlags;
|
|
11
|
+
/** Resolve `--value <v>` style pairs after the subcommand name. Returns
|
|
12
|
+
* an object keyed by the option name (without `--`). Throws on missing
|
|
13
|
+
* value or duplicate keys.
|
|
14
|
+
*
|
|
15
|
+
* Note: an empty value (`--smart-account ""`) IS captured as an empty
|
|
16
|
+
* string so the caller can distinguish "flag omitted" from "flag passed
|
|
17
|
+
* empty" - a foot-gun: silently dropping empty values caused callers to
|
|
18
|
+
* believe the interpreter adapter was engaged when it was not. The next
|
|
19
|
+
* token is treated as a value iff it is present and does not start with
|
|
20
|
+
* `--`; tokens starting with `--` are never consumed as values. */
|
|
21
|
+
export declare function parsePairs(argv: ReadonlyArray<string>): Record<string, string>;
|
|
22
|
+
/** Read a JSON file, parse it, and return the value. Throws with a CLI-
|
|
23
|
+
* friendly error if the file is missing or malformed. */
|
|
24
|
+
export declare function readJsonFile(path: string): unknown;
|
|
25
|
+
/** Write a JSON-serialisable value to disk. Pretty-prints by default so the
|
|
26
|
+
* artefact is human-readable; CI scripts that need compact JSON can pipe
|
|
27
|
+
* through `jq` instead. */
|
|
28
|
+
export declare function writeJsonFile(path: string, value: unknown): void;
|
|
29
|
+
/** Wraps a core ToolResponse for the CLI:
|
|
30
|
+
* - always prints something useful (JSON or a short summary)
|
|
31
|
+
* - exits non-zero on ToolError so CI scripts can gate on `$?`
|
|
32
|
+
* - writes the `{ ok, data }` envelope to --out when present (matches
|
|
33
|
+
* the --json stdout shape so CI scripts get a single canonical payload)
|
|
34
|
+
*
|
|
35
|
+
* Throws CliError so the router can map it to a process exit code + a
|
|
36
|
+
* structured JSON envelope under --json. */
|
|
37
|
+
export declare function formatToolResponse<T>(res: ToolResponse<T>, flags: CliFlags, outLabel?: string): T;
|
|
38
|
+
/** CLI-specific error codes, distinct from the core's ErrorCode union. They
|
|
39
|
+
* cover failures that arise around the core call (bad argv, missing or
|
|
40
|
+
* malformed input files) and never collide with a core code. */
|
|
41
|
+
export type CliErrorCode = 'CLI_MISSING_ARG' | 'CLI_FILE_NOT_FOUND' | 'CLI_INVALID_JSON' | 'CLI_INTERNAL';
|
|
42
|
+
/** A ToolError whose `code` may be a core ErrorCode OR a CLI-local code. The
|
|
43
|
+
* CLI wraps both core failures and its own argv / IO failures in this shape;
|
|
44
|
+
* a core ToolError is assignable here since ErrorCode is a subset. */
|
|
45
|
+
export type CliToolError = Omit<ToolError, 'code'> & {
|
|
46
|
+
code: ErrorCode | CliErrorCode;
|
|
47
|
+
};
|
|
48
|
+
/** CLI-local error class wrapping a (core or CLI) ToolError so the router can
|
|
49
|
+
* map it to a non-zero exit. The error is preserved verbatim for --json. */
|
|
50
|
+
export declare class CliError extends Error {
|
|
51
|
+
readonly toolError: CliToolError;
|
|
52
|
+
constructor(err: CliToolError);
|
|
53
|
+
}
|
|
54
|
+
/** Pretty-print a CliError to stderr and exit non-zero. Used by the router
|
|
55
|
+
* when the catch fires. */
|
|
56
|
+
export declare function emitCliError(e: unknown, flags: CliFlags): never;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// apps/policy-builder-cli/src/output.ts
|
|
3
|
+
//
|
|
4
|
+
// Output helpers for the CLI: formatToolResponse for the `--json` flag and
|
|
5
|
+
// file I/O for `--out`. The CLI is intentionally tiny - no commander / yargs
|
|
6
|
+
// dependency; the router is a hand-rolled argv parser.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.CliError = void 0;
|
|
9
|
+
exports.parseFlags = parseFlags;
|
|
10
|
+
exports.parsePairs = parsePairs;
|
|
11
|
+
exports.readJsonFile = readJsonFile;
|
|
12
|
+
exports.writeJsonFile = writeJsonFile;
|
|
13
|
+
exports.formatToolResponse = formatToolResponse;
|
|
14
|
+
exports.emitCliError = emitCliError;
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
/** Parse the argv tail for the known flags. Unrecognised flags are ignored
|
|
18
|
+
* (the caller enforces per-subcommand required flags separately). */
|
|
19
|
+
function parseFlags(argv) {
|
|
20
|
+
let json = false;
|
|
21
|
+
let quiet = false;
|
|
22
|
+
let out = null;
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === '--json')
|
|
26
|
+
json = true;
|
|
27
|
+
else if (a === '--quiet')
|
|
28
|
+
quiet = true;
|
|
29
|
+
else if (a === '--out' && argv[i + 1]) {
|
|
30
|
+
out = argv[i + 1];
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
else if (a?.startsWith('--out=')) {
|
|
34
|
+
out = a.slice('--out='.length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { json, quiet, out };
|
|
38
|
+
}
|
|
39
|
+
/** Resolve `--value <v>` style pairs after the subcommand name. Returns
|
|
40
|
+
* an object keyed by the option name (without `--`). Throws on missing
|
|
41
|
+
* value or duplicate keys.
|
|
42
|
+
*
|
|
43
|
+
* Note: an empty value (`--smart-account ""`) IS captured as an empty
|
|
44
|
+
* string so the caller can distinguish "flag omitted" from "flag passed
|
|
45
|
+
* empty" - a foot-gun: silently dropping empty values caused callers to
|
|
46
|
+
* believe the interpreter adapter was engaged when it was not. The next
|
|
47
|
+
* token is treated as a value iff it is present and does not start with
|
|
48
|
+
* `--`; tokens starting with `--` are never consumed as values. */
|
|
49
|
+
function parsePairs(argv) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const a = argv[i];
|
|
53
|
+
if (a?.startsWith('--') && a.includes('=')) {
|
|
54
|
+
const eq = a.indexOf('=');
|
|
55
|
+
const key = a.slice(2, eq);
|
|
56
|
+
const val = a.slice(eq + 1);
|
|
57
|
+
if (key && val !== undefined)
|
|
58
|
+
out[key] = val;
|
|
59
|
+
}
|
|
60
|
+
else if (a?.startsWith('--')) {
|
|
61
|
+
const key = a.slice(2);
|
|
62
|
+
if (!key)
|
|
63
|
+
continue;
|
|
64
|
+
const next = argv[i + 1];
|
|
65
|
+
// Only consume the next token if it is present AND does not look like
|
|
66
|
+
// another flag. Empty strings DO count as values so callers can
|
|
67
|
+
// distinguish "omitted" from "passed empty".
|
|
68
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
69
|
+
out[key] = next;
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Standalone flag (no value) - record as empty string so `!== undefined`
|
|
74
|
+
// checks upstream can detect presence.
|
|
75
|
+
out[key] = '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/** Read a JSON file, parse it, and return the value. Throws with a CLI-
|
|
82
|
+
* friendly error if the file is missing or malformed. */
|
|
83
|
+
function readJsonFile(path) {
|
|
84
|
+
const abs = (0, node_path_1.resolve)(path);
|
|
85
|
+
if (!(0, node_fs_1.existsSync)(abs)) {
|
|
86
|
+
throw new CliError({
|
|
87
|
+
code: 'CLI_FILE_NOT_FOUND',
|
|
88
|
+
message: `file not found: ${path}`,
|
|
89
|
+
severity: 'error',
|
|
90
|
+
retryable: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const raw = (0, node_fs_1.readFileSync)(abs, 'utf8');
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
throw new CliError({
|
|
99
|
+
code: 'CLI_INVALID_JSON',
|
|
100
|
+
message: `invalid JSON in ${path}: ${e.message}`,
|
|
101
|
+
severity: 'error',
|
|
102
|
+
retryable: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Write a JSON-serialisable value to disk. Pretty-prints by default so the
|
|
107
|
+
* artefact is human-readable; CI scripts that need compact JSON can pipe
|
|
108
|
+
* through `jq` instead. */
|
|
109
|
+
function writeJsonFile(path, value) {
|
|
110
|
+
const abs = (0, node_path_1.resolve)(path);
|
|
111
|
+
(0, node_fs_1.writeFileSync)(abs, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
112
|
+
}
|
|
113
|
+
/** Wraps a core ToolResponse for the CLI:
|
|
114
|
+
* - always prints something useful (JSON or a short summary)
|
|
115
|
+
* - exits non-zero on ToolError so CI scripts can gate on `$?`
|
|
116
|
+
* - writes the `{ ok, data }` envelope to --out when present (matches
|
|
117
|
+
* the --json stdout shape so CI scripts get a single canonical payload)
|
|
118
|
+
*
|
|
119
|
+
* Throws CliError so the router can map it to a process exit code + a
|
|
120
|
+
* structured JSON envelope under --json. */
|
|
121
|
+
function formatToolResponse(res, flags, outLabel = 'result') {
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
const envelope = { ok: true, data: res.data };
|
|
124
|
+
if (flags.out)
|
|
125
|
+
writeJsonFile(flags.out, envelope);
|
|
126
|
+
if (flags.json) {
|
|
127
|
+
// newline-terminated JSON so it pipes cleanly
|
|
128
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
129
|
+
}
|
|
130
|
+
else if (!flags.quiet) {
|
|
131
|
+
process.stdout.write(`${outLabel}: ok\n`);
|
|
132
|
+
}
|
|
133
|
+
return res.data;
|
|
134
|
+
}
|
|
135
|
+
throw new CliError(res.error);
|
|
136
|
+
}
|
|
137
|
+
/** CLI-local error class wrapping a (core or CLI) ToolError so the router can
|
|
138
|
+
* map it to a non-zero exit. The error is preserved verbatim for --json. */
|
|
139
|
+
class CliError extends Error {
|
|
140
|
+
toolError;
|
|
141
|
+
constructor(err) {
|
|
142
|
+
super(err.message);
|
|
143
|
+
this.toolError = err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.CliError = CliError;
|
|
147
|
+
/** Pretty-print a CliError to stderr and exit non-zero. Used by the router
|
|
148
|
+
* when the catch fires. */
|
|
149
|
+
function emitCliError(e, flags) {
|
|
150
|
+
if (e instanceof CliError) {
|
|
151
|
+
if (flags.json) {
|
|
152
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: e.toolError })}\n`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
process.stderr.write(`error: ${e.toolError.code} - ${e.toolError.message}\n`);
|
|
156
|
+
}
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
if (flags.json) {
|
|
160
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: { code: 'CLI_INTERNAL', message: e.message, severity: 'fatal', retryable: false } })}\n`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
process.stderr.write(`internal error: ${e.message}\n`);
|
|
164
|
+
}
|
|
165
|
+
process.exit(2);
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-builder-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "CLI wrapper around the OZ policy-synth core (record + synthesize) for solo-dev and CI workflows.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/src/index.js",
|
|
8
8
|
"types": "./dist/src/index.d.ts",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22.12",
|
|
11
|
+
"bun": ">=1.3.0"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/untangledfinance/oz-policy-builder.git",
|
|
16
|
+
"directory": "apps/policy-builder-cli"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/untangledfinance/oz-policy-builder#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/untangledfinance/oz-policy-builder/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"stellar",
|
|
24
|
+
"soroban",
|
|
25
|
+
"openzeppelin",
|
|
26
|
+
"policy",
|
|
27
|
+
"smart-account",
|
|
28
|
+
"authorization"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "Untangled Finance Limited",
|
|
33
|
+
"url": "https://untangled.finance"
|
|
34
|
+
},
|
|
9
35
|
"exports": {
|
|
10
36
|
".": {
|
|
11
37
|
"types": "./dist/src/index.d.ts",
|
|
12
38
|
"bun": "./src/index.ts",
|
|
13
39
|
"import": "./dist/src/index.js",
|
|
40
|
+
"require": "./dist-cjs/src/index.js",
|
|
14
41
|
"default": "./dist/src/index.js"
|
|
15
42
|
},
|
|
16
43
|
"./package.json": "./package.json"
|
|
@@ -20,6 +47,7 @@
|
|
|
20
47
|
},
|
|
21
48
|
"files": [
|
|
22
49
|
"dist",
|
|
50
|
+
"dist-cjs",
|
|
23
51
|
"src",
|
|
24
52
|
"!src/**/*.test.ts"
|
|
25
53
|
],
|
|
@@ -28,15 +56,19 @@
|
|
|
28
56
|
},
|
|
29
57
|
"scripts": {
|
|
30
58
|
"test": "bun test",
|
|
31
|
-
"build": "tsc -p tsconfig.build.json"
|
|
59
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
|
|
60
|
+
"build:esm": "tsc -p tsconfig.build.json",
|
|
61
|
+
"build:cjs": "tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
|
|
62
|
+
"prepublishOnly": "bun run build && bun test",
|
|
63
|
+
"prepack": "bun run build"
|
|
32
64
|
},
|
|
33
65
|
"dependencies": {
|
|
34
|
-
"@crediolabs/policy-
|
|
35
|
-
"@crediolabs/policy-synth": "^0.1.0",
|
|
66
|
+
"@crediolabs/policy-synth": "0.1.5",
|
|
36
67
|
"zod": "3.25.76"
|
|
37
68
|
},
|
|
38
69
|
"devDependencies": {
|
|
39
70
|
"@biomejs/biome": "2.5.5",
|
|
71
|
+
"@types/bun": "^1.3.0",
|
|
40
72
|
"@types/node": "^26.1.1",
|
|
41
73
|
"typescript": "5.9.3"
|
|
42
74
|
}
|
package/src/commands/record.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// `recordTransaction` - no business logic, just argv -> RecordInput + the
|
|
5
5
|
// canonical CLI envelope.
|
|
6
6
|
|
|
7
|
-
import { runRecordTransaction } from '@crediolabs/policy-builder-mcp'
|
|
8
7
|
import type { RecordedTransaction } from '@crediolabs/policy-synth'
|
|
8
|
+
import { runRecordTransaction } from '@crediolabs/policy-synth/run'
|
|
9
9
|
import { CliError, type CliFlags, formatToolResponse, parsePairs } from '../output.ts'
|
|
10
10
|
|
|
11
11
|
export async function runRecordCommand(
|