@orchestraworks/cli 0.8.0 → 0.9.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 (117) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts/prelude.js +416 -88
  3. package/dist/cli/api/execute.d.ts +2 -5
  4. package/dist/cli/api/execute.js +67 -40
  5. package/dist/cli/api/raw.d.ts +3 -1
  6. package/dist/cli/api/raw.js +33 -6
  7. package/dist/cli/api/register.js +6 -1
  8. package/dist/cli/preview.d.ts +5 -4
  9. package/dist/cli/preview.js +15 -9
  10. package/dist/cli/upload.d.ts +26 -0
  11. package/dist/cli/upload.js +64 -16
  12. package/dist/generated/cli-commands.d.ts +5 -0
  13. package/dist/generated/cli-commands.json +31100 -17245
  14. package/dist/generated/cli-dispatch.js +99 -0
  15. package/dist/generated/managed-skills.js +1 -0
  16. package/node_modules/@orchestraworks/client/dist/apis/ArtifactsApi.d.ts +3 -3
  17. package/node_modules/@orchestraworks/client/dist/apis/ArtifactsApi.js +2 -2
  18. package/node_modules/@orchestraworks/client/dist/apis/FilesApi.d.ts +292 -2
  19. package/node_modules/@orchestraworks/client/dist/apis/FilesApi.js +988 -30
  20. package/node_modules/@orchestraworks/client/dist/esm/apis/ArtifactsApi.d.ts +3 -3
  21. package/node_modules/@orchestraworks/client/dist/esm/apis/ArtifactsApi.js +2 -2
  22. package/node_modules/@orchestraworks/client/dist/esm/apis/FilesApi.d.ts +292 -2
  23. package/node_modules/@orchestraworks/client/dist/esm/apis/FilesApi.js +986 -28
  24. package/node_modules/@orchestraworks/client/dist/esm/models/Artifact.d.ts +12 -53
  25. package/node_modules/@orchestraworks/client/dist/esm/models/Artifact.js +7 -29
  26. package/node_modules/@orchestraworks/client/dist/esm/models/Comment.d.ts +2 -2
  27. package/node_modules/@orchestraworks/client/dist/esm/models/Comment.js +3 -3
  28. package/node_modules/@orchestraworks/client/dist/esm/models/CommentSummary.d.ts +2 -2
  29. package/node_modules/@orchestraworks/client/dist/esm/models/CommentSummary.js +3 -3
  30. package/node_modules/@orchestraworks/client/dist/esm/models/CopyFileRequest.d.ts +38 -0
  31. package/node_modules/@orchestraworks/client/dist/esm/models/CopyFileRequest.js +47 -0
  32. package/node_modules/@orchestraworks/client/dist/esm/models/CreateArtifactRequest.d.ts +7 -7
  33. package/node_modules/@orchestraworks/client/dist/esm/models/CreateAuthHandoffRequest.d.ts +1 -1
  34. package/node_modules/@orchestraworks/client/dist/esm/models/CreateFileRequest.d.ts +44 -2
  35. package/node_modules/@orchestraworks/client/dist/esm/models/CreateFileRequest.js +14 -0
  36. package/node_modules/@orchestraworks/client/dist/esm/models/CreateRoomTokenRequest.d.ts +1 -1
  37. package/node_modules/@orchestraworks/client/dist/esm/models/FileMatch.d.ts +38 -0
  38. package/node_modules/@orchestraworks/client/dist/esm/models/FileMatch.js +47 -0
  39. package/node_modules/@orchestraworks/client/dist/esm/models/FileVersion.d.ts +101 -0
  40. package/node_modules/@orchestraworks/client/dist/esm/models/FileVersion.js +83 -0
  41. package/node_modules/@orchestraworks/client/dist/esm/models/ModelFile.d.ts +69 -0
  42. package/node_modules/@orchestraworks/client/dist/esm/models/ModelFile.js +36 -0
  43. package/node_modules/@orchestraworks/client/dist/esm/models/PageArtifact.d.ts +51 -0
  44. package/node_modules/@orchestraworks/client/dist/esm/models/{PageArtifactSummary.js → PageArtifact.js} +11 -11
  45. package/node_modules/@orchestraworks/client/dist/esm/models/PageFileVersion.d.ts +51 -0
  46. package/node_modules/@orchestraworks/client/dist/esm/models/PageFileVersion.js +56 -0
  47. package/node_modules/@orchestraworks/client/dist/esm/models/RoomTokenRoom.d.ts +2 -2
  48. package/node_modules/@orchestraworks/client/dist/esm/models/RoomTokenRoom.js +2 -2
  49. package/node_modules/@orchestraworks/client/dist/esm/models/RoomTokenRoomRequest.d.ts +3 -3
  50. package/node_modules/@orchestraworks/client/dist/esm/models/RoomTokenRoomRequest.js +2 -2
  51. package/node_modules/@orchestraworks/client/dist/esm/models/SelectorAnchor.d.ts +24 -0
  52. package/node_modules/@orchestraworks/client/dist/esm/models/SelectorAnchor.js +8 -0
  53. package/node_modules/@orchestraworks/client/dist/esm/models/SnapshotAnchor.d.ts +1 -1
  54. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateArtifactRequest.d.ts +0 -8
  55. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateArtifactRequest.js +0 -2
  56. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateFileRequest.d.ts +40 -0
  57. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateFileRequest.js +43 -0
  58. package/node_modules/@orchestraworks/client/dist/esm/models/WorkItem.d.ts +3 -3
  59. package/node_modules/@orchestraworks/client/dist/esm/models/WorkItem.js +3 -3
  60. package/node_modules/@orchestraworks/client/dist/esm/models/WriteFileDataRequest.d.ts +50 -0
  61. package/node_modules/@orchestraworks/client/dist/esm/models/WriteFileDataRequest.js +51 -0
  62. package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +7 -3
  63. package/node_modules/@orchestraworks/client/dist/esm/models/index.js +7 -3
  64. package/node_modules/@orchestraworks/client/dist/models/Artifact.d.ts +12 -53
  65. package/node_modules/@orchestraworks/client/dist/models/Artifact.js +8 -30
  66. package/node_modules/@orchestraworks/client/dist/models/Comment.d.ts +2 -2
  67. package/node_modules/@orchestraworks/client/dist/models/Comment.js +3 -3
  68. package/node_modules/@orchestraworks/client/dist/models/CommentSummary.d.ts +2 -2
  69. package/node_modules/@orchestraworks/client/dist/models/CommentSummary.js +3 -3
  70. package/node_modules/@orchestraworks/client/dist/models/CopyFileRequest.d.ts +38 -0
  71. package/node_modules/@orchestraworks/client/dist/models/CopyFileRequest.js +54 -0
  72. package/node_modules/@orchestraworks/client/dist/models/CreateArtifactRequest.d.ts +7 -7
  73. package/node_modules/@orchestraworks/client/dist/models/CreateAuthHandoffRequest.d.ts +1 -1
  74. package/node_modules/@orchestraworks/client/dist/models/CreateFileRequest.d.ts +44 -2
  75. package/node_modules/@orchestraworks/client/dist/models/CreateFileRequest.js +14 -0
  76. package/node_modules/@orchestraworks/client/dist/models/CreateRoomTokenRequest.d.ts +1 -1
  77. package/node_modules/@orchestraworks/client/dist/models/FileMatch.d.ts +38 -0
  78. package/node_modules/@orchestraworks/client/dist/models/FileMatch.js +54 -0
  79. package/node_modules/@orchestraworks/client/dist/models/FileVersion.d.ts +101 -0
  80. package/node_modules/@orchestraworks/client/dist/models/FileVersion.js +91 -0
  81. package/node_modules/@orchestraworks/client/dist/models/ModelFile.d.ts +69 -0
  82. package/node_modules/@orchestraworks/client/dist/models/ModelFile.js +37 -1
  83. package/node_modules/@orchestraworks/client/dist/models/PageArtifact.d.ts +51 -0
  84. package/node_modules/@orchestraworks/client/dist/models/{PageArtifactSummary.js → PageArtifact.js} +16 -16
  85. package/node_modules/@orchestraworks/client/dist/models/PageFileVersion.d.ts +51 -0
  86. package/node_modules/@orchestraworks/client/dist/models/PageFileVersion.js +63 -0
  87. package/node_modules/@orchestraworks/client/dist/models/RoomTokenRoom.d.ts +2 -2
  88. package/node_modules/@orchestraworks/client/dist/models/RoomTokenRoom.js +2 -2
  89. package/node_modules/@orchestraworks/client/dist/models/RoomTokenRoomRequest.d.ts +3 -3
  90. package/node_modules/@orchestraworks/client/dist/models/RoomTokenRoomRequest.js +2 -2
  91. package/node_modules/@orchestraworks/client/dist/models/SelectorAnchor.d.ts +24 -0
  92. package/node_modules/@orchestraworks/client/dist/models/SelectorAnchor.js +8 -0
  93. package/node_modules/@orchestraworks/client/dist/models/SnapshotAnchor.d.ts +1 -1
  94. package/node_modules/@orchestraworks/client/dist/models/UpdateArtifactRequest.d.ts +0 -8
  95. package/node_modules/@orchestraworks/client/dist/models/UpdateArtifactRequest.js +0 -2
  96. package/node_modules/@orchestraworks/client/dist/models/UpdateFileRequest.d.ts +40 -0
  97. package/node_modules/@orchestraworks/client/dist/models/UpdateFileRequest.js +50 -0
  98. package/node_modules/@orchestraworks/client/dist/models/WorkItem.d.ts +3 -3
  99. package/node_modules/@orchestraworks/client/dist/models/WorkItem.js +3 -3
  100. package/node_modules/@orchestraworks/client/dist/models/WriteFileDataRequest.d.ts +50 -0
  101. package/node_modules/@orchestraworks/client/dist/models/WriteFileDataRequest.js +58 -0
  102. package/node_modules/@orchestraworks/client/dist/models/index.d.ts +7 -3
  103. package/node_modules/@orchestraworks/client/dist/models/index.js +7 -3
  104. package/node_modules/@orchestraworks/client/package.json +1 -1
  105. package/package.json +4 -4
  106. package/templates/engineering-team/package.json +1 -1
  107. package/templates/todo-factory/package.json +1 -1
  108. package/node_modules/@orchestraworks/client/dist/esm/models/ArtifactSummary.d.ts +0 -139
  109. package/node_modules/@orchestraworks/client/dist/esm/models/ArtifactSummary.js +0 -95
  110. package/node_modules/@orchestraworks/client/dist/esm/models/ArtifactVersion.d.ts +0 -110
  111. package/node_modules/@orchestraworks/client/dist/esm/models/ArtifactVersion.js +0 -77
  112. package/node_modules/@orchestraworks/client/dist/esm/models/PageArtifactSummary.d.ts +0 -51
  113. package/node_modules/@orchestraworks/client/dist/models/ArtifactSummary.d.ts +0 -139
  114. package/node_modules/@orchestraworks/client/dist/models/ArtifactSummary.js +0 -103
  115. package/node_modules/@orchestraworks/client/dist/models/ArtifactVersion.d.ts +0 -110
  116. package/node_modules/@orchestraworks/client/dist/models/ArtifactVersion.js +0 -84
  117. package/node_modules/@orchestraworks/client/dist/models/PageArtifactSummary.d.ts +0 -51
@@ -13,7 +13,7 @@ import { findCommand } from "../../generated/cli-commands.js";
13
13
  import { CLI_DISPATCH } from "../../generated/cli-dispatch.js";
14
14
  import { authFlagsFrom, platformTargetFrom, resolveAnonymousBaseUrl, resolveAuthWithWarnings, } from "../core/auth.js";
15
15
  import { readCredentialFromCommandLine, readCredentialFromEnv } from "../core/credential-input.js";
16
- import { contentSourceFieldsIn, uploadFile } from "../upload.js";
16
+ import { contentSourceFieldsIn, inlineFileOf, storeInlineFile, uploadFile } from "../upload.js";
17
17
  import { readBodyDocument } from "../core/invocation.js";
18
18
  import { defaultIo, parseOutputMode, printResponse, printWarnings, } from "../core/output.js";
19
19
  import { createPlatformApis } from "../core/platform-client.js";
@@ -24,25 +24,6 @@ import { ensureWriteApproved, IMPERATIVE_APPROVAL_DEFAULT, managedResourceWarnin
24
24
  const ANONYMOUS_NOUN = "health";
25
25
  /** The item mutations that pre-read for the managed marker: neither carries it in request or response. */
26
26
  const MANAGED_WARNING_VERBS = new Set(["update", "delete"]);
27
- /**
28
- * Operations whose answer is a redirect to the bytes themselves. `fetch` follows it, so what this executor
29
- * reads back IS the content — but the operation declares a body of its own nowhere, and without this the
30
- * shared reader treats an unvouched byte body as material to withhold and prints a sentinel instead of the
31
- * file. Hand-maintained: every entry is an operation whose success response is a bare-content redirect.
32
- */
33
- const BARE_CONTENT_OPERATIONS = new Set(["getFileContent"]);
34
- /** What those operations effectively answer: the content itself, which streams raw and is never masked. */
35
- const BARE_CONTENT_SCHEMA = { kind: "scalar" };
36
- /**
37
- * The schema the reader and the redactor are both handed — `orchestra api` included, so the raw caller and the
38
- * typed command cannot disagree about which responses are the bare content.
39
- */
40
- export function responseSchemaOf(row) {
41
- if (row === undefined) {
42
- return undefined;
43
- }
44
- return BARE_CONTENT_OPERATIONS.has(row.operationId) ? BARE_CONTENT_SCHEMA : row.responseSchema;
45
- }
46
27
  /** Named so the error mapper prints the sentence and nothing else — no platform framing, no stack. */
47
28
  export class CommandUsageError extends Error {
48
29
  constructor(message) {
@@ -80,9 +61,7 @@ export async function executeCommand(row, invocation, deps = {}, ctx = {}) {
80
61
  }
81
62
  // The WHOLE target goes to the shared client, including the credential session, so a command that
82
63
  // outlives its access token renews it in flight instead of failing halfway through.
83
- const apis = createPlatformApis(target, deps.fetchApi, {
84
- warn: (lines) => printWarnings(lines, io),
85
- });
64
+ const apis = createPlatformApis(target, invocation.conventions.markdown === true ? acceptingMarkdown(deps.fetchApi) : deps.fetchApi, { warn: (lines) => printWarnings(lines, io) });
86
65
  // (2, 3) Compose and validate the invocation.
87
66
  const composed = composeRequest(row, invocation, env, deps);
88
67
  refuseDoubledContentSource(row, invocation, composed);
@@ -105,8 +84,8 @@ export async function executeCommand(row, invocation, deps = {}, ctx = {}) {
105
84
  // file that does not exist yet. It runs after the gate because storing bytes is itself a write.
106
85
  const stored = await storeDeclaredFile(row, invocation, composed.body, apis, target, deps, io);
107
86
  if (stored !== undefined) {
108
- // The upload already made this row's own create and completed it, so there is no second call to
109
- // make: `--file` stores one file, and the command answers the one it stored.
87
+ // The store already made this row's own create, so there is no second call to make: `--file` stores
88
+ // one file, and the command answers the one it stored.
110
89
  writeOutputFile(invocation, stored.body, io);
111
90
  const storedRedaction = redactBody(stored.body, stored.schema, invocation.conventions.reveal === true);
112
91
  printResponse(storedRedaction === undefined ? stored.body : storedRedaction.rendered, {
@@ -133,7 +112,7 @@ export async function executeCommand(row, invocation, deps = {}, ctx = {}) {
133
112
  return;
134
113
  }
135
114
  const response = await dispatch(row.operationId)(apis, dispatchArgs);
136
- const responseSchema = responseSchemaOf(row);
115
+ const responseSchema = row.responseSchema;
137
116
  const raw = await readRawBody(response.raw, responseSchema);
138
117
  if (writeOutputFile(invocation, raw, io) && raw instanceof Uint8Array) {
139
118
  return;
@@ -312,34 +291,69 @@ function refuseDoubledContentSource(row, invocation, composed) {
312
291
  * Nothing happens without `--file`.
313
292
  *
314
293
  * Where the row names a file by id, the create that follows is handed that id and this answers nothing.
315
- * Where the row DECLARES the upload itself — `sha256`, `sizeBytes` — the upload has already made exactly
316
- * that create and completed it, so this answers the stored row and the command prints it. Writing those
317
- * fields into a create that then runs again declared a SECOND file, which nothing completed and which
318
- * `--output json` printed: two rows for one `--file`, and the wrong one shown.
294
+ * Where the row DECLARES the upload itself — `sha256`, `sizeBytes` — the store IS that create, so this
295
+ * answers the stored row and the command prints it. Writing those fields into a create that then runs again
296
+ * declared a SECOND file, which nothing completed and which `--output json` printed: two rows for one
297
+ * `--file`, and the wrong one shown.
319
298
  */
320
299
  async function storeDeclaredFile(row, invocation, body, apis, target, deps, io) {
321
300
  const path = invocation.conventions.file;
322
301
  if (path === undefined || body === undefined) {
323
302
  return undefined;
324
303
  }
325
- const uploaded = await uploadFile(path, {
304
+ const namesFile = row.flags.some((flag) => flag.name === "file-id");
305
+ // The one create describes one file: what it says about the name and the type governs the store too, so
306
+ // the bytes cannot land as something the create did not ask for.
307
+ const named = { filename: stringField(body, "filename"), contentType: stringField(body, "contentType") };
308
+ const inline = inlineFileOf(path, named);
309
+ if (inline !== undefined && !namesFile) {
310
+ Object.assign(body, inline);
311
+ return await createInline(row, invocation, body, apis, target);
312
+ }
313
+ const placement = {
326
314
  apis,
327
- fetchApi: (deps.fetchApi ?? fetch),
328
315
  io,
329
316
  organizationId: target.organizationId,
330
- teamId: stringField(body, "teamId") ??
331
- (await ownerTeam(row, invocation, apis, target.organizationId)),
332
- // The one create describes one file: what it says about the name and the type governs both calls, so
333
- // the upload cannot land as something the create did not ask for.
334
- filename: stringField(body, "filename"),
335
- contentType: stringField(body, "contentType"),
317
+ teamId: stringField(body, "teamId") ?? (await ownerTeam(row, invocation, apis, target.organizationId)),
318
+ };
319
+ if (inline !== undefined) {
320
+ body["fileId"] = await storeInlineFile(inline, placement);
321
+ return undefined;
322
+ }
323
+ const uploaded = await uploadFile(path, {
324
+ ...placement,
325
+ ...named,
326
+ fetchApi: (deps.fetchApi ?? fetch),
327
+ ...(namesFile ? {} : { request: body }),
336
328
  });
337
- if (row.flags.some((flag) => flag.name === "file-id")) {
329
+ if (namesFile) {
338
330
  body["fileId"] = uploaded.fileId;
339
331
  return undefined;
340
332
  }
333
+ if (stringField(body, "path") !== undefined && uploaded.completion !== undefined) {
334
+ // The complete saved the library file at the path and answered it, so there is nothing to read back.
335
+ const schema = findCommand(row.noun, "complete-upload")?.responseSchema;
336
+ return { body: await readRawBody(uploaded.completion, schema), schema };
337
+ }
341
338
  return await readStoredFile(row, invocation, apis, target, uploaded.fileId);
342
339
  }
340
+ /** The row's own create, carrying the file's text as `content`; it answers `{file}`, and the file is printed. */
341
+ async function createInline(row, invocation, body, apis, target) {
342
+ const response = await dispatch(row.operationId)(apis, {
343
+ pathParams: invocation.pathParams,
344
+ query: {},
345
+ body: toWireValue(body),
346
+ ...(target.organizationId !== undefined ? { organizationId: target.organizationId } : {}),
347
+ });
348
+ const answer = await readRawBody(response.raw, row.responseSchema);
349
+ const file = typeof answer === "object" && answer !== null && !Array.isArray(answer)
350
+ ? answer["file"]
351
+ : undefined;
352
+ if (file === undefined || row.responseSchema?.kind !== "object") {
353
+ return { body: answer, schema: row.responseSchema };
354
+ }
355
+ return { body: file, schema: row.responseSchema.fields["file"]?.schema };
356
+ }
343
357
  /**
344
358
  * The stored row as this noun's own read answers it, printed bare as every other create prints what it
345
359
  * made, so `.id` names the new file. There is no upload locator to print beside it: the bytes are pushed.
@@ -354,7 +368,7 @@ async function readStoredFile(row, invocation, apis, target, fileId) {
354
368
  query: {},
355
369
  ...(target.organizationId !== undefined ? { organizationId: target.organizationId } : {}),
356
370
  });
357
- const schema = responseSchemaOf(read);
371
+ const schema = read.responseSchema;
358
372
  return { body: await readRawBody(response.raw, schema), schema };
359
373
  }
360
374
  /**
@@ -505,6 +519,19 @@ function sameItemPath(sibling, row) {
505
519
  }
506
520
  return sibling.positionals.every((positional, index) => positional.param === row.positionals[index]?.param);
507
521
  }
522
+ /**
523
+ * The platform answers an HTML or Markdown file's content as Markdown when the request accepts it, and the
524
+ * generated client sends no `Accept` of its own, so this one header is the whole of `--markdown`.
525
+ */
526
+ function acceptingMarkdown(fetchApi) {
527
+ const inner = (fetchApi ?? fetch);
528
+ const wrapped = (input, init) => {
529
+ const headers = new Headers(init?.headers);
530
+ headers.set("Accept", "text/markdown");
531
+ return inner(input, { ...init, headers });
532
+ };
533
+ return wrapped;
534
+ }
508
535
  function dispatch(operationId) {
509
536
  const entry = CLI_DISPATCH[operationId];
510
537
  if (entry === undefined) {
@@ -57,7 +57,9 @@ export declare function assertPlatformOrigin(baseUrl: string, path: string): str
57
57
  /**
58
58
  * `undefined` when this build cannot resolve the path — an endpoint newer than the CLI, or one outside the
59
59
  * public document. Matching is method-sensitive and template-aware, with query string and fragment dropped
60
- * first: they select data, never an operation.
60
+ * first: they select data, never an operation. Where two templates match, a literal segment beats a
61
+ * parameter at the first place they differ, so `/files/matches` is the search and not a read of a file
62
+ * whose id is `matches`.
61
63
  */
62
64
  export declare function resolveRawCommand(method: string, path: string): CommandRow | undefined;
63
65
  /**
@@ -17,7 +17,7 @@ import { sendRawPlatformRequest } from "../core/platform-client.js";
17
17
  import { redact, redactBinaryBody, writeResponseFile, } from "../core/redaction.js";
18
18
  import { resolveApprovalPolicy } from "../core/approval-policy.js";
19
19
  import { classifyWrite, ensureApproved, IMPERATIVE_APPROVAL_DEFAULT, printApprovalBypassNotice, } from "./approval.js";
20
- import { readRawBody, responseSchemaOf } from "./execute.js";
20
+ import { readRawBody } from "./execute.js";
21
21
  /**
22
22
  * Deliberately the five that address resources: `OPTIONS`/`HEAD`/`TRACE` answer questions about the
23
23
  * transport rather than the platform, and accepting one only converts a typo into a confusing 405.
@@ -92,8 +92,8 @@ async function sendRawCall(method, path, options, deps, ctx) {
92
92
  });
93
93
  // The reader and {@link redactRawBody} have to agree about which responses are bare content, or the body
94
94
  // the redactor would stream raw is one the reader has already parsed into something else — so both are
95
- // handed the SAME schema, the typed executor's bare-content exemptions included.
96
- const responseSchema = responseSchemaOf(row);
95
+ // handed the SAME schema, the row's own, exactly as the typed executor is.
96
+ const responseSchema = row?.responseSchema;
97
97
  const body = await readRawBody(response, responseSchema);
98
98
  if (options.outputFile !== undefined) {
99
99
  writeResponseFile(options.outputFile, body, io);
@@ -189,11 +189,38 @@ function readRequestDocument(method, options, deps) {
189
189
  /**
190
190
  * `undefined` when this build cannot resolve the path — an endpoint newer than the CLI, or one outside the
191
191
  * public document. Matching is method-sensitive and template-aware, with query string and fragment dropped
192
- * first: they select data, never an operation.
192
+ * first: they select data, never an operation. Where two templates match, a literal segment beats a
193
+ * parameter at the first place they differ, so `/files/matches` is the search and not a read of a file
194
+ * whose id is `matches`.
193
195
  */
194
196
  export function resolveRawCommand(method, path) {
195
197
  const segments = pathSegments(path);
196
- return CLI_COMMANDS.find((row) => row.method === method && matchesTemplate(row.path, segments));
198
+ let best;
199
+ for (const row of CLI_COMMANDS) {
200
+ if (row.method !== method || !matchesTemplate(row.path, segments)) {
201
+ continue;
202
+ }
203
+ if (best === undefined || moreLiteral(row.path, best.path)) {
204
+ best = row;
205
+ }
206
+ }
207
+ return best;
208
+ }
209
+ /** True when `template` has a literal segment where `other`, of the same length, first has a parameter. */
210
+ function moreLiteral(template, other) {
211
+ const parts = pathSegments(template);
212
+ const otherParts = pathSegments(other);
213
+ for (let index = 0; index < parts.length; index += 1) {
214
+ const literal = !isParameter(parts[index] ?? "");
215
+ const otherLiteral = !isParameter(otherParts[index] ?? "");
216
+ if (literal !== otherLiteral) {
217
+ return literal;
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+ function isParameter(segment) {
223
+ return segment.startsWith("{") && segment.endsWith("}");
197
224
  }
198
225
  function pathSegments(path) {
199
226
  const withoutQuery = path.split(/[?#]/)[0] ?? "";
@@ -205,7 +232,7 @@ function matchesTemplate(template, segments) {
205
232
  if (parts.length !== segments.length) {
206
233
  return false;
207
234
  }
208
- return parts.every((part, index) => part.startsWith("{") && part.endsWith("}") ? true : part === segments[index]);
235
+ return parts.every((part, index) => isParameter(part) || part === segments[index]);
209
236
  }
210
237
  /**
211
238
  * Reported as FULL paths, so the refusal names a property a reader can find in their file.
@@ -49,6 +49,7 @@ function registerRow(group, row, options) {
49
49
  reveal: values["reveal"] === true,
50
50
  outputFile: optional(values["outputFile"]),
51
51
  file: optional(values["file"]),
52
+ markdown: values["markdown"] === true,
52
53
  approveSecrets: values["approveSecrets"] === true ? true : undefined,
53
54
  requireApproval: optional(values["requireApproval"]),
54
55
  profile: optional(values["profile"]),
@@ -99,11 +100,15 @@ function registerConventionFlags(command, row) {
99
100
  command.option("--reveal", "print credential material in full instead of masking it");
100
101
  command.option("--output-file <path>", "write the full raw response to <path> (mode 0600; JSON, or the bytes for file content)");
101
102
  if (rowTakesFileOption(row)) {
102
- command.option("--file <path>", "upload the bytes at <path> and let this create name them, instead of saying where they are");
103
+ command.option("--file <path>", "store the file at <path> (text up to 256 KiB inline, anything else uploaded) and let this create " +
104
+ "name it, instead of saying where its bytes are");
103
105
  }
104
106
  if (row.pageWindow !== undefined) {
105
107
  command.option("--all-pages", "follow every page and concatenate the items");
106
108
  }
109
+ if (row.markdownResponse === true) {
110
+ command.option("--markdown", "ask for the content as Markdown (sends Accept: text/markdown)");
111
+ }
107
112
  if (row.method !== "GET") {
108
113
  command.option("--approve-secrets", "pre-approve the sensitive-write confirmation");
109
114
  // A read verb gets NEITHER of these: it has no approval to configure, so the flag is an unknown option
@@ -39,8 +39,9 @@ export declare function previewCommand(file: string, options?: PreviewCommandOpt
39
39
  /**
40
40
  * The policy the artifacts host serves a published page under, mirrored here so a page that reaches for
41
41
  * something production forbids — an outbound fetch, a script from an unlisted source, a nested frame —
42
- * fails in the rehearsal rather than after it is published. The one substitution is the frame ancestor:
43
- * the console's origin there, the preview's own here. `test/cli/preview.test.ts` reads the platform's
44
- * own source and fails when the two drift.
42
+ * fails in the rehearsal rather than after it is published. Two substitutions: the frame ancestor is the
43
+ * console's origin there and the preview's own here, and the store origin, where the host points what a page
44
+ * loads by a relative path, is blank here, as a rehearsal has no store and points nothing at one.
45
+ * `test/cli/preview.test.ts` reads the platform's own source and fails when the two drift.
45
46
  */
46
- export declare function pageContentSecurityPolicy(consoleOrigin: string): string;
47
+ export declare function pageContentSecurityPolicy(consoleOrigin: string, storeOrigin?: string): string;
@@ -180,12 +180,15 @@ const OPENING_BYTES = 1024;
180
180
  /**
181
181
  * The policy the artifacts host serves a published page under, mirrored here so a page that reaches for
182
182
  * something production forbids — an outbound fetch, a script from an unlisted source, a nested frame —
183
- * fails in the rehearsal rather than after it is published. The one substitution is the frame ancestor:
184
- * the console's origin there, the preview's own here. `test/cli/preview.test.ts` reads the platform's
185
- * own source and fails when the two drift.
183
+ * fails in the rehearsal rather than after it is published. Two substitutions: the frame ancestor is the
184
+ * console's origin there and the preview's own here, and the store origin, where the host points what a page
185
+ * loads by a relative path, is blank here, as a rehearsal has no store and points nothing at one.
186
+ * `test/cli/preview.test.ts` reads the platform's own source and fails when the two drift.
186
187
  */
187
- export function pageContentSecurityPolicy(consoleOrigin) {
188
+ export function pageContentSecurityPolicy(consoleOrigin, storeOrigin = "") {
188
189
  const ancestor = consoleOrigin.trim() === "" ? "'none'" : consoleOrigin.trim();
190
+ const store = storeOrigin.trim();
191
+ const media = store === "" ? "'none'" : store;
189
192
  return [
190
193
  "default-src 'none'",
191
194
  "sandbox allow-scripts allow-popups",
@@ -194,11 +197,14 @@ export function pageContentSecurityPolicy(consoleOrigin) {
194
197
  "frame-src 'none'",
195
198
  `frame-ancestors ${ancestor}`,
196
199
  "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net" +
197
- " https://cdn.tailwindcss.com https://code.jquery.com",
198
- "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
199
- "img-src 'self' data:",
200
- "font-src https://fonts.gstatic.com",
201
- ].join("; ");
200
+ ` https://cdn.tailwindcss.com https://code.jquery.com ${store}`,
201
+ `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com ${store}`,
202
+ `img-src 'self' data: ${store}`,
203
+ `media-src ${media}`,
204
+ `font-src https://fonts.gstatic.com ${store}`,
205
+ ]
206
+ .map((directive) => directive.trim())
207
+ .join("; ");
202
208
  }
203
209
  function originOf(server, host) {
204
210
  return `http://${host ?? "127.0.0.1"}:${server.address().port}`;
@@ -1,6 +1,8 @@
1
1
  import type { CommandRow } from "../generated/cli-commands.js";
2
2
  import type { CliApis } from "../generated/cli-dispatch.js";
3
3
  import type { CliIo } from "./core/output.js";
4
+ /** The most `content` the platform takes inline, counted in UTF-8 bytes. */
5
+ export declare const INLINE_LIMIT_BYTES: number;
4
6
  /**
5
7
  * A row `--file` attaches to, decided by what the ROW declares rather than by its noun and verb, because
6
8
  * this file has no business knowing either.
@@ -21,6 +23,14 @@ export interface UploadedFile {
21
23
  readonly filename: string;
22
24
  /** True when the platform already held these bytes, so this call stored nothing. */
23
25
  readonly alreadyStored: boolean;
26
+ /** The complete call's unread answer, when one was sent: for a save by path, the library file it saved. */
27
+ readonly completion?: Response;
28
+ }
29
+ /** A local file a create can carry as its own `content`, with the name and type the create gives it. */
30
+ export interface InlineFile {
31
+ readonly content: string;
32
+ readonly filename: string;
33
+ readonly contentType: string;
24
34
  }
25
35
  export interface UploadOptions {
26
36
  /**
@@ -36,7 +46,23 @@ export interface UploadOptions {
36
46
  readonly teamId?: string | undefined;
37
47
  readonly filename?: string | undefined;
38
48
  readonly contentType?: string | undefined;
49
+ /**
50
+ * The create's own fields when the upload is that create. A `path` among them makes the declare an upload
51
+ * intent, which is completed even when the platform answers no plan, since only the complete saves it.
52
+ */
53
+ readonly request?: Readonly<Record<string, unknown>> | undefined;
39
54
  }
55
+ /**
56
+ * The file's text when a create can carry it inline, unchanged: at most {@link INLINE_LIMIT_BYTES} of UTF-8,
57
+ * with no NUL, which the platform cannot store as text, and not blank. A byte order mark stays in the text,
58
+ * so the stored bytes are the file's own.
59
+ */
60
+ export declare function inlineFileOf(path: string, named?: {
61
+ filename?: string | undefined;
62
+ contentType?: string | undefined;
63
+ }): InlineFile | undefined;
64
+ /** Stores the text as a fixed file in one create, for a create that names it by id next. */
65
+ export declare function storeInlineFile(file: InlineFile, options: Pick<UploadOptions, "apis" | "io" | "organizationId" | "teamId">): Promise<string>;
40
66
  export declare function uploadFile(path: string, options: UploadOptions): Promise<UploadedFile>;
41
67
  /**
42
68
  * What the file is, read off its name. The store keeps whatever it is told, and the console decides how to
@@ -1,14 +1,15 @@
1
- // The whole behaviour of `--file`: bytes on disk become a stored platform file, in the four steps the
2
- // platform's upload contract names — declare the address, push the bytes the answer authorizes, complete,
3
- // and hand the caller the file id the create then names.
1
+ // The whole behaviour of `--file`: bytes on disk become a stored platform file. A small text travels inline
2
+ // as the create's own `content`; anything else takes the four steps the platform's upload contract names —
3
+ // declare the address, push the bytes the answer authorizes, complete, and hand the caller the file id the
4
+ // create then names.
4
5
  //
5
- // Two rules here are not style. The bytes are STREAMED from disk in both shapes, so a file larger than
6
- // memory uploads exactly like a small one. And a presigned request is sent through the BARE fetch rather
7
- // than the platform client: the signed headers travel verbatim, the platform bearer never reaches the
8
- // object store, and the CLI's 60-second request bound — right for an API call, wrong for a long transfer —
9
- // does not apply.
6
+ // Two rules here are not style. A declared upload's bytes are STREAMED from disk in both shapes, so a file
7
+ // larger than memory uploads exactly like a small one. And a presigned request is sent through the BARE
8
+ // fetch rather than the platform client: the signed headers travel verbatim, the platform bearer never
9
+ // reaches the object store, and the CLI's 60-second request bound — right for an API call, wrong for a long
10
+ // transfer — does not apply.
10
11
  import { createHash } from "node:crypto";
11
- import { createReadStream, statSync } from "node:fs";
12
+ import { createReadStream, readFileSync, statSync } from "node:fs";
12
13
  import { basename, extname } from "node:path";
13
14
  import { Readable } from "node:stream";
14
15
  /** Parts in flight at once. */
@@ -24,6 +25,13 @@ const DECLARE_ATTEMPTS = 2;
24
25
  const CONTENT_SOURCE_FIELDS = ["content", "fileId", "url"];
25
26
  /** The platform's own default for a declared upload that named no type. */
26
27
  const DEFAULT_CONTENT_TYPE = "application/octet-stream";
28
+ /** The most `content` the platform takes inline, counted in UTF-8 bytes. */
29
+ export const INLINE_LIMIT_BYTES = 256 * 1024;
30
+ /**
31
+ * Blank as the platform judges `content`, widened to JavaScript's own whitespace, so a text the platform
32
+ * would refuse as blank goes through a declared upload instead.
33
+ */
34
+ const BLANK = /^[\s\u001c-\u001f]*$/;
27
35
  /**
28
36
  * A row `--file` attaches to, decided by what the ROW declares rather than by its noun and verb, because
29
37
  * this file has no business knowing either.
@@ -50,19 +58,52 @@ class UploadError extends Error {
50
58
  /** Raised by a push whose signature the store no longer honours; the only failure a fresh declare repairs. */
51
59
  class PlanExpiredError extends Error {
52
60
  }
61
+ /**
62
+ * The file's text when a create can carry it inline, unchanged: at most {@link INLINE_LIMIT_BYTES} of UTF-8,
63
+ * with no NUL, which the platform cannot store as text, and not blank. A byte order mark stays in the text,
64
+ * so the stored bytes are the file's own.
65
+ */
66
+ export function inlineFileOf(path, named = {}) {
67
+ if (statOf(path).size > INLINE_LIMIT_BYTES) {
68
+ return undefined;
69
+ }
70
+ const bytes = readFileSync(path);
71
+ if (bytes.byteLength > INLINE_LIMIT_BYTES) {
72
+ return undefined;
73
+ }
74
+ let content;
75
+ try {
76
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ if (content.includes("\u0000") || BLANK.test(content)) {
82
+ return undefined;
83
+ }
84
+ const filename = named.filename ?? basename(path);
85
+ return { content, filename, contentType: named.contentType ?? contentTypeOf(filename) };
86
+ }
87
+ /** Stores the text as a fixed file in one create, for a create that names it by id next. */
88
+ export async function storeInlineFile(file, options) {
89
+ const stored = await options.apis.files.createFile({
90
+ createFileRequest: { ...file, ...(options.teamId !== undefined ? { teamId: options.teamId } : {}) },
91
+ ...(options.organizationId !== undefined ? { xOrganizationId: options.organizationId } : {}),
92
+ });
93
+ options.io.err(`note: stored ${file.filename} (${Buffer.byteLength(file.content, "utf8")} bytes) inline as ${stored.file.id}.`);
94
+ return stored.file.id;
95
+ }
53
96
  export async function uploadFile(path, options) {
54
97
  const stats = statOf(path);
55
- if (stats.size === 0) {
56
- throw new UploadError(`--file ${path} is empty; there are no bytes to upload.`);
57
- }
58
98
  const filename = options.filename ?? basename(path);
59
99
  const contentType = options.contentType ?? contentTypeOf(filename);
60
100
  const sha256 = await sha256Of(path);
101
+ const intent = typeof options.request?.["path"] === "string";
61
102
  for (let attempt = 1;; attempt += 1) {
62
103
  const declared = await declare(path, { ...options, filename, contentType }, sha256, stats.size);
63
104
  try {
64
105
  const parts = await push(declared, path, stats.size, options);
65
- await complete(declared, parts, options);
106
+ const completion = await complete(declared, intent ? (parts ?? []) : parts, options);
66
107
  report(declared, parts, filename, stats.size, options.io);
67
108
  return {
68
109
  fileId: declared.file.id,
@@ -71,6 +112,7 @@ export async function uploadFile(path, options) {
71
112
  contentType,
72
113
  filename,
73
114
  alreadyStored: parts === undefined,
115
+ ...(completion !== undefined ? { completion } : {}),
74
116
  };
75
117
  }
76
118
  catch (failure) {
@@ -106,6 +148,7 @@ async function sha256Of(path) {
106
148
  async function declare(path, options, sha256, sizeBytes) {
107
149
  return await options.apis.files.createFile({
108
150
  createFileRequest: {
151
+ ...options.request,
109
152
  sha256,
110
153
  sizeBytes,
111
154
  filename: options.filename,
@@ -185,7 +228,10 @@ async function putRange(locator, path, start, end, length, options) {
185
228
  for (const [name, value] of Object.entries(locator.headers)) {
186
229
  headers.set(name, value);
187
230
  }
188
- const body = Readable.toWeb(createReadStream(path, { start, end }));
231
+ // A read stream refuses an empty range, so an empty file pushes an empty body.
232
+ const body = length === 0
233
+ ? new Uint8Array(0)
234
+ : Readable.toWeb(createReadStream(path, { start, end }));
189
235
  let response;
190
236
  try {
191
237
  response = await options.fetchApi(locator.url, {
@@ -213,14 +259,15 @@ async function putRange(locator, path, start, end, length, options) {
213
259
  }
214
260
  async function complete(declared, parts, options) {
215
261
  if (parts === undefined) {
216
- return;
262
+ return undefined;
217
263
  }
218
264
  // `files complete-upload` generates no flags of its own, so this is the one caller of the SDK method.
219
- await options.apis.files.completeFileUpload({
265
+ const response = await options.apis.files.completeFileUploadRaw({
220
266
  id: declared.file.id,
221
267
  completeFileUploadRequest: { parts },
222
268
  ...(options.organizationId !== undefined ? { xOrganizationId: options.organizationId } : {}),
223
269
  });
270
+ return response.raw;
224
271
  }
225
272
  /** One line for the whole transfer, whatever it took — a line per part would bury the command's own output. */
226
273
  function report(declared, parts, filename, sizeBytes, io) {
@@ -241,6 +288,7 @@ export function contentTypeOf(filename) {
241
288
  return CONTENT_TYPES[extname(filename).toLowerCase()] ?? DEFAULT_CONTENT_TYPE;
242
289
  }
243
290
  const CONTENT_TYPES = {
291
+ ".uri": "text/uri-list",
244
292
  ".html": "text/html",
245
293
  ".htm": "text/html",
246
294
  ".css": "text/css",
@@ -55,6 +55,11 @@ export interface CommandRow {
55
55
  readonly kind: "page" | "cursor";
56
56
  readonly param: string;
57
57
  };
58
+ /**
59
+ * Present when a success response declares `text/markdown`, so the command takes `--markdown`, which sends
60
+ * `Accept: text/markdown`.
61
+ */
62
+ readonly markdownResponse?: true;
58
63
  }
59
64
  /** One flag. A credential field registers the `-from-env`/`-from-command-line` PAIR and no plain flag. */
60
65
  export interface FlagSpec {