@martini-film/cli 0.3.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.
Files changed (44) hide show
  1. package/README.md +165 -44
  2. package/dist/api.js +57 -17
  3. package/dist/api.js.map +1 -1
  4. package/dist/approve.js +2 -1
  5. package/dist/approve.js.map +1 -1
  6. package/dist/cli.js +429 -144
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/assets.js +141 -25
  9. package/dist/commands/assets.js.map +1 -1
  10. package/dist/commands/call.js +99 -0
  11. package/dist/commands/call.js.map +1 -0
  12. package/dist/commands/context.js +15 -2
  13. package/dist/commands/context.js.map +1 -1
  14. package/dist/commands/files.js +100 -7
  15. package/dist/commands/files.js.map +1 -1
  16. package/dist/commands/guide.js +67 -0
  17. package/dist/commands/guide.js.map +1 -0
  18. package/dist/commands/init.js +196 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/login.js +37 -6
  21. package/dist/commands/login.js.map +1 -1
  22. package/dist/commands/tools.js +63 -78
  23. package/dist/commands/tools.js.map +1 -1
  24. package/dist/commands/use.js +143 -0
  25. package/dist/commands/use.js.map +1 -0
  26. package/dist/config.js +82 -6
  27. package/dist/config.js.map +1 -1
  28. package/dist/generated/guide.json +104 -0
  29. package/dist/generated/manifest.json +10 -2
  30. package/dist/guideCache.js +204 -0
  31. package/dist/guideCache.js.map +1 -0
  32. package/dist/help.js +375 -32
  33. package/dist/help.js.map +1 -1
  34. package/dist/manifest.js +27 -0
  35. package/dist/manifest.js.map +1 -1
  36. package/dist/manifestCache.js +196 -0
  37. package/dist/manifestCache.js.map +1 -0
  38. package/dist/output.js +210 -28
  39. package/dist/output.js.map +1 -1
  40. package/dist/schema.js +99 -14
  41. package/dist/schema.js.map +1 -1
  42. package/dist/wait.js +31 -1
  43. package/dist/wait.js.map +1 -1
  44. package/package.json +2 -2
package/dist/manifest.js CHANGED
@@ -1,10 +1,37 @@
1
1
  import manifestJson from './generated/manifest.json' with { type: 'json' };
2
+ export const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]{1,80}$/;
2
3
  export function loadManifest() {
3
4
  return manifestJson;
4
5
  }
5
6
  export function findTool(name, manifest = loadManifest()) {
6
7
  return manifest.tools.find(tool => tool.name === name);
7
8
  }
9
+ export function compareManifests(build, server) {
10
+ const built = new Map(build.tools.map(tool => [tool.name, tool]));
11
+ const served = new Map(server.tools.map(tool => [tool.name, tool]));
12
+ const sorted = (names) => [...names].sort((a, b) => a.localeCompare(b));
13
+ return {
14
+ serverOnly: sorted([...served.keys()].filter(name => !built.has(name))),
15
+ buildOnly: sorted([...built.keys()].filter(name => !served.has(name))),
16
+ changed: sorted([...served.values()]
17
+ .filter(tool => {
18
+ const local = built.get(tool.name);
19
+ return local !== undefined && canonicalJson(local.inputSchema) !== canonicalJson(tool.inputSchema);
20
+ })
21
+ .map(tool => tool.name)),
22
+ };
23
+ }
24
+ function canonicalJson(value) {
25
+ if (Array.isArray(value))
26
+ return `[${value.map(canonicalJson).join(',')}]`;
27
+ if (value && typeof value === 'object') {
28
+ const entries = Object.entries(value)
29
+ .filter(([, item]) => item !== undefined)
30
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
31
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
32
+ }
33
+ return JSON.stringify(value) ?? 'null';
34
+ }
8
35
  export function groupTools(tools) {
9
36
  const groups = new Map();
10
37
  for (const tool of tools) {
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAOA,OAAO,YAAY,MAAM,2BAA2B,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAmD1E,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAmC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,WAAqB,YAAY,EAAE;IACxE,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AACxD,CAAC;AAGD,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACf,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAClG,CAAC;AAED,MAAM,cAAc,GAA4B;IAC9C,CAAC,sBAAsB,EAAE,UAAU,CAAC;IACpC,CAAC,4BAA4B,EAAE,YAAY,CAAC;IAC5C,CAAC,uBAAuB,EAAE,QAAQ,CAAC;IACnC,CAAC,iBAAiB,EAAE,QAAQ,CAAC;IAC7B,CAAC,kEAAkE,EAAE,UAAU,CAAC;IAChF,CAAC,mCAAmC,EAAE,OAAO,CAAC;IAC9C;QACE,0GAA0G;QAC1G,WAAW;KACZ;IACD,CAAC,yBAAyB,EAAE,SAAS,CAAC;IACtC,CAAC,4BAA4B,EAAE,SAAS,CAAC;IACzC,CAAC,6BAA6B,EAAE,QAAQ,CAAC;IACzC,CAAC,+CAA+C,EAAE,YAAY,CAAC;IAC/D,CAAC,wFAAwF,EAAE,QAAQ,CAAC;IACpG,CAAC,SAAS,EAAE,UAAU,CAAC;IACvB,CAAC,6DAA6D,EAAE,QAAQ,CAAC;IACzE,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,SAAS,EAAE,UAAU,CAAC;CACxB,CAAA;AAED,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,cAAc;QAAE,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;IACnF,OAAO,OAAO,CAAA;AAChB,CAAC"}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AASA,OAAO,YAAY,MAAM,2BAA2B,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAmD1E,MAAM,CAAC,MAAM,iBAAiB,GAAG,wBAAwB,CAAA;AAGzD,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAmC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,WAAqB,YAAY,EAAE;IACxE,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AACxD,CAAC;AAgBD,MAAM,UAAU,gBAAgB,CAAC,KAAe,EAAE,MAAgB;IAChE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IACjE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IACnE,MAAM,MAAM,GAAG,CAAC,KAAuB,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACzF,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACvE,SAAS,EAAE,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACtE,OAAO,EAAE,MAAM,CACb,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;aACjB,MAAM,CAAC,IAAI,CAAC,EAAE;YACb,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAClC,OAAO,KAAK,KAAK,SAAS,IAAI,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACpG,CAAC,CAAC;aACD,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAC1B;KACF,CAAA;AACH,CAAC;AAGD,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IAC1E,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;aAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC;aACxC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACnD,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACvG,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAA;AACxC,CAAC;AAGD,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACf,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAClG,CAAC;AAED,MAAM,cAAc,GAA4B;IAC9C,CAAC,sBAAsB,EAAE,UAAU,CAAC;IACpC,CAAC,4BAA4B,EAAE,YAAY,CAAC;IAC5C,CAAC,uBAAuB,EAAE,QAAQ,CAAC;IACnC,CAAC,iBAAiB,EAAE,QAAQ,CAAC;IAC7B,CAAC,kEAAkE,EAAE,UAAU,CAAC;IAChF,CAAC,mCAAmC,EAAE,OAAO,CAAC;IAC9C;QACE,0GAA0G;QAC1G,WAAW;KACZ;IACD,CAAC,yBAAyB,EAAE,SAAS,CAAC;IACtC,CAAC,4BAA4B,EAAE,SAAS,CAAC;IACzC,CAAC,6BAA6B,EAAE,QAAQ,CAAC;IACzC,CAAC,+CAA+C,EAAE,YAAY,CAAC;IAC/D,CAAC,wFAAwF,EAAE,QAAQ,CAAC;IACpG,CAAC,SAAS,EAAE,UAAU,CAAC;IACvB,CAAC,6DAA6D,EAAE,QAAQ,CAAC;IACzE,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,SAAS,EAAE,UAAU,CAAC;CACxB,CAAA;AAED,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,cAAc;QAAE,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;IACnF,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -0,0 +1,196 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { cacheDir, isRunKey } from './config.js';
5
+ export const MANIFEST_REFRESH_ENV = 'MARTINI_MANIFEST_REFRESH';
6
+ const CACHE_FORMAT = 1;
7
+ function digest(value, length) {
8
+ return createHash('sha256').update(value).digest('hex').slice(0, length);
9
+ }
10
+ export function keyFingerprint(apiKey) {
11
+ return digest(apiKey, 16);
12
+ }
13
+ export function credentialCachePath(kind, env, apiUrl, apiKey) {
14
+ return join(cacheDir(env), `${kind}-${digest(apiUrl, 12)}-${keyFingerprint(apiKey)}.json`);
15
+ }
16
+ export function manifestCachePath(env, apiUrl, apiKey) {
17
+ return credentialCachePath('manifest', env, apiUrl, apiKey);
18
+ }
19
+ export function refreshDisabled(env) {
20
+ const value = env[MANIFEST_REFRESH_ENV]?.trim().toLowerCase();
21
+ return value === '0' || value === 'false' || value === 'no' || value === 'off';
22
+ }
23
+ export function openManifest(options) {
24
+ const { baked, apiUrl, apiKey, env, warn } = options;
25
+ const now = options.now ?? Date.now;
26
+ const runKey = isRunKey(apiKey);
27
+ const cachePath = apiKey && !runKey ? manifestCachePath(env, apiUrl, apiKey) : null;
28
+ const fingerprint = apiKey ? keyFingerprint(apiKey) : '';
29
+ let cached = cachePath ? readCache(cachePath, apiUrl, fingerprint, warn) : null;
30
+ let bakedConfirmed = false;
31
+ return {
32
+ get state() {
33
+ if (cached) {
34
+ return {
35
+ manifest: cached.manifest,
36
+ source: 'cache',
37
+ baked,
38
+ server: cached.manifest,
39
+ fetchedAt: cached.fetchedAt,
40
+ cachePath,
41
+ };
42
+ }
43
+ return { manifest: baked, source: 'baked', baked, server: bakedConfirmed ? baked : null, cachePath };
44
+ },
45
+ async refresh({ force = false } = {}) {
46
+ if (runKey)
47
+ return 'skipped';
48
+ if (refreshDisabled(env)) {
49
+ if (force) {
50
+ warn('MANIFEST_REFRESH_DISABLED', `${MANIFEST_REFRESH_ENV}=${env[MANIFEST_REFRESH_ENV]} turns refreshes off, so the tool list was not fetched. Unset it to refresh.`);
51
+ }
52
+ return 'skipped';
53
+ }
54
+ const inUse = cached ? `the cached list from ${cached.fetchedAt}` : "this build's list";
55
+ let answer;
56
+ try {
57
+ answer = await options.fetchManifest(force ? undefined : (cached?.manifest.hash ?? baked.hash));
58
+ }
59
+ catch (error) {
60
+ warn('MANIFEST_REFRESH_FAILED', `Could not refresh the tool list from ${apiUrl} (${messageOf(error)}); commands use ${inUse}.`);
61
+ return 'failed';
62
+ }
63
+ if (answer.notModified) {
64
+ if (!cached)
65
+ bakedConfirmed = true;
66
+ return 'unchanged';
67
+ }
68
+ const manifest = usableManifest(answer.body, answer.etag);
69
+ if (!manifest) {
70
+ warn('MANIFEST_REFRESH_FAILED', `The server answered GET /v1/tools without a usable tool list; commands use ${inUse}.`);
71
+ return 'failed';
72
+ }
73
+ cached = {
74
+ format: CACHE_FORMAT,
75
+ apiUrl,
76
+ keyFingerprint: fingerprint,
77
+ fetchedAt: new Date(now()).toISOString(),
78
+ manifest,
79
+ };
80
+ if (cachePath)
81
+ writeCache(cachePath, cached, warn);
82
+ return 'updated';
83
+ },
84
+ };
85
+ }
86
+ function isRecord(value) {
87
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
88
+ }
89
+ function isUsableSchema(schema, depth = 0) {
90
+ if (!isRecord(schema))
91
+ return false;
92
+ if (depth > 16)
93
+ return true;
94
+ const { properties, required, items, additionalProperties } = schema;
95
+ if (properties !== undefined &&
96
+ (!isRecord(properties) || !Object.values(properties).every(child => isUsableSchema(child, depth + 1))))
97
+ return false;
98
+ if (required !== undefined && (!Array.isArray(required) || !required.every(item => typeof item === 'string')))
99
+ return false;
100
+ if (items !== undefined && !isUsableSchema(items, depth + 1))
101
+ return false;
102
+ if (isRecord(additionalProperties) && !isUsableSchema(additionalProperties, depth + 1))
103
+ return false;
104
+ for (const key of ['anyOf', 'oneOf', 'allOf']) {
105
+ const variants = schema[key];
106
+ if (variants !== undefined &&
107
+ (!Array.isArray(variants) || !variants.every(child => isUsableSchema(child, depth + 1))))
108
+ return false;
109
+ }
110
+ return true;
111
+ }
112
+ function isUsableTool(tool) {
113
+ if (!isRecord(tool) || typeof tool.name !== 'string' || typeof tool.description !== 'string')
114
+ return false;
115
+ if (!isRecord(tool.annotations))
116
+ return false;
117
+ return isUsableSchema(tool.inputSchema);
118
+ }
119
+ function usableManifest(body, etag) {
120
+ if (!isRecord(body))
121
+ return null;
122
+ const hash = typeof body.hash === 'string' && body.hash ? body.hash : etag;
123
+ if (!hash || !Array.isArray(body.tools) || body.tools.length === 0 || !body.tools.every(isUsableTool))
124
+ return null;
125
+ return {
126
+ version: 1,
127
+ hash,
128
+ generatedAt: typeof body.generatedAt === 'string' ? body.generatedAt : '',
129
+ tools: body.tools,
130
+ };
131
+ }
132
+ function messageOf(error) {
133
+ return error instanceof Error ? error.message : String(error);
134
+ }
135
+ function readCache(path, apiUrl, fingerprint, warn) {
136
+ let text;
137
+ try {
138
+ text = readFileSync(path, 'utf8');
139
+ }
140
+ catch (error) {
141
+ const code = error.code;
142
+ if (code === 'ENOENT' || code === 'ENOTDIR')
143
+ return null;
144
+ warn('MANIFEST_CACHE_IGNORED', `Could not read the cached tool list ${path} (${messageOf(error)}); commands use this build's list.`);
145
+ return null;
146
+ }
147
+ let entry;
148
+ try {
149
+ entry = JSON.parse(text);
150
+ }
151
+ catch {
152
+ entry = null;
153
+ }
154
+ if (isRecord(entry) && entry.format !== undefined && entry.format !== CACHE_FORMAT)
155
+ return null;
156
+ const manifest = isRecord(entry) ? usableManifest(entry.manifest, null) : null;
157
+ if (!isRecord(entry) ||
158
+ !manifest ||
159
+ entry.apiUrl !== apiUrl ||
160
+ entry.keyFingerprint !== fingerprint ||
161
+ typeof entry.fetchedAt !== 'string') {
162
+ try {
163
+ rmSync(path, { force: true });
164
+ }
165
+ catch {
166
+ }
167
+ warn('MANIFEST_CACHE_IGNORED', `The cached tool list ${path} was not valid and has been removed; commands use this build's list until the next refresh.`);
168
+ return null;
169
+ }
170
+ return { format: CACHE_FORMAT, apiUrl, keyFingerprint: fingerprint, fetchedAt: entry.fetchedAt, manifest };
171
+ }
172
+ export function writePrivateFileAtomic(path, text) {
173
+ const temp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
174
+ try {
175
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
176
+ writeFileSync(temp, text, { mode: 0o600 });
177
+ renameSync(temp, path);
178
+ }
179
+ catch (error) {
180
+ try {
181
+ rmSync(temp, { force: true });
182
+ }
183
+ catch {
184
+ }
185
+ throw error;
186
+ }
187
+ }
188
+ function writeCache(path, entry, warn) {
189
+ try {
190
+ writePrivateFileAtomic(path, JSON.stringify(entry));
191
+ }
192
+ catch (error) {
193
+ warn('MANIFEST_CACHE_WRITE_FAILED', `Could not save the tool list to ${path} (${messageOf(error)}); only this command uses it.`);
194
+ }
195
+ }
196
+ //# sourceMappingURL=manifestCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifestCache.js","sourceRoot":"","sources":["../src/manifestCache.ts"],"names":[],"mappings":"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"}
package/dist/output.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { writeFileSync } from 'node:fs';
2
- import { CliApiError } from './api.js';
2
+ import { extname } from 'node:path';
3
+ import { CliApiError, isMediaBlock as isMediaContentBlock } from './api.js';
3
4
  import { ArgumentError } from './schema.js';
4
5
  import { WaitTimeoutError } from './wait.js';
5
6
  export const EXIT = {
@@ -19,11 +20,17 @@ function writeError(options, text) {
19
20
  (options.stderr ?? (line => process.stderr.write(line)))(text);
20
21
  }
21
22
  export function printResult(value, options = {}) {
23
+ if (options.out && isMediaOutPath(options.out)) {
24
+ const { out, ...rest } = options;
25
+ printWarning('OUT_NOT_MEDIA', `This result is JSON, not media, so nothing was written to ${out}; it is printed here.`, rest);
26
+ printResult(value, rest);
27
+ return;
28
+ }
22
29
  if (options.out) {
23
30
  const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2) + '\n';
24
31
  writeFileSync(options.out, text);
25
- if (!options.quiet)
26
- write(options, `${summarize(value)} → ${options.out}\n`);
32
+ const { out: _out, ...receiptOptions } = options;
33
+ printResult({ path: options.out, bytes: Buffer.byteLength(text), summary: summarize(value) }, receiptOptions);
27
34
  return;
28
35
  }
29
36
  if (options.quiet)
@@ -36,7 +43,43 @@ export function printResult(value, options = {}) {
36
43
  const printable = withoutMediaBytes(value);
37
44
  write(options, (tty ? JSON.stringify(printable, null, 2) : JSON.stringify(printable)) + '\n');
38
45
  }
39
- export const MEDIA_BYTES_HINT = 'The bytes are not printed. Write them with --out <path> plus --raw (a result with one media block) or --out-dir <dir> (every block, read-only tools); --out <path> alone writes this JSON with the base64 kept.';
46
+ export const MEDIA_BYTES_HINT = 'The bytes are not printed. Save them with --out <file.jpg|.png|.webp|.mp3|.wav> (a result with one media block; --raw does the same for any file name) or --out-dir <dir> (every block, read-only tools); --out <file.json> writes this JSON with the base64 kept.';
47
+ export const MEDIA_EXTENSIONS = {
48
+ 'image/jpeg': 'jpg',
49
+ 'image/png': 'png',
50
+ 'image/webp': 'webp',
51
+ 'audio/mpeg': 'mp3',
52
+ 'audio/wav': 'wav',
53
+ };
54
+ const MEDIA_OUT_EXTENSIONS = new Set([
55
+ 'jpg',
56
+ 'jpeg',
57
+ 'png',
58
+ 'webp',
59
+ 'gif',
60
+ 'mp3',
61
+ 'wav',
62
+ 'm4a',
63
+ 'aac',
64
+ 'flac',
65
+ 'ogg',
66
+ 'mp4',
67
+ 'mov',
68
+ 'webm',
69
+ ]);
70
+ function extensionOf(path) {
71
+ return extname(path).slice(1).toLowerCase();
72
+ }
73
+ export function isMediaOutPath(path) {
74
+ return MEDIA_OUT_EXTENSIONS.has(extensionOf(path));
75
+ }
76
+ export function mismatchedExtension(path, contentType) {
77
+ const expected = MEDIA_EXTENSIONS[contentType.split(';')[0].trim().toLowerCase()];
78
+ const actual = extensionOf(path);
79
+ if (!expected || !MEDIA_OUT_EXTENSIONS.has(actual))
80
+ return null;
81
+ return actual === expected || (expected === 'jpg' && actual === 'jpeg') ? null : expected;
82
+ }
40
83
  function isMediaBlock(block) {
41
84
  return block !== null && typeof block === 'object' && typeof block.data === 'string';
42
85
  }
@@ -61,6 +104,11 @@ function withoutMediaBytes(value) {
61
104
  : block),
62
105
  };
63
106
  }
107
+ export function noteMediaStubs(blocks, options = {}) {
108
+ if (blocks.some(block => isMediaContentBlock(block) && !('data' in block))) {
109
+ printWarning('MEDIA_NOT_PRINTED', MEDIA_BYTES_HINT, options);
110
+ }
111
+ }
64
112
  export function assertRawDestination(options) {
65
113
  if (options.out)
66
114
  return;
@@ -71,8 +119,19 @@ export function assertRawDestination(options) {
71
119
  export function printBytes(bytes, options) {
72
120
  if (options.out) {
73
121
  writeFileSync(options.out, bytes);
74
- if (!options.quiet)
75
- write(options, `${bytes.byteLength} bytes${options.contentType ? ` (${options.contentType})` : ''} → ${options.out}\n`);
122
+ const expected = options.contentType ? mismatchedExtension(options.out, options.contentType) : null;
123
+ if (expected) {
124
+ printWarning('EXTENSION_MISMATCH', `${options.out} holds ${options.contentType} bytes; a viewer that trusts the extension may refuse it. Name the file .${expected}.`, options);
125
+ }
126
+ const { out: _out, contentType, meta, fields, ...receiptOptions } = options;
127
+ const receipt = {
128
+ path: options.out,
129
+ bytes: bytes.byteLength,
130
+ ...(contentType ? { contentType } : {}),
131
+ ...(meta !== null && meta !== undefined ? { result: meta } : {}),
132
+ };
133
+ const trimmed = fields?.length ? selectFields(receipt, fields) : receipt;
134
+ printResult(fields?.length && isEmptyProjection(trimmed) ? receipt : trimmed, receiptOptions);
76
135
  return;
77
136
  }
78
137
  assertRawDestination(options);
@@ -83,21 +142,121 @@ export function printWarning(code, message, options = {}) {
83
142
  return;
84
143
  writeError(options, JSON.stringify({ warning: { code, message } }) + '\n');
85
144
  }
86
- export function warnNoFieldsMatched(result, fields, options = {}, knownFields = []) {
87
- if (fields.length === 0 || options.quiet)
88
- return;
89
- const empty = result !== null && typeof result === 'object' && Object.keys(result).length === 0;
90
- if (!empty)
91
- return;
145
+ export function isEmptyProjection(result) {
146
+ if (result === null || typeof result !== 'object')
147
+ return false;
148
+ return Object.keys(result).length === 0 || !holdsValue(result);
149
+ }
150
+ export function unmatchedFields(result, fields) {
151
+ return fields.filter(path => !holdsValue(project(result, path.split('.'))));
152
+ }
153
+ function holdsValue(value) {
154
+ if (value === undefined)
155
+ return false;
156
+ if (Array.isArray(value))
157
+ return value.length === 0 || value.some(item => item !== null && holdsValue(item));
158
+ if (value !== null && typeof value === 'object') {
159
+ const values = Object.values(value);
160
+ return values.length === 0 || values.some(holdsValue);
161
+ }
162
+ return true;
163
+ }
164
+ export function noFieldsMatchedMessage(fields, knownFields = []) {
92
165
  const shape = knownFields.length
93
- ? `The result's top-level fields are: ${knownFields.join(', ')} (martini <tool> --help lists the nested ones).`
166
+ ? `The result's top-level fields are: ${knownFields.join(', ')} (--help on this command lists the nested ones).`
94
167
  : 'Run once without --fields to see the shape.';
95
- writeError(options, JSON.stringify({
96
- warning: {
97
- code: 'NO_FIELDS_MATCHED',
98
- message: `--fields ${fields.join(',')} matched nothing in the result. Paths are dotted and case-sensitive (asset.imageUrl, nodes.nodeId). ${shape}`,
99
- },
100
- }) + '\n');
168
+ return `--fields ${fields.join(',')} matched nothing in the result. Paths are dotted and case-sensitive (asset.imageUrl, nodes.nodeId); a field that is a map keyed by id takes a * step, quoted for the shell ('assets.*.status'). ${shape}`;
169
+ }
170
+ export function warnUnmatchedFields(result, fields, options = {}, schema) {
171
+ if (fields.length < 2)
172
+ return;
173
+ const unmatched = unmatchedFields(result, fields);
174
+ const missing = unmatched.filter(path => !isDeclaredPath(path, schema));
175
+ if (missing.length === 0 || unmatched.length === fields.length)
176
+ return;
177
+ printWarning('FIELDS_NOT_FOUND', `--fields ${missing.join(',')} matched nothing in the result; the other paths are printed. Check the spelling; --help on this command lists the result fields it declares.`, options);
178
+ }
179
+ function walkMapPath(path, schema) {
180
+ const out = [];
181
+ const everyRecord = [];
182
+ let undeclaredMapStep = false;
183
+ let node = schema;
184
+ for (const segment of path.split('.')) {
185
+ while (node && node.type === 'array' && node.items)
186
+ node = node.items;
187
+ if (node && segment !== '*' && isIdKeyedMap(node)) {
188
+ let values = node.additionalProperties;
189
+ while (values && values.type === 'array' && values.items)
190
+ values = values.items;
191
+ everyRecord.push('*');
192
+ if (values?.properties?.[segment] === undefined) {
193
+ undeclaredMapStep = true;
194
+ out.push(segment);
195
+ everyRecord.push(segment);
196
+ node = values;
197
+ continue;
198
+ }
199
+ out.push('*');
200
+ node = values;
201
+ }
202
+ out.push(segment);
203
+ everyRecord.push(segment);
204
+ if (!node)
205
+ continue;
206
+ node =
207
+ segment === '*'
208
+ ? typeof node.additionalProperties === 'object'
209
+ ? node.additionalProperties
210
+ : undefined
211
+ : node.properties?.[segment];
212
+ }
213
+ return { path: out.join('.'), everyRecord: everyRecord.join('.'), undeclaredMapStep };
214
+ }
215
+ export function expandMapPaths(paths, schema) {
216
+ if (!schema)
217
+ return [...paths];
218
+ return paths.map(path => walkMapPath(path, schema).path);
219
+ }
220
+ export function hasUndeclaredMapStep(paths, schema) {
221
+ return schema !== undefined && paths.some(path => walkMapPath(path, schema).undeclaredMapStep);
222
+ }
223
+ export function resolveMapPaths(result, paths, schema) {
224
+ if (!schema)
225
+ return [...paths];
226
+ return paths.map(path => {
227
+ const walked = walkMapPath(path, schema);
228
+ if (!walked.undeclaredMapStep)
229
+ return walked.path;
230
+ return isEmptyProjection(selectFields(result, [walked.path])) ? walked.everyRecord : walked.path;
231
+ });
232
+ }
233
+ export function isDeclaredPath(path, schema) {
234
+ let node = schema;
235
+ for (const segment of path.split('.')) {
236
+ while (node && node.type === 'array' && node.items)
237
+ node = node.items;
238
+ if (!node)
239
+ return false;
240
+ const variants = node.anyOf ?? node.oneOf ?? [node];
241
+ const next = variants
242
+ .map((variant) => {
243
+ if (segment === '*')
244
+ return typeof variant.additionalProperties === 'object' ? variant.additionalProperties : undefined;
245
+ return variant.properties?.[segment];
246
+ })
247
+ .find(found => found !== undefined);
248
+ if (next === undefined)
249
+ return false;
250
+ node = next;
251
+ }
252
+ return true;
253
+ }
254
+ function isIdKeyedMap(schema) {
255
+ const values = schema.additionalProperties;
256
+ return (Object.keys(schema.properties ?? {}).length === 0 &&
257
+ values !== null &&
258
+ typeof values === 'object' &&
259
+ Object.keys(values.properties ?? {}).length > 0);
101
260
  }
102
261
  export function selectFields(value, paths) {
103
262
  if (paths.length === 0)
@@ -128,13 +287,20 @@ function project(value, segments) {
128
287
  }
129
288
  return Object.keys(out).length ? out : undefined;
130
289
  }
131
- if (!Object.prototype.hasOwnProperty.call(value, head))
132
- return undefined;
290
+ if (!Object.prototype.hasOwnProperty.call(value, head)) {
291
+ return isIdKeyedData(value) ? project(value, ['*', ...segments]) : undefined;
292
+ }
133
293
  const inner = project(value[head], rest);
134
294
  return inner === undefined ? undefined : { [head]: inner };
135
295
  }
136
296
  return undefined;
137
297
  }
298
+ const ID_KEY = /^(?:[a-z]+-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
299
+ function isIdKeyedData(value) {
300
+ const entries = Object.entries(value);
301
+ return (entries.length > 0 &&
302
+ entries.every(([key, item]) => ID_KEY.test(key) && item !== null && typeof item === 'object' && !Array.isArray(item)));
303
+ }
138
304
  function merge(a, b) {
139
305
  if (a === undefined)
140
306
  return b;
@@ -179,7 +345,11 @@ export function exitCodeFor(error) {
179
345
  /^(INVALID_API_KEY|API_KEY_REVOKED|API_KEY_EXPIRED|API_KEY_DEFAULTS_MISSING)$/.test(error.code)) {
180
346
  return EXIT.auth;
181
347
  }
182
- if (error.code === 'INSUFFICIENT_CREDITS' || error.code === 'INSUFFICIENT_OLIVES' || error.status === 402) {
348
+ if (error.code === 'INSUFFICIENT_CREDITS' ||
349
+ error.code === 'INSUFFICIENT_OLIVES' ||
350
+ error.status === 402 ||
351
+ error.code === 'PLAN_OVER_BUDGET' ||
352
+ error.code === 'PLAN_UNPRICED') {
183
353
  return EXIT.insufficientCredits;
184
354
  }
185
355
  if (/_NOT_READY$/.test(error.code) ||
@@ -187,7 +357,10 @@ export function exitCodeFor(error) {
187
357
  error.code === 'RUN_NOT_ACTIVE' ||
188
358
  error.code === 'LOGIN_PENDING')
189
359
  return EXIT.notReady;
190
- if (error.code === 'INVALID_ARGUMENTS' || error.code === 'INVALID_REQUEST' || error.code === 'TOOL_NOT_FOUND') {
360
+ if (error.code === 'INVALID_ARGUMENTS' ||
361
+ error.code === 'INVALID_REQUEST' ||
362
+ error.code === 'TOOL_NOT_FOUND' ||
363
+ error.code === 'NO_FIELDS_MATCHED') {
191
364
  return EXIT.usage;
192
365
  }
193
366
  return EXIT.toolError;
@@ -196,6 +369,8 @@ export function exitCodeFor(error) {
196
369
  }
197
370
  const RUN_CREDENTIAL_GONE_HINT = 'This execution’s run credential is expired or revoked. Stop and report it; do not run martini login. Retrying or resuming the run provisions a new one.';
198
371
  export function hintFor(error, context = {}) {
372
+ if (error instanceof CliApiError && error.hint)
373
+ return error.hint;
199
374
  if (error instanceof WaitTimeoutError) {
200
375
  return `Still running; nothing was lost. Resume with: ${error.resumeCommand()} — give --timeout the seconds your shell step allows (a video generation takes minutes).`;
201
376
  }
@@ -224,6 +399,10 @@ export function hintFor(error, context = {}) {
224
399
  : 'Run keys work only while their workflow run is executing.';
225
400
  case 'INSUFFICIENT_CREDITS':
226
401
  return 'Top up olives in Martini, or lower the batch.';
402
+ case 'PLAN_OVER_BUDGET':
403
+ return 'The quote is on stdout. Ask the person before raising --max-olives, or make the batch smaller.';
404
+ case 'NO_FIELDS_MATCHED':
405
+ return 'Nothing was changed; run the command again with paths the result has.';
227
406
  case 'FFPROBE_MISSING':
228
407
  return 'Install ffmpeg (it ships ffprobe) and make sure it is on PATH.';
229
408
  default:
@@ -232,12 +411,17 @@ export function hintFor(error, context = {}) {
232
411
  return undefined;
233
412
  }
234
413
  }
235
- if (error instanceof ArgumentError)
236
- return 'Run martini <command> --help.';
414
+ if (error instanceof ArgumentError) {
415
+ return error.hint ?? (context.command ? `Run martini ${context.command} --help.` : 'Run martini --help.');
416
+ }
237
417
  return undefined;
238
418
  }
239
419
  export function printError(error, options = {}) {
240
420
  const code = exitCodeFor(error);
421
+ const hint = hintFor(error, {
422
+ inRun: options.inRun ?? false,
423
+ ...(options.command ? { command: options.command } : {}),
424
+ });
241
425
  const payload = {
242
426
  error: {
243
427
  code: error instanceof CliApiError ? error.code : error instanceof ArgumentError ? 'USAGE' : 'CLI_ERROR',
@@ -245,9 +429,7 @@ export function printError(error, options = {}) {
245
429
  ...(error instanceof CliApiError && error.tool ? { tool: error.tool } : {}),
246
430
  ...(error instanceof WaitTimeoutError ? { pending: error.pending } : {}),
247
431
  ...(error instanceof WaitTimeoutError && error.settled.length ? { settled: error.settled } : {}),
248
- ...(hintFor(error, { inRun: options.inRun ?? false })
249
- ? { hint: hintFor(error, { inRun: options.inRun ?? false }) }
250
- : {}),
432
+ ...(hint ? { hint } : {}),
251
433
  exitCode: code,
252
434
  },
253
435
  };