@naturali/cli 0.132.0 → 0.133.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/index.mjs +181 -9
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -54,9 +54,42 @@ the spec, so you never have to know.
|
|
|
54
54
|
| Number / boolean | `--temperature 0.7`, `--force` (a bare flag is `true`) |
|
|
55
55
|
| Object | `--tool-choice '{"type":"tool","name":"get_weather"}'` |
|
|
56
56
|
| Array | repeat the flag (`--capabilities a --capabilities b`) or pass a JSON array (`--tool-bindings '[{"tool_id":"tool_a"}]'`) |
|
|
57
|
+
| From a file | `--content @playbook.md` (`@@` sends a literal leading `@`) |
|
|
57
58
|
|
|
58
59
|
An array-typed field is always sent as a list, even with one value.
|
|
59
60
|
|
|
61
|
+
### File values
|
|
62
|
+
|
|
63
|
+
`@path` reads the value from disk. A `.yaml`, `.yml` or `.json` file is parsed,
|
|
64
|
+
and a `{ "file": "..." }` inside it is replaced by that file's contents,
|
|
65
|
+
resolved relative to the document holding the reference:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# naturali/squad.formation.yaml
|
|
69
|
+
resources:
|
|
70
|
+
Playbook:
|
|
71
|
+
type: document
|
|
72
|
+
properties:
|
|
73
|
+
path: /squad/playbook.md
|
|
74
|
+
content:
|
|
75
|
+
file: ../intelligence/playbook.md
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
naturali validate-formation --project-id proj_V1StGXR8Z5jdHi6B \
|
|
80
|
+
--template @naturali/squad.formation.yaml
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
That is what lets a formation template declare documents whose text is versioned
|
|
84
|
+
beside it, instead of carrying a pasted copy of each. Any other extension is read
|
|
85
|
+
as text and never parsed, so a markdown file opening with `{` stays a string.
|
|
86
|
+
|
|
87
|
+
Only a lone `{ "file": ... }` is a reference — an object with other keys beside
|
|
88
|
+
it is left alone, so a property genuinely named `file` is not hijacked.
|
|
89
|
+
|
|
90
|
+
A template can read any file you can. Read one you did not write before
|
|
91
|
+
deploying it.
|
|
92
|
+
|
|
60
93
|
Every path parameter a command needs must be passed explicitly; a command that
|
|
61
94
|
leaves one out fails and names it rather than guessing.
|
|
62
95
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as nodePath from "node:path";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { Command } from "commander";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { parse } from "yaml";
|
|
4
7
|
//#region \0rolldown/runtime.js
|
|
5
8
|
var __defProp = Object.defineProperty;
|
|
6
9
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -14,7 +17,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
14
17
|
};
|
|
15
18
|
//#endregion
|
|
16
19
|
//#region package.json
|
|
17
|
-
var version = "0.
|
|
20
|
+
var version = "0.133.0";
|
|
18
21
|
//#endregion
|
|
19
22
|
//#region ../sdk/src/generated/core/bodySerializer.gen.ts
|
|
20
23
|
const serializeFormDataPair = (data, key, value) => {
|
|
@@ -5216,13 +5219,13 @@ const parseStringFlagValue = (value) => {
|
|
|
5216
5219
|
*
|
|
5217
5220
|
* An array *of objects* — `tool_bindings` — has only the literal form:
|
|
5218
5221
|
* `--tool-bindings '[{"tool_id":"tool_a"}]'`.
|
|
5222
|
+
*
|
|
5223
|
+
* Values arrive already coerced, because an item may have come from a `@file`
|
|
5224
|
+
* reference and must keep the type its file declared.
|
|
5219
5225
|
*/
|
|
5220
|
-
const buildArrayFlagValue = (
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
});
|
|
5224
|
-
if (parsed.length === 1 && Array.isArray(parsed[0])) return parsed[0];
|
|
5225
|
-
return parsed;
|
|
5226
|
+
const buildArrayFlagValue = (values) => {
|
|
5227
|
+
if (values.length === 1 && Array.isArray(values[0])) return values[0];
|
|
5228
|
+
return values;
|
|
5226
5229
|
};
|
|
5227
5230
|
//#endregion
|
|
5228
5231
|
//#region src/help.ts
|
|
@@ -5279,16 +5282,174 @@ const renderCommandHelp = (args) => {
|
|
|
5279
5282
|
return lines.join("\n");
|
|
5280
5283
|
};
|
|
5281
5284
|
//#endregion
|
|
5285
|
+
//#region src/fileValues.ts
|
|
5286
|
+
/**
|
|
5287
|
+
* File references in flag values.
|
|
5288
|
+
*
|
|
5289
|
+
* A flag value written `@path` is read from disk instead of the command line.
|
|
5290
|
+
* That covers the two shapes a shell makes awkward: a long text field whose
|
|
5291
|
+
* content is already a file (`--content @playbook.md`), and a structured
|
|
5292
|
+
* document that itself names other files.
|
|
5293
|
+
*
|
|
5294
|
+
* A structured reference — `.yaml`, `.yml` or `.json` — is parsed, and any
|
|
5295
|
+
* `{ "file": "..." }` inside it is replaced by that file's contents, resolved
|
|
5296
|
+
* relative to the document holding the reference. That is what lets one
|
|
5297
|
+
* formation template declare documents whose text is versioned beside it,
|
|
5298
|
+
* rather than carrying a pasted copy of each.
|
|
5299
|
+
*
|
|
5300
|
+
* A template can read any file the caller can, as `curl -d @file` can — which
|
|
5301
|
+
* is the reason to read a template you did not write before deploying it.
|
|
5302
|
+
*/
|
|
5303
|
+
/** How deep `{ "file": ... }` references may nest before we call it a cycle. */
|
|
5304
|
+
const MAX_DEPTH = 10;
|
|
5305
|
+
const STRUCTURED = /\.(ya?ml|json)$/i;
|
|
5306
|
+
/** `@@` escapes a leading `@`, for a value that really starts with one. */
|
|
5307
|
+
const isFileReference = (raw) => {
|
|
5308
|
+
return raw.startsWith("@") && !raw.startsWith("@@");
|
|
5309
|
+
};
|
|
5310
|
+
/** Strips the escape from a literal value that begins with `@@`. */
|
|
5311
|
+
const unescapeLeadingAt = (raw) => {
|
|
5312
|
+
return raw.startsWith("@@") ? raw.slice(1) : raw;
|
|
5313
|
+
};
|
|
5314
|
+
var FileValueError = class extends Error {};
|
|
5315
|
+
const readText = (args) => {
|
|
5316
|
+
const { path } = args;
|
|
5317
|
+
try {
|
|
5318
|
+
return readFileSync(path, "utf8");
|
|
5319
|
+
} catch (error) {
|
|
5320
|
+
throw new FileValueError(`cannot read ${path}: ${error instanceof Error && "code" in error && error.code === "ENOENT" ? "no such file" : error instanceof Error ? error.message : String(error)}`);
|
|
5321
|
+
}
|
|
5322
|
+
};
|
|
5323
|
+
const parseStructured = (args) => {
|
|
5324
|
+
const { path, text } = args;
|
|
5325
|
+
try {
|
|
5326
|
+
return parse(text);
|
|
5327
|
+
} catch (error) {
|
|
5328
|
+
throw new FileValueError(`cannot parse ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
/**
|
|
5332
|
+
* A lone `{ file: "..." }` is a reference; an object with other keys beside it
|
|
5333
|
+
* is not, so a property genuinely named `file` keeps its value.
|
|
5334
|
+
*/
|
|
5335
|
+
const referenceTarget = (node) => {
|
|
5336
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) return;
|
|
5337
|
+
const keys = Object.keys(node);
|
|
5338
|
+
if (keys.length !== 1 || keys[0] !== "file") return;
|
|
5339
|
+
const target = node.file;
|
|
5340
|
+
return typeof target === "string" ? target : void 0;
|
|
5341
|
+
};
|
|
5342
|
+
/**
|
|
5343
|
+
* Walks a parsed document, replacing each reference with what it names. One
|
|
5344
|
+
* function rather than two: reading a structured file resumes the same walk on
|
|
5345
|
+
* its contents, so splitting it would only make the recursion mutual.
|
|
5346
|
+
*/
|
|
5347
|
+
const expand = (args) => {
|
|
5348
|
+
const { node, baseDir, depth } = args;
|
|
5349
|
+
const target = referenceTarget(node);
|
|
5350
|
+
if (target !== void 0) {
|
|
5351
|
+
if (depth >= MAX_DEPTH) throw new FileValueError(`file references nested more than ${MAX_DEPTH} deep — a reference cycle?`);
|
|
5352
|
+
const path = resolve(baseDir, target);
|
|
5353
|
+
const text = readText({ path });
|
|
5354
|
+
if (!STRUCTURED.test(path)) return text;
|
|
5355
|
+
return expand({
|
|
5356
|
+
node: parseStructured({
|
|
5357
|
+
path,
|
|
5358
|
+
text
|
|
5359
|
+
}),
|
|
5360
|
+
baseDir: dirname(path),
|
|
5361
|
+
depth: depth + 1
|
|
5362
|
+
});
|
|
5363
|
+
}
|
|
5364
|
+
if (Array.isArray(node)) return node.map((item) => {
|
|
5365
|
+
return expand({
|
|
5366
|
+
node: item,
|
|
5367
|
+
baseDir,
|
|
5368
|
+
depth
|
|
5369
|
+
});
|
|
5370
|
+
});
|
|
5371
|
+
if (typeof node === "object" && node !== null) return Object.fromEntries(Object.entries(node).map(([key, value]) => {
|
|
5372
|
+
return [key, expand({
|
|
5373
|
+
node: value,
|
|
5374
|
+
baseDir,
|
|
5375
|
+
depth
|
|
5376
|
+
})];
|
|
5377
|
+
}));
|
|
5378
|
+
return node;
|
|
5379
|
+
};
|
|
5380
|
+
/**
|
|
5381
|
+
* Resolves one raw flag value.
|
|
5382
|
+
*
|
|
5383
|
+
* `fromFile` tells the caller the value already has the type its file
|
|
5384
|
+
* declared, so it must skip the string coercions: those exist to guess a JSON
|
|
5385
|
+
* value out of a shell word, and a file has nothing to guess about.
|
|
5386
|
+
*/
|
|
5387
|
+
const resolveFlagValue = (args) => {
|
|
5388
|
+
const { raw } = args;
|
|
5389
|
+
if (!isFileReference(raw)) return {
|
|
5390
|
+
ok: true,
|
|
5391
|
+
value: unescapeLeadingAt(raw),
|
|
5392
|
+
fromFile: false
|
|
5393
|
+
};
|
|
5394
|
+
const reference = raw.slice(1);
|
|
5395
|
+
if (reference === "") return {
|
|
5396
|
+
ok: false,
|
|
5397
|
+
message: "\"@\" needs a path after it."
|
|
5398
|
+
};
|
|
5399
|
+
try {
|
|
5400
|
+
return {
|
|
5401
|
+
ok: true,
|
|
5402
|
+
value: expand({
|
|
5403
|
+
node: { file: reference },
|
|
5404
|
+
baseDir: process.cwd(),
|
|
5405
|
+
depth: 0
|
|
5406
|
+
}),
|
|
5407
|
+
fromFile: true
|
|
5408
|
+
};
|
|
5409
|
+
} catch (error) {
|
|
5410
|
+
if (error instanceof FileValueError) return {
|
|
5411
|
+
ok: false,
|
|
5412
|
+
message: error.message
|
|
5413
|
+
};
|
|
5414
|
+
throw error;
|
|
5415
|
+
}
|
|
5416
|
+
};
|
|
5417
|
+
//#endregion
|
|
5282
5418
|
//#region src/routeCall.ts
|
|
5283
5419
|
const placeFlags = (args) => {
|
|
5284
5420
|
const { route, parsedFlags, call } = args;
|
|
5421
|
+
const fileErrors = [];
|
|
5422
|
+
/**
|
|
5423
|
+
* A value read from a file is used as the file declared it. Only a value
|
|
5424
|
+
* still in its shell spelling goes through `coerce`, which guesses a JSON
|
|
5425
|
+
* type out of a word.
|
|
5426
|
+
*/
|
|
5427
|
+
const readValue = (readArgs) => {
|
|
5428
|
+
const { flagKey, raw, coerce } = readArgs;
|
|
5429
|
+
const resolved = resolveFlagValue({ raw });
|
|
5430
|
+
if (!resolved.ok) {
|
|
5431
|
+
fileErrors.push(`--${flagKey}: ${resolved.message}`);
|
|
5432
|
+
return;
|
|
5433
|
+
}
|
|
5434
|
+
return resolved.fromFile ? resolved.value : coerce(resolved.value);
|
|
5435
|
+
};
|
|
5285
5436
|
const typeByCanonical = new Map(route.flags.map((flag) => {
|
|
5286
5437
|
return [toCanonical(flag.name), flag.type];
|
|
5287
5438
|
}));
|
|
5288
5439
|
for (const [flagKey, rawValue] of Object.entries(parsedFlags.single)) {
|
|
5289
5440
|
const canonical = toCanonical(flagKey);
|
|
5290
5441
|
const declaredType = typeByCanonical.get(canonical);
|
|
5291
|
-
const value = declaredType === "array" ? buildArrayFlagValue(parsedFlags.repeated[flagKey] ?? [rawValue])
|
|
5442
|
+
const value = declaredType === "array" ? buildArrayFlagValue((parsedFlags.repeated[flagKey] ?? [rawValue]).map((item) => {
|
|
5443
|
+
return readValue({
|
|
5444
|
+
flagKey,
|
|
5445
|
+
raw: item,
|
|
5446
|
+
coerce: parseFlagValue
|
|
5447
|
+
});
|
|
5448
|
+
})) : readValue({
|
|
5449
|
+
flagKey,
|
|
5450
|
+
raw: rawValue,
|
|
5451
|
+
coerce: declaredType === "string" ? parseStringFlagValue : parseFlagValue
|
|
5452
|
+
});
|
|
5292
5453
|
const pathParam = route.pathParams.find((param) => {
|
|
5293
5454
|
return toCanonical(param) === canonical;
|
|
5294
5455
|
});
|
|
@@ -5309,6 +5470,7 @@ const placeFlags = (args) => {
|
|
|
5309
5470
|
}
|
|
5310
5471
|
call.body[kebabToSnake(flagKey)] = value;
|
|
5311
5472
|
}
|
|
5473
|
+
return fileErrors;
|
|
5312
5474
|
};
|
|
5313
5475
|
const PROJECT_ID_PARAM = "project_id";
|
|
5314
5476
|
/**
|
|
@@ -5336,11 +5498,16 @@ const buildRouteCall = (args) => {
|
|
|
5336
5498
|
query: {},
|
|
5337
5499
|
body: {}
|
|
5338
5500
|
};
|
|
5339
|
-
placeFlags({
|
|
5501
|
+
const fileErrors = placeFlags({
|
|
5340
5502
|
route,
|
|
5341
5503
|
parsedFlags,
|
|
5342
5504
|
call
|
|
5343
5505
|
});
|
|
5506
|
+
if (fileErrors.length > 0) return {
|
|
5507
|
+
ok: false,
|
|
5508
|
+
kind: "file_errors",
|
|
5509
|
+
fileErrors
|
|
5510
|
+
};
|
|
5344
5511
|
applyProjectIdEnvDefault({
|
|
5345
5512
|
route,
|
|
5346
5513
|
call
|
|
@@ -5350,6 +5517,7 @@ const buildRouteCall = (args) => {
|
|
|
5350
5517
|
});
|
|
5351
5518
|
if (missingPathParams.length > 0) return {
|
|
5352
5519
|
ok: false,
|
|
5520
|
+
kind: "missing_path_params",
|
|
5353
5521
|
missingPathParams
|
|
5354
5522
|
};
|
|
5355
5523
|
return {
|
|
@@ -5494,6 +5662,10 @@ const dispatchCommand = async (args) => {
|
|
|
5494
5662
|
route,
|
|
5495
5663
|
parsedFlags
|
|
5496
5664
|
});
|
|
5665
|
+
if (!built.ok && built.kind === "file_errors") {
|
|
5666
|
+
console.error(built.fileErrors.join("\n"));
|
|
5667
|
+
process.exit(1);
|
|
5668
|
+
}
|
|
5497
5669
|
if (!built.ok) {
|
|
5498
5670
|
const flagsHint = built.missingPathParams.map((param) => {
|
|
5499
5671
|
return `--${param.replace(/_/g, "-")} <value>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturali/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.133.0",
|
|
4
4
|
"description": "Command-line interface for the naturali.ai API, generated from its OpenAPI specs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"commander": "^15.0.0"
|
|
24
|
+
"commander": "^15.0.0",
|
|
25
|
+
"yaml": "^2.9.1"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@naturali/sdk": "0.
|
|
28
|
+
"@naturali/sdk": "0.133.0",
|
|
28
29
|
"@ttoss/openapi-codegen": "^0.3.1",
|
|
29
30
|
"@types/node": "^26.5.1",
|
|
30
31
|
"tsdown": "^0.23.0",
|