@martini-film/cli 0.2.0 → 0.4.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 +173 -56
- package/dist/api.js +69 -2
- package/dist/api.js.map +1 -1
- package/dist/approve.js +2 -1
- package/dist/approve.js.map +1 -1
- package/dist/args.js +31 -6
- package/dist/args.js.map +1 -1
- package/dist/cli.js +457 -110
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.js +297 -45
- package/dist/commands/assets.js.map +1 -1
- package/dist/commands/call.js +99 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/context.js +16 -3
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/files.js +208 -83
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/guide.js +67 -0
- package/dist/commands/guide.js.map +1 -0
- package/dist/commands/init.js +196 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.js +69 -15
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/tools.js +63 -78
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/use.js +143 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/config.js +87 -6
- package/dist/config.js.map +1 -1
- package/dist/generated/guide.json +104 -0
- package/dist/generated/manifest.json +3885 -241
- package/dist/guideCache.js +204 -0
- package/dist/guideCache.js.map +1 -0
- package/dist/help.js +391 -34
- package/dist/help.js.map +1 -1
- package/dist/manifest.js +34 -3
- package/dist/manifest.js.map +1 -1
- package/dist/manifestCache.js +196 -0
- package/dist/manifestCache.js.map +1 -0
- package/dist/output.js +251 -28
- package/dist/output.js.map +1 -1
- package/dist/schema.js +132 -15
- package/dist/schema.js.map +1 -1
- package/dist/wait.js +181 -8
- package/dist/wait.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { CliApiError, createApiClient, isMediaBlock, isTextBlock } from './api.js';
|
|
2
4
|
import { approvalSupport, runWithApproval } from './approve.js';
|
|
3
5
|
import { flagBoolean, flagString, parseArgs } from './args.js';
|
|
4
6
|
import { download, upload, wait } from './commands/assets.js';
|
|
7
|
+
import { call } from './commands/call.js';
|
|
5
8
|
import { assertKnownFlags, compoundBooleanFlags, CONTEXT_FLAG_ALIASES, needsLocalProjection, selectedFields, timeoutMs, wantsWait, } from './commands/context.js';
|
|
6
9
|
import { files } from './commands/files.js';
|
|
10
|
+
import { guide } from './commands/guide.js';
|
|
7
11
|
import { login, logout, whoami } from './commands/login.js';
|
|
8
|
-
import { init
|
|
12
|
+
import { init } from './commands/init.js';
|
|
13
|
+
import { models, probe, tools, version } from './commands/tools.js';
|
|
14
|
+
import { use } from './commands/use.js';
|
|
9
15
|
import { resolveConfig } from './config.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
16
|
+
import { GUIDE_HASH_HEADER, guideFetcher, guideHashNotice, loadBakedGuide, openGuide, } from './guideCache.js';
|
|
17
|
+
import { renderCompoundHelp, renderHelp, renderToolHelp, schemaKeyLines } from './help.js';
|
|
18
|
+
import { findTool, loadManifest, TOOL_NAME_PATTERN } from './manifest.js';
|
|
19
|
+
import { openManifest } from './manifestCache.js';
|
|
20
|
+
import { EXIT, MEDIA_EXTENSIONS, assertRawDestination, expandMapPaths, hasUndeclaredMapStep, isDeclaredPath, isEmptyProjection, isMediaOutPath, noFieldsMatchedMessage, noteMediaStubs, printBytes, printError, printResult, printWarning, resolveMapPaths, selectFields, unmatchedFields, warnUnmatchedFields, } from './output.js';
|
|
21
|
+
import { ArgumentError, argumentSchema, buildArguments, flagSpecs, GLOBAL_FLAG_NAMES, unknownToolFlags, } from './schema.js';
|
|
22
|
+
import { assetsOutcome, handleOutcome, notCompleted, producedAssetIds, waitForAssets, waitForDelivery, waitForJob, waitForRun, WaitTimeoutError, } from './wait.js';
|
|
15
23
|
export const COMPOUND_COMMANDS = [
|
|
16
24
|
login,
|
|
17
25
|
logout,
|
|
18
26
|
whoami,
|
|
27
|
+
use,
|
|
28
|
+
guide,
|
|
19
29
|
tools,
|
|
30
|
+
call,
|
|
20
31
|
models,
|
|
21
32
|
upload,
|
|
22
33
|
download,
|
|
@@ -26,26 +37,15 @@ export const COMPOUND_COMMANDS = [
|
|
|
26
37
|
init,
|
|
27
38
|
version,
|
|
28
39
|
];
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
'version',
|
|
32
|
-
'fields',
|
|
33
|
-
'out',
|
|
34
|
-
'wait',
|
|
35
|
-
'timeout',
|
|
36
|
-
'quiet',
|
|
37
|
-
'raw',
|
|
38
|
-
'project',
|
|
39
|
-
'canvas',
|
|
40
|
-
'run',
|
|
41
|
-
'api-key',
|
|
42
|
-
'api-url',
|
|
43
|
-
'profile',
|
|
44
|
-
'context',
|
|
40
|
+
export { GLOBAL_FLAG_NAMES };
|
|
41
|
+
const TOOL_ONLY_GLOBAL_FLAGS = new Set([
|
|
45
42
|
'approve',
|
|
46
43
|
'max-olives',
|
|
44
|
+
'out-dir',
|
|
45
|
+
'schema',
|
|
46
|
+
'all',
|
|
47
|
+
'keys',
|
|
47
48
|
]);
|
|
48
|
-
const TOOL_ONLY_GLOBAL_FLAGS = new Set(['approve', 'max-olives']);
|
|
49
49
|
const GLOBAL_FLAGS_FOR_COMPOUNDS = new Set([
|
|
50
50
|
...[...GLOBAL_FLAG_NAMES].filter(flag => !TOOL_ONLY_GLOBAL_FLAGS.has(flag)),
|
|
51
51
|
...CONTEXT_FLAG_ALIASES,
|
|
@@ -58,7 +58,27 @@ const WAIT_BY_DEFAULT = new Set([
|
|
|
58
58
|
'upscale_assets',
|
|
59
59
|
'render_blender_take',
|
|
60
60
|
]);
|
|
61
|
-
const
|
|
61
|
+
const ORDERED_RESULT_TOOLS = new Set(['inspect_video']);
|
|
62
|
+
const BOOLEAN_GLOBAL_FLAGS = new Set(['help', 'version', 'wait', 'quiet', 'raw', 'context', 'approve', 'all', 'keys']);
|
|
63
|
+
function parseCommandLine(argv, manifest) {
|
|
64
|
+
const toolFor = (command) => command && !COMPOUND_COMMANDS.some(candidate => candidate.name === command)
|
|
65
|
+
? findTool(command, manifest)
|
|
66
|
+
: undefined;
|
|
67
|
+
return parseArgs(argv, (flag, command) => {
|
|
68
|
+
if (BOOLEAN_GLOBAL_FLAGS.has(flag))
|
|
69
|
+
return true;
|
|
70
|
+
const compound = COMPOUND_COMMANDS.find(candidate => candidate.name === command);
|
|
71
|
+
if (compound)
|
|
72
|
+
return compoundBooleanFlags(compound).includes(flag);
|
|
73
|
+
const tool = toolFor(command);
|
|
74
|
+
return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'boolean') : false;
|
|
75
|
+
}, (flag, command) => {
|
|
76
|
+
if (GLOBAL_FLAG_NAMES.has(flag))
|
|
77
|
+
return false;
|
|
78
|
+
const tool = toolFor(command);
|
|
79
|
+
return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'array') : false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
62
82
|
export async function main(options) {
|
|
63
83
|
const env = options.env ?? process.env;
|
|
64
84
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -67,87 +87,216 @@ export async function main(options) {
|
|
|
67
87
|
...(options.stderr ? { stderr: options.stderr } : {}),
|
|
68
88
|
...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
|
|
69
89
|
};
|
|
70
|
-
|
|
71
|
-
const parsed = parseArgs(options.argv, (flag, command) => {
|
|
72
|
-
if (BOOLEAN_GLOBAL_FLAGS.has(flag))
|
|
73
|
-
return true;
|
|
74
|
-
const compound = COMPOUND_COMMANDS.find(candidate => candidate.name === command);
|
|
75
|
-
if (compound)
|
|
76
|
-
return compoundBooleanFlags(compound).includes(flag);
|
|
77
|
-
const tool = command ? findTool(command, manifest) : undefined;
|
|
78
|
-
return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'boolean') : false;
|
|
79
|
-
});
|
|
90
|
+
let session;
|
|
80
91
|
try {
|
|
81
|
-
|
|
82
|
-
parsed.command = 'version';
|
|
83
|
-
if (parsed.command === undefined ||
|
|
84
|
-
parsed.command === 'help' ||
|
|
85
|
-
(flagBoolean(parsed.flags, 'help') && !parsed.command)) {
|
|
86
|
-
printResult(renderHelp(manifest, COMPOUND_COMMANDS), output);
|
|
87
|
-
return EXIT.ok;
|
|
88
|
-
}
|
|
89
|
-
const compound = COMPOUND_COMMANDS.find(command => command.name === parsed.command);
|
|
90
|
-
const tool = compound ? undefined : findTool(parsed.command, manifest);
|
|
91
|
-
if (!compound && !tool) {
|
|
92
|
-
throw new ArgumentError(`Unknown command "${parsed.command}". Run martini --help for the list.`);
|
|
93
|
-
}
|
|
94
|
-
if (flagBoolean(parsed.flags, 'help')) {
|
|
95
|
-
printResult(tool ? renderToolHelp(tool) : renderCompoundHelp(compound), output);
|
|
96
|
-
return EXIT.ok;
|
|
97
|
-
}
|
|
98
|
-
if (compound)
|
|
99
|
-
assertKnownFlags(compound, parsed.flags, GLOBAL_FLAGS_FOR_COMPOUNDS);
|
|
100
|
-
const config = resolveConfig(parsed.flags, { env, cwd });
|
|
101
|
-
const clientFor = options.createClient ??
|
|
102
|
-
(resolved => createApiClient(resolved, { ...(options.fetch ? { fetch: options.fetch } : {}), env }));
|
|
103
|
-
const out = flagString(parsed.flags, 'out');
|
|
104
|
-
if (out)
|
|
105
|
-
output.out = out;
|
|
106
|
-
output.inRun = Boolean(config.runId);
|
|
107
|
-
output.quiet = flagBoolean(parsed.flags, 'quiet') ?? false;
|
|
108
|
-
const context = {
|
|
109
|
-
client: clientFor(config),
|
|
110
|
-
clientFor,
|
|
111
|
-
config,
|
|
112
|
-
manifest,
|
|
113
|
-
flags: parsed.flags,
|
|
114
|
-
positionals: parsed.positionals,
|
|
115
|
-
output,
|
|
116
|
-
env,
|
|
117
|
-
cwd,
|
|
118
|
-
now: options.now ?? Date.now,
|
|
119
|
-
sleep: options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
120
|
-
};
|
|
121
|
-
const select = selectedFields(context);
|
|
122
|
-
timeoutMs(context);
|
|
123
|
-
if (compound) {
|
|
124
|
-
const result = await compound.run(context);
|
|
125
|
-
const projected = select.length && result && typeof result === 'object' ? selectFields(result, select) : result;
|
|
126
|
-
warnNoFieldsMatched(projected, select, output);
|
|
127
|
-
printResult(projected, output);
|
|
128
|
-
return EXIT.ok;
|
|
129
|
-
}
|
|
130
|
-
const result = await runTool(tool, context, parsed.flags);
|
|
131
|
-
warnNoFieldsMatched(result, select, output, knownResultFields(tool));
|
|
132
|
-
printResult(result, output);
|
|
133
|
-
return EXIT.ok;
|
|
92
|
+
session = openSession(options, env, cwd, output);
|
|
134
93
|
}
|
|
135
94
|
catch (error) {
|
|
136
|
-
if (error instanceof CliApiError && error.result !== undefined)
|
|
137
|
-
printResult(error.result, output);
|
|
138
95
|
return printError(error, output);
|
|
139
96
|
}
|
|
97
|
+
const { config, clientFor, client, manifests } = session;
|
|
98
|
+
let refreshed;
|
|
99
|
+
for (;;) {
|
|
100
|
+
const manifest = manifests.state.manifest;
|
|
101
|
+
const parsed = parseCommandLine(options.argv, manifest);
|
|
102
|
+
if (parsed.command)
|
|
103
|
+
output.command = parsed.command;
|
|
104
|
+
try {
|
|
105
|
+
if (flagBoolean(parsed.flags, 'version') && parsed.command === undefined)
|
|
106
|
+
parsed.command = 'version';
|
|
107
|
+
if (parsed.command === undefined && parsed.flags.has('schema')) {
|
|
108
|
+
throw new ArgumentError("--schema describes one tool's arguments: martini <tool> --schema [flag].");
|
|
109
|
+
}
|
|
110
|
+
if (parsed.command === undefined ||
|
|
111
|
+
parsed.command === 'help' ||
|
|
112
|
+
(flagBoolean(parsed.flags, 'help') && !parsed.command)) {
|
|
113
|
+
printResult(renderHelp(manifest, COMPOUND_COMMANDS), output);
|
|
114
|
+
return EXIT.ok;
|
|
115
|
+
}
|
|
116
|
+
const compound = COMPOUND_COMMANDS.find(command => command.name === parsed.command);
|
|
117
|
+
const tool = compound ? undefined : findTool(parsed.command, manifest);
|
|
118
|
+
if (!compound && !tool) {
|
|
119
|
+
if (refreshed === undefined && TOOL_NAME_PATTERN.test(parsed.command)) {
|
|
120
|
+
refreshed = await manifests.refresh();
|
|
121
|
+
if (refreshed === 'updated')
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
throw unknownCommandError(parsed.command, refreshed);
|
|
125
|
+
}
|
|
126
|
+
if (flagBoolean(parsed.flags, 'help')) {
|
|
127
|
+
printResult(tool
|
|
128
|
+
? renderToolHelp(tool, { all: flagBoolean(parsed.flags, 'all') === true })
|
|
129
|
+
: renderCompoundHelp(compound), output);
|
|
130
|
+
return EXIT.ok;
|
|
131
|
+
}
|
|
132
|
+
if (parsed.flags.has('schema')) {
|
|
133
|
+
if (compound) {
|
|
134
|
+
throw new ArgumentError(`--schema describes a tool's arguments; ${compound.name} is a compound command (martini ${compound.name} --help lists its flags).`);
|
|
135
|
+
}
|
|
136
|
+
const target = schemaTarget(options.argv, parsed.flags);
|
|
137
|
+
const schema = argumentSchema(tool, target);
|
|
138
|
+
printResult(flagBoolean(parsed.flags, 'keys') === true
|
|
139
|
+
? `${schemaKeyLines(schema, target ? `--${target.replace(/^--/, '')}` : `${tool.name} arguments`).join('\n')}\n`
|
|
140
|
+
: `${JSON.stringify(schema, null, 2)}\n`, output);
|
|
141
|
+
return EXIT.ok;
|
|
142
|
+
}
|
|
143
|
+
if (parsed.flags.has('keys'))
|
|
144
|
+
throw new ArgumentError('--keys goes with --schema: martini <tool> --schema <flag> --keys.');
|
|
145
|
+
if (parsed.flags.has('all'))
|
|
146
|
+
throw new ArgumentError('--all goes with --help: martini <tool> --help --all.');
|
|
147
|
+
if (compound)
|
|
148
|
+
assertKnownFlags(compound, parsed.flags, GLOBAL_FLAGS_FOR_COMPOUNDS);
|
|
149
|
+
else {
|
|
150
|
+
if (refreshed === undefined && unknownToolFlags(tool, parsed.flags, nonArgumentFlags(tool)).length > 0) {
|
|
151
|
+
refreshed = await manifests.refresh();
|
|
152
|
+
if (refreshed === 'updated')
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (parsed.positionals.length > 0)
|
|
156
|
+
throw strayPositionalError(tool, parsed.positionals);
|
|
157
|
+
}
|
|
158
|
+
for (const warning of config.warnings)
|
|
159
|
+
printWarning('CONFIG_WARNING', warning, output);
|
|
160
|
+
const out = flagString(parsed.flags, 'out');
|
|
161
|
+
if (out)
|
|
162
|
+
output.out = out;
|
|
163
|
+
output.inRun = Boolean(config.runId);
|
|
164
|
+
output.quiet = flagBoolean(parsed.flags, 'quiet') ?? false;
|
|
165
|
+
return await runCommand(compound, tool, {
|
|
166
|
+
client,
|
|
167
|
+
clientFor,
|
|
168
|
+
config,
|
|
169
|
+
manifest,
|
|
170
|
+
manifests,
|
|
171
|
+
guide: lazyGuide(options, config, env, output),
|
|
172
|
+
flags: parsed.flags,
|
|
173
|
+
positionals: parsed.positionals,
|
|
174
|
+
output,
|
|
175
|
+
env,
|
|
176
|
+
cwd,
|
|
177
|
+
now: options.now ?? Date.now,
|
|
178
|
+
sleep: options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof CliApiError && error.result !== undefined)
|
|
183
|
+
printResult(error.result, output);
|
|
184
|
+
return printError(error, output);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
140
187
|
}
|
|
141
|
-
|
|
188
|
+
function openSession(options, env, cwd, output) {
|
|
189
|
+
const baked = options.manifest ?? loadManifest();
|
|
190
|
+
const preliminary = parseCommandLine(options.argv, baked);
|
|
191
|
+
const config = resolveConfig(preliminary.flags, { env, cwd });
|
|
192
|
+
const warnings = { ...output, quiet: flagBoolean(preliminary.flags, 'quiet') ?? false };
|
|
193
|
+
const noticeGuideHash = guideHashNotice({
|
|
194
|
+
baked: loadBakedGuide(),
|
|
195
|
+
apiUrl: config.apiUrl,
|
|
196
|
+
apiKey: config.apiKey,
|
|
197
|
+
env,
|
|
198
|
+
quiet: warnings.quiet === true,
|
|
199
|
+
warn: (code, message) => printWarning(code, message, warnings),
|
|
200
|
+
});
|
|
201
|
+
const clientFor = options.createClient ??
|
|
202
|
+
((resolved) => createApiClient(resolved, {
|
|
203
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
204
|
+
env,
|
|
205
|
+
onResponse: response => {
|
|
206
|
+
const hash = response.headers.get(GUIDE_HASH_HEADER);
|
|
207
|
+
if (hash)
|
|
208
|
+
noticeGuideHash(hash);
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
const client = clientFor(config);
|
|
212
|
+
const manifests = openManifest({
|
|
213
|
+
baked,
|
|
214
|
+
apiUrl: config.apiUrl,
|
|
215
|
+
apiKey: config.apiKey,
|
|
216
|
+
env,
|
|
217
|
+
fetchManifest: ifNoneMatch => client.fetchManifest(ifNoneMatch),
|
|
218
|
+
warn: (code, message) => printWarning(code, message, warnings),
|
|
219
|
+
...(options.now ? { now: options.now } : {}),
|
|
220
|
+
});
|
|
221
|
+
return { config, clientFor, client, manifests };
|
|
222
|
+
}
|
|
223
|
+
function lazyGuide(options, config, env, output) {
|
|
224
|
+
let runtime;
|
|
225
|
+
return () => (runtime ??= openGuide({
|
|
226
|
+
baked: loadBakedGuide(),
|
|
227
|
+
apiUrl: config.apiUrl,
|
|
228
|
+
apiKey: config.apiKey,
|
|
229
|
+
env,
|
|
230
|
+
fetchGuide: guideFetcher({ fetch: options.fetch ?? fetch, apiUrl: config.apiUrl, apiKey: config.apiKey, env }),
|
|
231
|
+
warn: (code, message) => printWarning(code, message, output),
|
|
232
|
+
...(options.now ? { now: options.now } : {}),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
function schemaTarget(argv, flags) {
|
|
236
|
+
const value = flagString(flags, 'schema');
|
|
237
|
+
if (value)
|
|
238
|
+
return value;
|
|
239
|
+
const index = argv.indexOf('--schema');
|
|
240
|
+
const next = index >= 0 ? argv[index + 1] : undefined;
|
|
241
|
+
if (!next?.startsWith('--') || next === '--')
|
|
242
|
+
return undefined;
|
|
243
|
+
const name = next.slice(2).split('=')[0];
|
|
244
|
+
return GLOBAL_FLAG_NAMES.has(name) ? undefined : name;
|
|
245
|
+
}
|
|
246
|
+
function unknownCommandError(name, refreshed) {
|
|
247
|
+
const message = `Unknown command "${name}". Run martini --help for the list.`;
|
|
248
|
+
if (!TOOL_NAME_PATTERN.test(name))
|
|
249
|
+
return new ArgumentError(message);
|
|
250
|
+
const checked = refreshed === 'updated' || refreshed === 'unchanged';
|
|
251
|
+
return new ArgumentError(message, {
|
|
252
|
+
hint: checked
|
|
253
|
+
? `The server's tool list for this key has no "${name}" either (martini tools compares it with this build's). To send the call by name anyway: martini call ${name} --args '{…}'.`
|
|
254
|
+
: `The server's tool list was not checked, so a tool added since this build may be missing: martini call ${name} --args '{…}' sends the call by name.`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function nonArgumentFlags(tool) {
|
|
142
258
|
const properties = tool.inputSchema.properties ?? {};
|
|
143
259
|
const ignore = new Set(GLOBAL_FLAG_NAMES);
|
|
144
260
|
for (const alias of CONTEXT_FLAG_ALIASES) {
|
|
145
261
|
if (!(alias.replace(/-id$/, 'Id') in properties))
|
|
146
262
|
ignore.add(alias);
|
|
147
263
|
}
|
|
264
|
+
return ignore;
|
|
265
|
+
}
|
|
266
|
+
async function runCommand(compound, tool, context) {
|
|
267
|
+
const { output } = context;
|
|
268
|
+
const select = selectedFields(context);
|
|
269
|
+
timeoutMs(context);
|
|
270
|
+
if (compound) {
|
|
271
|
+
const result = await compound.run(context);
|
|
272
|
+
printResult(result && typeof result === 'object'
|
|
273
|
+
? projectResult(result, select, { readOnly: compound.readOnly === true, known: [], command: compound.name }, output)
|
|
274
|
+
: result, output);
|
|
275
|
+
return EXIT.ok;
|
|
276
|
+
}
|
|
277
|
+
printResult(await runTool(tool, context, context.flags), output);
|
|
278
|
+
return EXIT.ok;
|
|
279
|
+
}
|
|
280
|
+
function projectResult(result, select, what, output) {
|
|
281
|
+
if (select.length === 0)
|
|
282
|
+
return result;
|
|
283
|
+
const projected = selectFields(result, select);
|
|
284
|
+
if (!isEmptyProjection(projected) || isEmptyProjection(result)) {
|
|
285
|
+
warnUnmatchedFields(result, select, output, what.schema);
|
|
286
|
+
return projected;
|
|
287
|
+
}
|
|
288
|
+
if (allDeclared(select, what.schema))
|
|
289
|
+
return projected;
|
|
290
|
+
const message = noFieldsMatchedMessage(what.typed ?? select, what.known);
|
|
291
|
+
if (what.readOnly)
|
|
292
|
+
throw new CliApiError(message, { code: 'NO_FIELDS_MATCHED', status: 0 });
|
|
293
|
+
printWarning('NO_FIELDS_MATCHED', `${message} ${what.command} did its work, so its whole result is printed instead; do not run it again for this.`, output);
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
async function runTool(tool, context, flags) {
|
|
148
297
|
const useContext = flagBoolean(flags, 'context') !== false;
|
|
149
298
|
const args = buildArguments(tool, flags, {
|
|
150
|
-
ignore,
|
|
299
|
+
ignore: nonArgumentFlags(tool),
|
|
151
300
|
context: useContext
|
|
152
301
|
? {
|
|
153
302
|
...(context.config.projectId ? { projectId: context.config.projectId } : {}),
|
|
@@ -156,29 +305,108 @@ async function runTool(tool, context, flags) {
|
|
|
156
305
|
}
|
|
157
306
|
: {},
|
|
158
307
|
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
308
|
+
const readOnly = tool.annotations.readOnlyHint === true;
|
|
309
|
+
const out = flagString(flags, 'out');
|
|
310
|
+
const outDir = flagString(flags, 'out-dir');
|
|
311
|
+
if (outDir) {
|
|
312
|
+
if (out)
|
|
313
|
+
throw new ArgumentError('Use either --out or --out-dir.');
|
|
314
|
+
if (!readOnly) {
|
|
315
|
+
throw new ArgumentError('--out-dir requires a read-only tool. Run this command without --out-dir.');
|
|
316
|
+
}
|
|
317
|
+
const result = await context.client.callToolBlocks(tool.name, args, { media: 'inline' });
|
|
318
|
+
const files = mediaFiles(result.blocks, tool.name);
|
|
319
|
+
if (files.length === 0)
|
|
320
|
+
return readableBlocks(result.blocks);
|
|
321
|
+
const directory = resolve(context.cwd, outDir);
|
|
322
|
+
mkdirSync(directory, { recursive: true });
|
|
323
|
+
for (const file of files)
|
|
324
|
+
writeFileSync(join(directory, file.name), file.bytes);
|
|
325
|
+
const receipt = {
|
|
326
|
+
dir: directory,
|
|
327
|
+
files: files.map(file => ({
|
|
328
|
+
path: join(directory, file.name),
|
|
329
|
+
mimeType: file.mimeType,
|
|
330
|
+
bytes: file.bytes.byteLength,
|
|
331
|
+
...(file.about !== undefined ? { about: file.about } : {}),
|
|
332
|
+
})),
|
|
333
|
+
...(unpairedTexts(result.blocks).length ? { notes: unpairedTexts(result.blocks) } : {}),
|
|
334
|
+
};
|
|
335
|
+
return projectResult(receipt, selectedFields(context), { readOnly: false, known: Object.keys(receipt), command: tool.name }, context.output);
|
|
336
|
+
}
|
|
337
|
+
const mediaOut = out !== undefined && isMediaOutPath(out);
|
|
338
|
+
const raw = flagBoolean(flags, 'raw') === true;
|
|
339
|
+
const mediaFromResult = mediaOut && !raw && !readOnly;
|
|
340
|
+
if (raw || (mediaOut && readOnly)) {
|
|
341
|
+
assertRawDestination(context.output);
|
|
342
|
+
let bytes;
|
|
343
|
+
try {
|
|
344
|
+
bytes = await context.client.callToolRaw(tool.name, args);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (error instanceof CliApiError && error.code === 'NOT_RAW_ELIGIBLE') {
|
|
348
|
+
throw new ArgumentError(raw
|
|
349
|
+
? 'This result cannot use --raw: it does not hold exactly one media block. Use --out-dir <dir> to write several media files.'
|
|
350
|
+
: `--out ${out} names a media file, but ${tool.name} did not answer exactly one image or audio block. Use --out-dir <dir> for several media files, or name a .json file for the JSON result.`);
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
printBytes(bytes.bytes, {
|
|
355
|
+
...context.output,
|
|
356
|
+
contentType: bytes.contentType,
|
|
357
|
+
meta: bytes.meta,
|
|
358
|
+
fields: selectedFields(context),
|
|
359
|
+
});
|
|
162
360
|
delete context.output.out;
|
|
163
361
|
context.output.quiet = true;
|
|
164
|
-
return
|
|
362
|
+
return undefined;
|
|
165
363
|
}
|
|
166
|
-
const
|
|
364
|
+
const requested = mediaFromResult ? [] : selectedFields(context);
|
|
365
|
+
const finish = (result) => (mediaFromResult ? saveMediaFromResult(result, tool, context, out) : result);
|
|
366
|
+
if (ORDERED_RESULT_TOOLS.has(tool.name)) {
|
|
367
|
+
approvalFlags(tool, flags);
|
|
368
|
+
const media = context.output.out ? 'inline' : 'stub';
|
|
369
|
+
const result = await context.client.callToolBlocks(tool.name, args, { media });
|
|
370
|
+
if (media === 'stub')
|
|
371
|
+
noteMediaStubs(result.blocks, context.output);
|
|
372
|
+
return projectResult(readableBlocks(result.blocks), requested, { readOnly: true, known: ['blocks'], command: tool.name }, context.output);
|
|
373
|
+
}
|
|
374
|
+
const select = expandMapPaths(requested, tool.outputSchema);
|
|
375
|
+
const known = Object.keys(tool.outputSchema?.properties ?? {});
|
|
167
376
|
const shouldWait = wantsWait(context, WAIT_BY_DEFAULT.has(tool.name));
|
|
168
377
|
const approve = approvalFlags(tool, flags);
|
|
169
|
-
const local = shouldWait ||
|
|
378
|
+
const local = shouldWait ||
|
|
379
|
+
approve !== null ||
|
|
380
|
+
needsLocalProjection(select) ||
|
|
381
|
+
hasUndeclaredMapStep(requested, tool.outputSchema) ||
|
|
382
|
+
!readOnly ||
|
|
383
|
+
tool.outputSchema === undefined;
|
|
384
|
+
const what = { readOnly, known, command: tool.name, typed: requested, schema: tool.outputSchema };
|
|
385
|
+
const selectIn = (value) => resolveMapPaths(value, requested, tool.outputSchema);
|
|
170
386
|
const result = approve
|
|
171
387
|
? await runWithApproval(tool, context.client, args, approve.support, approve.maxOlives === undefined ? {} : { maxOlives: approve.maxOlives })
|
|
172
388
|
: await context.client.callTool(tool.name, args, !local && select.length ? { select } : {});
|
|
173
|
-
if (!shouldWait)
|
|
174
|
-
|
|
389
|
+
if (!shouldWait) {
|
|
390
|
+
if (local)
|
|
391
|
+
return finish(projectResult(result, selectIn(result), what, context.output));
|
|
392
|
+
if (select.length && unmatchedFields(result, select).length === select.length) {
|
|
393
|
+
if (allDeclared(select, tool.outputSchema))
|
|
394
|
+
return result;
|
|
395
|
+
throw new CliApiError(noFieldsMatchedMessage(requested, known), { code: 'NO_FIELDS_MATCHED', status: 0 });
|
|
396
|
+
}
|
|
397
|
+
warnUnmatchedFields(result, select, context.output, tool.outputSchema);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
175
400
|
const projectId = typeof args.projectId === 'string' ? args.projectId : context.config.projectId;
|
|
176
401
|
try {
|
|
177
|
-
|
|
402
|
+
const waited = await waitForToolResult(result, context, projectId);
|
|
403
|
+
return finish(projectResult(waited, selectIn(waited), what, context.output));
|
|
178
404
|
}
|
|
179
405
|
catch (error) {
|
|
180
|
-
if (error instanceof
|
|
181
|
-
|
|
406
|
+
if (error instanceof WaitTimeoutError && projectId)
|
|
407
|
+
error.project = projectId;
|
|
408
|
+
if (error instanceof CliApiError && error.code !== 'NO_FIELDS_MATCHED' && error.result === undefined) {
|
|
409
|
+
const kept = selectFields(result, selectIn(result));
|
|
182
410
|
const base = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : { result: kept };
|
|
183
411
|
error.result =
|
|
184
412
|
error instanceof WaitTimeoutError
|
|
@@ -188,6 +416,117 @@ async function runTool(tool, context, flags) {
|
|
|
188
416
|
throw error;
|
|
189
417
|
}
|
|
190
418
|
}
|
|
419
|
+
function saveMediaFromResult(result, tool, context, out) {
|
|
420
|
+
const record = result && typeof result === 'object' && !Array.isArray(result) ? result : {};
|
|
421
|
+
const media = (Array.isArray(record.media) ? record.media : []).filter((block) => block !== null && typeof block === 'object' && typeof block.data === 'string');
|
|
422
|
+
delete context.output.out;
|
|
423
|
+
if (media.length !== 1) {
|
|
424
|
+
printWarning('OUT_NOT_MEDIA', `${tool.name} answered ${media.length === 0 ? 'no image or audio block' : `${media.length} media blocks`}, so nothing was written to ${out}; its result is printed instead. ${tool.name} did its work: do not run it again for this. Name a .json file to save a result, or use --out-dir on a read-only tool for several media files.`, context.output);
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
const { media: _media, ...rest } = record;
|
|
428
|
+
printBytes(Buffer.from(media[0].data, 'base64'), {
|
|
429
|
+
...context.output,
|
|
430
|
+
out,
|
|
431
|
+
...(typeof media[0].mimeType === 'string' ? { contentType: media[0].mimeType } : {}),
|
|
432
|
+
meta: rest,
|
|
433
|
+
fields: selectedFields(context),
|
|
434
|
+
});
|
|
435
|
+
context.output.quiet = true;
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
function allDeclared(paths, schema) {
|
|
439
|
+
return schema !== undefined && paths.every(path => isDeclaredPath(path, schema));
|
|
440
|
+
}
|
|
441
|
+
function readableBlocks(blocks) {
|
|
442
|
+
return {
|
|
443
|
+
blocks: blocks.map(block => {
|
|
444
|
+
if (!isTextBlock(block))
|
|
445
|
+
return block;
|
|
446
|
+
const parsed = parseLabelledJson(block.text);
|
|
447
|
+
return parsed ? { type: 'text', ...(parsed.label ? { label: parsed.label } : {}), json: parsed.json } : block;
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function parseLabelledJson(text) {
|
|
452
|
+
const header = /^(Asset [A-Za-z0-9_-]+ \(\d+\/\d+\))\s*\n/.exec(text);
|
|
453
|
+
const body = header ? text.slice(header[0].length) : text;
|
|
454
|
+
if (!body.trimStart().startsWith('{'))
|
|
455
|
+
return null;
|
|
456
|
+
try {
|
|
457
|
+
const json = JSON.parse(body);
|
|
458
|
+
if (!json || typeof json !== 'object' || Array.isArray(json))
|
|
459
|
+
return null;
|
|
460
|
+
return { ...(header ? { label: header[1] } : {}), json: json };
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function unpairedTexts(blocks) {
|
|
467
|
+
const notes = [];
|
|
468
|
+
blocks.forEach((block, index) => {
|
|
469
|
+
if (!isTextBlock(block))
|
|
470
|
+
return;
|
|
471
|
+
const previous = blocks[index - 1];
|
|
472
|
+
const next = blocks[index + 1];
|
|
473
|
+
const paired = (next !== undefined && isMediaBlock(next)) ||
|
|
474
|
+
(previous !== undefined && isMediaBlock(previous) && (index < 2 || !isTextBlock(blocks[index - 2])));
|
|
475
|
+
if (!paired)
|
|
476
|
+
notes.push(block.text);
|
|
477
|
+
});
|
|
478
|
+
return notes;
|
|
479
|
+
}
|
|
480
|
+
function mediaFiles(blocks, toolName) {
|
|
481
|
+
const invalid = (message) => new CliApiError(message, { code: 'INVALID_RESPONSE', status: 0, tool: toolName });
|
|
482
|
+
const taken = new Set();
|
|
483
|
+
const files = [];
|
|
484
|
+
let text;
|
|
485
|
+
blocks.forEach((block, index) => {
|
|
486
|
+
if (isTextBlock(block)) {
|
|
487
|
+
text = block.text;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (!isMediaBlock(block))
|
|
491
|
+
return;
|
|
492
|
+
const mimeType = typeof block.mimeType === 'string' ? block.mimeType : undefined;
|
|
493
|
+
if (!mimeType || !Object.hasOwn(MEDIA_EXTENSIONS, mimeType)) {
|
|
494
|
+
throw invalid(`Block ${index} (${block.type}): missing or unknown mimeType (${mimeType ?? 'missing'}).`);
|
|
495
|
+
}
|
|
496
|
+
if (!('data' in block) || typeof block.data !== 'string')
|
|
497
|
+
throw invalid(`Block ${index} (${block.type}): no data.`);
|
|
498
|
+
const before = stemFrom(text);
|
|
499
|
+
const describing = before ? text : textAfter(blocks, index);
|
|
500
|
+
const stem = before ?? stemFrom(describing) ?? `block-${index}`;
|
|
501
|
+
let name = `${stem}.${MEDIA_EXTENSIONS[mimeType]}`;
|
|
502
|
+
for (let copy = 2; taken.has(name); copy++)
|
|
503
|
+
name = `${stem}-${copy}.${MEDIA_EXTENSIONS[mimeType]}`;
|
|
504
|
+
taken.add(name);
|
|
505
|
+
const about = describing === undefined ? null : parseLabelledJson(describing);
|
|
506
|
+
files.push({ name, mimeType, bytes: Buffer.from(block.data, 'base64'), ...(about ? { about: about.json } : {}) });
|
|
507
|
+
});
|
|
508
|
+
return files;
|
|
509
|
+
}
|
|
510
|
+
function stemFrom(text) {
|
|
511
|
+
if (text === undefined)
|
|
512
|
+
return null;
|
|
513
|
+
const header = /^Asset ([A-Za-z0-9_-]+) \((\d+)\/\d+\)/.exec(text);
|
|
514
|
+
if (header)
|
|
515
|
+
return `${header[2]}-${header[1]}`;
|
|
516
|
+
try {
|
|
517
|
+
const parsed = JSON.parse(text);
|
|
518
|
+
const assetId = parsed && typeof parsed === 'object' ? parsed.assetId : undefined;
|
|
519
|
+
if (typeof assetId === 'string' && assetId)
|
|
520
|
+
return assetId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
function textAfter(blocks, index) {
|
|
527
|
+
const next = blocks[index + 1];
|
|
528
|
+
return next && isTextBlock(next) ? next.text : undefined;
|
|
529
|
+
}
|
|
191
530
|
async function waitForToolResult(result, context, projectId) {
|
|
192
531
|
if (!result || typeof result !== 'object')
|
|
193
532
|
return result;
|
|
@@ -195,25 +534,36 @@ async function waitForToolResult(result, context, projectId) {
|
|
|
195
534
|
if (record.approvalNeeded === true || record.generated === false)
|
|
196
535
|
return result;
|
|
197
536
|
const waitOptions = { timeoutMs: timeoutMs(context), now: context.now, sleep: context.sleep };
|
|
537
|
+
const settled = (waited) => {
|
|
538
|
+
const failure = notCompleted(waited);
|
|
539
|
+
if (!failure)
|
|
540
|
+
return { ...record, waited };
|
|
541
|
+
failure.result = { ...record, waited };
|
|
542
|
+
throw failure;
|
|
543
|
+
};
|
|
198
544
|
if (typeof record.runId === 'string') {
|
|
199
|
-
return
|
|
545
|
+
return settled(handleOutcome('run', await waitForRun(context.client, record.runId, waitOptions)));
|
|
200
546
|
}
|
|
201
547
|
if (typeof record.renderId === 'string' || typeof record.exportId === 'string') {
|
|
202
548
|
const ids = {
|
|
203
549
|
...(typeof record.renderId === 'string' ? { renderId: record.renderId } : {}),
|
|
204
550
|
...(typeof record.exportId === 'string' ? { exportId: record.exportId } : {}),
|
|
205
551
|
};
|
|
206
|
-
return
|
|
552
|
+
return settled(handleOutcome('render', await waitForDelivery(context.client, ids, waitOptions)));
|
|
207
553
|
}
|
|
208
554
|
if (typeof record.jobId === 'string') {
|
|
209
|
-
return
|
|
555
|
+
return settled(handleOutcome('job', await waitForJob(context.client, record.jobId, waitOptions)));
|
|
210
556
|
}
|
|
211
557
|
if (!projectId)
|
|
212
558
|
return result;
|
|
213
559
|
const ids = producedAssetIds(result);
|
|
214
560
|
if (ids.length === 0)
|
|
215
561
|
return result;
|
|
216
|
-
return
|
|
562
|
+
return settled(assetsOutcome(await waitForAssets(context.client, projectId, ids, waitOptions)));
|
|
563
|
+
}
|
|
564
|
+
function strayPositionalError(tool, tokens) {
|
|
565
|
+
const list = flagSpecs(tool).find(spec => spec.kind === 'array');
|
|
566
|
+
return new ArgumentError(`Unexpected argument${tokens.length > 1 ? 's' : ''} ${tokens.map(token => JSON.stringify(token)).join(' ')} for ${tool.name}: a tool takes only --flags. Quote a value that contains spaces${list ? `; an array flag takes its values right after it (--${list.flag} a b)` : ''}. Run martini ${tool.name} --help.`);
|
|
217
567
|
}
|
|
218
568
|
function approvalFlags(tool, flags) {
|
|
219
569
|
const approve = flagBoolean(flags, 'approve') === true;
|
|
@@ -233,7 +583,4 @@ function approvalFlags(tool, flags) {
|
|
|
233
583
|
throw new ArgumentError('--max-olives must be a number of olives.');
|
|
234
584
|
return { support, maxOlives };
|
|
235
585
|
}
|
|
236
|
-
function knownResultFields(tool) {
|
|
237
|
-
return Object.keys(tool.outputSchema?.properties ?? {});
|
|
238
|
-
}
|
|
239
586
|
//# sourceMappingURL=cli.js.map
|