@lotics/cli 0.204.0 → 0.205.1

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/dist/src/cli.js CHANGED
@@ -14940,10 +14940,12 @@ var LoticsClient = class {
14940
14940
  body = {};
14941
14941
  }
14942
14942
  const message2 = typeof body.message === "string" ? body.message : text;
14943
+ const hint = typeof body.hint === "string" ? `
14944
+ ${body.hint}` : "";
14943
14945
  const trace = requestId === void 0 ? "" : `
14944
14946
 
14945
14947
  Request id: ${requestId} \u2014 quote this to Lotics support.`;
14946
- throw new LoticsRequestError(`${response.status}: ${message2}${trace}`, response.status, body);
14948
+ throw new LoticsRequestError(`${response.status}: ${message2}${hint}${trace}`, response.status, body);
14947
14949
  }
14948
14950
  /**
14949
14951
  * The backend's `log()` middleware registers `user-agent`,
@@ -14993,9 +14995,23 @@ var LoticsClient = class {
14993
14995
  if (!response.ok) await this.throwResponseError(response, headers["x-request-id"]);
14994
14996
  return response.json();
14995
14997
  }
14998
+ /** `email` is null for a credential no person signs in as; `account_type` is absent from an older server. */
14999
+ /** `credential_origin` describes the CREDENTIAL this call carried, not the
15000
+ * member above: `sign_in` for a terminal somebody signed in from, `api_key`
15001
+ * for one an admin issued. Null for a row minted before it was recorded, and
15002
+ * absent from an older server — both mean not told. */
14996
15003
  async whoami() {
14997
15004
  return this.request("GET", "/v1/cli/whoami");
14998
15005
  }
15006
+ /**
15007
+ * Hand this client's own credential back — what `lotics auth logout` calls
15008
+ * before it forgets the key, so signing out ends the credential instead of
15009
+ * leaving a live one on a server nobody can see it from. Takes no id: the
15010
+ * only thing it can revoke is the key these requests carry.
15011
+ */
15012
+ async revokeSelfCredential() {
15013
+ return this.request("DELETE", "/v1/api_keys/self");
15014
+ }
14999
15015
  /**
15000
15016
  * File a hand-written report, or a list of them in one call. Unlike a
15001
15017
  * telemetry flush, the headers this stamps are CORRECT: the invocation making
@@ -15191,8 +15207,12 @@ var LoticsClient = class {
15191
15207
  * provenance is a 400, and one already on the latest release a 409 — both
15192
15208
  * refusals, both before any write. Admin-only.
15193
15209
  */
15194
- async upgradeApp(app_id) {
15195
- return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/upgrade`, {});
15210
+ async upgradeApp(app_id, opts = {}) {
15211
+ return this.request(
15212
+ "POST",
15213
+ `/v1/apps/${encodeURIComponent(app_id)}/upgrade`,
15214
+ opts.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
15215
+ );
15196
15216
  }
15197
15217
  /**
15198
15218
  * Capture live records from this workspace as a starter's sample data.
@@ -15386,6 +15406,36 @@ var LoticsClient = class {
15386
15406
  { public_subdomain }
15387
15407
  );
15388
15408
  }
15409
+ /**
15410
+ * Publish the app's API — the owner's promise about what its declared queries
15411
+ * and workflows return to a caller outside the app.
15412
+ *
15413
+ * Snapshots the contract and answers the version it took, plus what the
15414
+ * published surface exposes that the owner may not have intended. A query
15415
+ * that does not name its columns is refused (400) before anything is written:
15416
+ * the app cannot promise field names it never stated.
15417
+ */
15418
+ async publishAppApi(app_id) {
15419
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/api/publish`, {});
15420
+ }
15421
+ /** End the promise. `unpublished: false` means the app was publishing nothing,
15422
+ * which this leaves unchanged. The superseded snapshot stays, so a later
15423
+ * publish continues the numbering rather than reusing a version. */
15424
+ async unpublishAppApi(app_id) {
15425
+ return this.request("DELETE", `/v1/apps/${encodeURIComponent(app_id)}/api/publish`);
15426
+ }
15427
+ /** Whether the app is publishing an API, and which contract version. */
15428
+ async getAppApiPublication(app_id) {
15429
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/api/publication`);
15430
+ }
15431
+ /**
15432
+ * The published API as an OpenAPI 3.1 document — what a consumer's own
15433
+ * generator reads. Rendered from the live SNAPSHOT rather than the manifest,
15434
+ * so it describes what the app has promised; 404 while nothing is published.
15435
+ */
15436
+ async getAppOpenApiDocument(app_id) {
15437
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/openapi.json`);
15438
+ }
15389
15439
  async getAppVersion(app_id, version_id) {
15390
15440
  return this.request(
15391
15441
  "GET",
@@ -15475,7 +15525,10 @@ var LoticsClient = class {
15475
15525
  ...body.outputs ? { outputs: body.outputs } : {},
15476
15526
  ...body.name ? { name: body.name } : {},
15477
15527
  ...body.description ? { description: body.description } : {},
15478
- ...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {}
15528
+ ...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {},
15529
+ // Sent only when the caller asked for it: absent means "refuse a break",
15530
+ // which is the answer a caller who said nothing gave.
15531
+ ...body.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
15479
15532
  });
15480
15533
  }
15481
15534
  /**
@@ -15486,12 +15539,13 @@ var LoticsClient = class {
15486
15539
  * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
15487
15540
  * so the next `lotics app deploy` overwrites this from the manifest.
15488
15541
  */
15489
- async setAppQuery(app_id, alias, declaration, expected_sha) {
15542
+ async setAppQuery(app_id, alias, declaration, expected_sha, acknowledge_breaking_api_change) {
15490
15543
  return this.execute("set_app_query", {
15491
15544
  app_id,
15492
15545
  alias,
15493
15546
  declaration,
15494
- ...expected_sha ? { expected_sha } : {}
15547
+ ...expected_sha ? { expected_sha } : {},
15548
+ ...acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
15495
15549
  });
15496
15550
  }
15497
15551
  /**
@@ -15694,16 +15748,12 @@ var LoticsClient = class {
15694
15748
  formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
15695
15749
  formData.append("agent_aliases", JSON.stringify(args.agent_aliases ?? []));
15696
15750
  formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
15751
+ if (args.acknowledge_breaking_api_change) {
15752
+ formData.append("acknowledge_breaking_api_change", "true");
15753
+ }
15697
15754
  const url2 = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
15698
15755
  const headers = this.buildHeaders();
15699
15756
  const response = await fetch(url2, { method: "POST", headers, body: formData });
15700
- if (response.status === 409) {
15701
- const conflict = await response.json();
15702
- const err = new Error(conflict.message ?? "Deploy conflict \u2014 pull required");
15703
- err.code = "VERSION_CONFLICT";
15704
- err.current_version_id = conflict.current_version_id ?? null;
15705
- throw err;
15706
- }
15707
15757
  if (!response.ok) {
15708
15758
  await this.throwResponseError(response, headers["x-request-id"]);
15709
15759
  }
@@ -16432,6 +16482,7 @@ function upsertProfile(orgId, fields) {
16432
16482
  const workspace_id = fields.workspace_id ?? existing?.workspace_id;
16433
16483
  const profile = {
16434
16484
  api_key: fields.api_key,
16485
+ kind: fields.kind,
16435
16486
  org_name: existing?.org_name ?? fields.org_name,
16436
16487
  ...existing?.server_name === void 0 ? {} : { server_name: existing.server_name },
16437
16488
  api_url: fields.api_url,
@@ -34139,6 +34190,9 @@ function capabilityDescriptionRefusal(kind, alias, description) {
34139
34190
  const plural = kind === "query" ? "queries" : "workflows";
34140
34191
  return `${noun} alias "${alias}" description is ${description.length} characters \u2014 the cap is ${MAX_APP_CAPABILITY_DESCRIPTION}. It is a one-line summary an agent reads when choosing between this app's ${plural}; longer guidance belongs in the agent's own instructions.`;
34141
34192
  }
34193
+ var APP_ANONYMOUS_REQUEST_MAX_BYTES = 256 * 1024;
34194
+ var PUBLIC_APP_UPLOAD_BYTES_LIMIT = 500 * 1024 * 1024;
34195
+ var PUBLIC_APP_UPLOAD_BYTES_WINDOW_SECONDS = 60 * 60;
34142
34196
  var appWorkflowDeclarationSchema = zod_default.object({
34143
34197
  workflow_id: zod_default.string().describe("ID of the workflow this alias resolves to"),
34144
34198
  inputs: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional().describe(
@@ -34249,6 +34303,9 @@ var appSchema = zod_default.object({
34249
34303
  "Alias \u2192 agent declaration map. Each alias binds a streaming tool-loop agent the app runs via `useAgentRun(alias)` (an SSE stream), with declared tools, model, and typed inputs/outputs. Synced from the app's `lotics.agents` manifest on every deploy. The agent runs under the app's IAM principal; runs persist a flat history per session."
34250
34304
  ),
34251
34305
  theme: appThemeSchema.nullable().optional().describe("Theme settings for the app"),
34306
+ can_author: zod_default.boolean().optional().describe(
34307
+ "Whether the caller may author this app \u2014 open a draft, edit its source, publish it. True for the app's owner and for an organization admin. A `manager` share is not authoring: it manages the app under its owner's authority, which is somebody else's. Optional because an older server omits it \u2014 absent means that server's rule, an organization admin."
34308
+ ),
34252
34309
  /**
34253
34310
  * The starter this app is the ORIGIN of, when it is one — list enrichment,
34254
34311
  * read from the registry rather than stored on the app.
@@ -40054,7 +40111,7 @@ function resultSideEffects(result) {
40054
40111
  }
40055
40112
 
40056
40113
  // src/version.ts
40057
- var VERSION = "0.204.0";
40114
+ var VERSION = "0.205.1";
40058
40115
 
40059
40116
  // src/timezone.ts
40060
40117
  function machineTimezone() {
@@ -40631,6 +40688,11 @@ var COMMANDS = [
40631
40688
  " --prune-invoked <alias> unbinds a workflow the",
40632
40689
  " workspace has already RUN \u2014 named per alias, only",
40633
40690
  " when you know that caller is gone)",
40691
+ " [--acknowledge-breaking-api] \u2026 even though it breaks what the app's published API",
40692
+ " promises. Without it such a release is refused and",
40693
+ " every breaking change is named. It counts only from",
40694
+ " an organization admin. Same flag on app query set,",
40695
+ " app workflow set and app upgrade",
40634
40696
  " lotics app versions [app_id] Show deploy history newest-first (version,",
40635
40697
  " timestamp, deployer, build status, -m message;",
40636
40698
  " * marks the currently served version)",
@@ -40643,6 +40705,23 @@ var COMMANDS = [
40643
40705
  " Already on the latest version says so and does",
40644
40706
  " nothing; an app nobody copied has no package to",
40645
40707
  " offer one",
40708
+ " lotics app api publish [app_id] Publish the app's API \u2014 the promise its declared",
40709
+ " queries and workflows make to a caller outside it",
40710
+ " (a customer's own site or server), snapshotted as a",
40711
+ " contract version. Prints that version, when it was",
40712
+ " taken, and what the published surface exposes. A",
40713
+ " query that does not name the columns it returns is",
40714
+ " refused: those field names are not the app's to keep",
40715
+ " lotics app api unpublish [app_id] End the promise. A later publish continues the",
40716
+ " numbering rather than reusing a version",
40717
+ " lotics app api status [app_id] Whether an API is published, and which contract",
40718
+ " version its callers hold",
40719
+ " lotics app api spec [app_id] [-o <file.json>]",
40720
+ " The OpenAPI document of the published API, rendered",
40721
+ " from the snapshot \u2014 what a consumer's own generator",
40722
+ " reads. To stdout, or to the file named",
40723
+ " [--json] publish / unpublish / status answer with one object",
40724
+ " on stdout and nothing else; every warning rides in it",
40646
40725
  " lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)",
40647
40726
  " from the manifest + workspace schema \u2014 no deploy",
40648
40727
  " lotics app check [--screens] Run every deploy pre-flight WITHOUT building or",
@@ -44763,7 +44842,9 @@ function warn(text) {
44763
44842
  console.error(text);
44764
44843
  }
44765
44844
  function emitJson(value) {
44766
- const payload = collectedWarnings.length > 0 ? { ...value, warnings: [...collectedWarnings] } : value;
44845
+ const own = "warnings" in value && Array.isArray(value.warnings) ? value.warnings : [];
44846
+ const warnings = [...collectedWarnings, ...own];
44847
+ const payload = warnings.length > 0 ? { ...value, warnings } : value;
44767
44848
  collectedWarnings.length = 0;
44768
44849
  console.log(JSON.stringify(payload, null, 2));
44769
44850
  }
@@ -46228,6 +46309,12 @@ function deepLinkLoc(pathname, search) {
46228
46309
  if (pathname === "/" || pathname === "/index.html") return null;
46229
46310
  return pathname + search;
46230
46311
  }
46312
+ function devRpcFailure(op, id, message2, field_errors) {
46313
+ if (op !== "workflow") return { id, type: "error", message: message2 };
46314
+ var data = { status: "error", message: message2 };
46315
+ if (field_errors) data.field_errors = field_errors;
46316
+ return { id, type: "result", data };
46317
+ }
46231
46318
  function buildWrapperPage(args) {
46232
46319
  const { app_name, app_id, workspace_id, vite_url, api_url, web_app_url } = args;
46233
46320
  return `<!doctype html>
@@ -46302,6 +46389,10 @@ function buildWrapperPage(args) {
46302
46389
  appSrc.searchParams.set("lotics_host", window.location.origin);
46303
46390
  iframe.src = appSrc.toString();
46304
46391
 
46392
+ // What a failed op is posted back as \u2014 resolve-vs-reject, per op, the way
46393
+ // both deployed transports settle it.
46394
+ var devRpcFailure = (${devRpcFailure.toString()});
46395
+
46305
46396
  async function rpc(op, payload) {
46306
46397
  const res = await fetch("/_rpc", {
46307
46398
  method: "POST",
@@ -46310,9 +46401,13 @@ function buildWrapperPage(args) {
46310
46401
  });
46311
46402
  const text = await res.text();
46312
46403
  if (!res.ok) {
46313
- let detail = text;
46314
- try { detail = JSON.parse(text).message ?? text; } catch (_) {}
46315
- throw new Error(detail || ("HTTP " + res.status));
46404
+ // The server relays the API's own error envelope, so the refusal's
46405
+ // code and the inputs it named survive to the app (see rpcFailureBody).
46406
+ let detail = text, body = null;
46407
+ try { body = JSON.parse(text); detail = body.message ?? text; } catch (_) {}
46408
+ const err = new Error(detail || ("HTTP " + res.status));
46409
+ if (body) { err.code = body.code; err.field_errors = body.field_errors; }
46410
+ throw err;
46316
46411
  }
46317
46412
  return JSON.parse(text);
46318
46413
  }
@@ -46521,9 +46616,9 @@ function buildWrapperPage(args) {
46521
46616
  );
46522
46617
  } catch (err) {
46523
46618
  const message = err && err.message ? err.message : String(err);
46524
- console.error("[lotics-dev] " + msg.op + " failed:", message);
46619
+ console.error("[lotics-dev] " + msg.op + " failed:", (err && err.code ? err.code + " \u2014 " : "") + message);
46525
46620
  iframe.contentWindow.postMessage(
46526
- { id: msg.id, type: "error", message: message },
46621
+ devRpcFailure(msg.op, msg.id, message, err && err.field_errors),
46527
46622
  VITE_ORIGIN
46528
46623
  );
46529
46624
  }
@@ -46646,6 +46741,18 @@ var STOP_DEADLINE_MS = 2e3;
46646
46741
  function serializeRpcResult(result) {
46647
46742
  return JSON.stringify(result ?? null);
46648
46743
  }
46744
+ function rpcFailureBody(err) {
46745
+ const message2 = err instanceof Error ? err.message : String(err);
46746
+ if (!(err instanceof LoticsRequestError)) return { message: message2 };
46747
+ const code = err.body.code;
46748
+ const raw = err.body.field_errors;
46749
+ const named = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? Object.entries(raw).filter((e) => typeof e[1] === "string") : [];
46750
+ return {
46751
+ message: message2,
46752
+ ...typeof code === "string" ? { code } : {},
46753
+ ...named.length > 0 ? { field_errors: Object.fromEntries(named) } : {}
46754
+ };
46755
+ }
46649
46756
  function servesWrapperPage(method, pathname) {
46650
46757
  return method === "GET" && !pathname.startsWith("/_");
46651
46758
  }
@@ -46772,11 +46879,11 @@ async function startDevServer(args) {
46772
46879
  );
46773
46880
  } catch (err) {
46774
46881
  release();
46775
- const message2 = err instanceof Error ? err.message : String(err);
46776
- process.stderr.write(`[rpc] ERROR ${message2}
46882
+ const failure = rpcFailureBody(err);
46883
+ process.stderr.write(`[rpc] ERROR ${failure.message}
46777
46884
  `);
46778
46885
  res.writeHead(500, { "Content-Type": "application/json" });
46779
- res.end(JSON.stringify({ message: message2 }));
46886
+ res.end(JSON.stringify(failure));
46780
46887
  }
46781
46888
  return;
46782
46889
  }
@@ -53732,6 +53839,8 @@ var issueSchema = external_exports.object({
53732
53839
  });
53733
53840
 
53734
53841
  // src/app_commands.ts
53842
+ var BREAKING_API_CHANGE_CODE = "breaking_api_change";
53843
+ var ACKNOWLEDGMENT_ARG = "acknowledge_breaking_api_change";
53735
53844
  function orderedLike(value, template) {
53736
53845
  if (Array.isArray(value)) {
53737
53846
  const t = Array.isArray(template) ? template : [];
@@ -55248,7 +55357,9 @@ async function appUpgrade(client, args) {
55248
55357
  const appId = args.app_id ?? readAppMeta(process.cwd()).app_id;
55249
55358
  let result;
55250
55359
  try {
55251
- result = await client.upgradeApp(appId);
55360
+ result = await client.upgradeApp(appId, {
55361
+ acknowledge_breaking_api_change: args.acknowledgeBreakingApi
55362
+ });
55252
55363
  } catch (error51) {
55253
55364
  if (!(error51 instanceof LoticsRequestError) || error51.status !== 409 || error51.body.reason !== "already_latest") {
55254
55365
  throw error51;
@@ -55277,6 +55388,64 @@ async function appUpgrade(client, args) {
55277
55388
  }
55278
55389
  console.error(` Edit the new code: lotics app pull ${result.app_id}`);
55279
55390
  }
55391
+ function apiTargetApp(app_id) {
55392
+ return app_id ?? readAppMeta(process.cwd()).app_id;
55393
+ }
55394
+ async function appApiPublish(client, args) {
55395
+ const appId = apiTargetApp(args.app_id);
55396
+ let result;
55397
+ try {
55398
+ result = await client.publishAppApi(appId);
55399
+ } catch (error51) {
55400
+ if (!(error51 instanceof LoticsRequestError) || error51.status !== 400 || typeof error51.body.message !== "string") {
55401
+ throw error51;
55402
+ }
55403
+ fail(error51.body.message);
55404
+ }
55405
+ if (isMachineOutput()) {
55406
+ emitJson(result);
55407
+ return;
55408
+ }
55409
+ note(
55410
+ `Published the API of ${appId} \u2014 contract version ${result.contract_version}, ${result.published_at}.`
55411
+ );
55412
+ for (const warning of result.warnings) note(` \u26A0 ${warning}`);
55413
+ note(` The document its callers read: lotics app api spec ${appId}`);
55414
+ }
55415
+ async function appApiUnpublish(client, args) {
55416
+ const appId = apiTargetApp(args.app_id);
55417
+ const result = await client.unpublishAppApi(appId);
55418
+ if (isMachineOutput()) {
55419
+ emitJson(result);
55420
+ return;
55421
+ }
55422
+ note(
55423
+ result.unpublished ? `Stopped publishing the API of ${appId}.` : `${appId} was publishing no API \u2014 nothing changed.`
55424
+ );
55425
+ }
55426
+ async function appApiStatus(client, args) {
55427
+ const appId = apiTargetApp(args.app_id);
55428
+ const publication = await client.getAppApiPublication(appId);
55429
+ if (isMachineOutput()) {
55430
+ emitJson({ app_id: appId, ...publication });
55431
+ return;
55432
+ }
55433
+ console.log(
55434
+ publication.published ? `${appId}: publishing API contract version ${publication.contract_version}, since ${publication.published_at}.` : `${appId}: publishing no API.`
55435
+ );
55436
+ }
55437
+ async function appApiSpec(client, args) {
55438
+ const appId = apiTargetApp(args.app_id);
55439
+ const document2 = await client.getAppOpenApiDocument(appId);
55440
+ const text = `${JSON.stringify(document2, null, 2)}
55441
+ `;
55442
+ if (args.outputPath === void 0) {
55443
+ process.stdout.write(text);
55444
+ return;
55445
+ }
55446
+ fs11.writeFileSync(args.outputPath, text);
55447
+ note(`Wrote the OpenAPI document of ${appId} to ${args.outputPath}`);
55448
+ }
55280
55449
  function defaultPullTarget(appId, appName) {
55281
55450
  const cwdPkgPath = path12.join(process.cwd(), "package.json");
55282
55451
  if (fs11.existsSync(cwdPkgPath)) {
@@ -55519,7 +55688,7 @@ async function appDeploy(client, args) {
55519
55688
  });
55520
55689
  let live = liveApp;
55521
55690
  if (!nothingPending(pending)) {
55522
- await pushPendingBindings(client, projectDir, pending);
55691
+ await pushPendingBindings(client, projectDir, pending, args.acknowledgeBreakingApi);
55523
55692
  live = await client.getApp(meta3.app_id);
55524
55693
  }
55525
55694
  const sourceText = readAppSourceText(projectDir);
@@ -55594,7 +55763,8 @@ async function appDeploy(client, args) {
55594
55763
  // binding.
55595
55764
  workflow_aliases: called.workflows,
55596
55765
  query_aliases: called.queries,
55597
- agent_aliases: called.agents
55766
+ agent_aliases: called.agents,
55767
+ acknowledge_breaking_api_change: args.acknowledgeBreakingApi
55598
55768
  });
55599
55769
  writeAppMeta(projectDir, {
55600
55770
  ...meta3,
@@ -55651,7 +55821,8 @@ async function appDeploy(client, args) {
55651
55821
  orphans,
55652
55822
  undeclared,
55653
55823
  called.dynamic,
55654
- args.pruneInvoked ?? []
55824
+ args.pruneInvoked ?? [],
55825
+ args.acknowledgeBreakingApi
55655
55826
  );
55656
55827
  } else {
55657
55828
  warnAboutOrphanedBindings(called, liveAfter, meta3.bundle_calls, undeclared);
@@ -55669,11 +55840,19 @@ async function appDeploy(client, args) {
55669
55840
  }
55670
55841
  }
55671
55842
  } catch (err) {
55672
- const e = err;
55673
- if (e.code === "VERSION_CONFLICT") {
55843
+ if (err instanceof LoticsRequestError && err.status === 409) {
55844
+ const said = typeof err.body.message === "string" ? err.body.message : err.message;
55845
+ if (err.body.code === BREAKING_API_CHANGE_CODE) {
55846
+ throw new Error(
55847
+ args.acknowledgeBreakingApi ? said : `${said}
55848
+ The flag that carries the acknowledgment here is: lotics app deploy --acknowledge-breaking-api`
55849
+ );
55850
+ }
55851
+ const served = "current_version_id" in err.body ? err.body.current_version_id : void 0;
55852
+ const stale = served !== void 0 && served !== (meta3.current_version_id ?? null);
55674
55853
  throw new Error(
55675
- `Deploy conflict: server is at version ${e.current_version_id ?? "unknown"} but local copy is based on ${meta3.current_version_id ?? "no version"}. Pull the latest before redeploying:
55676
- lotics app pull ${meta3.app_id}`
55854
+ stale ? `${said}
55855
+ Pull the latest before redeploying: lotics app pull ${meta3.app_id}` : said
55677
55856
  );
55678
55857
  }
55679
55858
  throw err;
@@ -56018,7 +56197,7 @@ function reportStoredBodiesOwedMigration(projectDir, failing) {
56018
56197
  Edit each body, then 'lotics app workflow set <alias>'. Until then the stored body keeps running as it always has.`
56019
56198
  );
56020
56199
  }
56021
- async function pushPendingBindings(client, projectDir, pending) {
56200
+ async function pushPendingBindings(client, projectDir, pending, acknowledgeBreakingApi) {
56022
56201
  const plan = [
56023
56202
  ...pending.queries.map((alias) => `query ${alias}`),
56024
56203
  ...pending.workflows.map((alias) => `workflow ${alias}`),
@@ -56029,10 +56208,10 @@ async function pushPendingBindings(client, projectDir, pending) {
56029
56208
  Pushing ${plan.length} binding${plan.length === 1 ? "" : "s"} ahead of the app: ${plan.join(", ")}`
56030
56209
  );
56031
56210
  for (const alias of pending.queries) {
56032
- await appQuerySet(client, { alias, projectDir });
56211
+ await appQuerySet(client, { alias, projectDir, acknowledgeBreakingApi });
56033
56212
  }
56034
56213
  for (const alias of pending.workflows) {
56035
- await appWorkflowSet(client, { alias, projectDir });
56214
+ await appWorkflowSet(client, { alias, projectDir, acknowledgeBreakingApi });
56036
56215
  }
56037
56216
  for (const alias of pending.agents) {
56038
56217
  await appAgentSet(client, { alias, projectDir });
@@ -56202,7 +56381,7 @@ function warnAboutOrphanedBindings(called, live, previouslyCalled, undeclared) {
56202
56381
  );
56203
56382
  return lines.length;
56204
56383
  }
56205
- async function pruneOrphanedBindings(client, projectDir, app, orphans, undeclared, dynamic, pruneInvoked) {
56384
+ async function pruneOrphanedBindings(client, projectDir, app, orphans, undeclared, dynamic, pruneInvoked, acknowledgeBreakingApi) {
56206
56385
  const stillBound = { queries: [], workflows: [], agents: [] };
56207
56386
  const targets = [
56208
56387
  ...orphans.queries.map((alias) => ({
@@ -56260,7 +56439,9 @@ async function pruneOrphanedBindings(client, projectDir, app, orphans, undeclare
56260
56439
  // Sent only for the aliases the operator named, so the default stays the
56261
56440
  // guard: a run recorded against an alias means something outside the
56262
56441
  // bundle reaches it.
56263
- ...overrideInvocationGuard ? { even_if_invoked: true } : {}
56442
+ ...overrideInvocationGuard ? { even_if_invoked: true } : {},
56443
+ // Agents carry no published entry, so the field is not theirs to take.
56444
+ ...acknowledgeBreakingApi && target.kind !== "agents" ? { acknowledge_breaking_api_change: true } : {}
56264
56445
  });
56265
56446
  if (res.error) {
56266
56447
  console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
@@ -56889,6 +57070,13 @@ function renderWorkflowIssue(issue2) {
56889
57070
  return issue2.suggestion ? `${head}
56890
57071
  ${issue2.suggestion}` : head;
56891
57072
  }
57073
+ function refusalAtThisSurface(res, command, acknowledged) {
57074
+ const said = res.error ?? "";
57075
+ const breaking = res.error_code === void 0 ? said.includes(ACKNOWLEDGMENT_ARG) : res.error_code === BREAKING_API_CHANGE_CODE;
57076
+ if (acknowledged === true || !breaking) return said;
57077
+ return `${said}
57078
+ The flag that carries the acknowledgment here is: ${command} --acknowledge-breaking-api`;
57079
+ }
56892
57080
  async function appWorkflowSet(client, args) {
56893
57081
  const projectDir = args.projectDir ?? process.cwd();
56894
57082
  const meta3 = readAppMeta(projectDir);
@@ -56930,10 +57118,19 @@ async function appWorkflowSet(client, args) {
56930
57118
  // The body this edit was built on. Turns the push into a conditional write:
56931
57119
  // if the live body moved since this project last saw it, the server refuses
56932
57120
  // rather than letting this copy overwrite a change nobody here has seen.
56933
- expected_body_sha: readSynced(projectDir).workflows[args.alias]?.live
57121
+ expected_body_sha: readSynced(projectDir).workflows[args.alias]?.live,
57122
+ // The author's answer to a published contract this body would break. Absent,
57123
+ // the server refuses such a write and names each change.
57124
+ acknowledge_breaking_api_change: args.acknowledgeBreakingApi
56934
57125
  });
56935
57126
  if (res.error) {
56936
- console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
57127
+ console.error(
57128
+ `Failed to set workflow "${args.alias}": ` + refusalAtThisSurface(
57129
+ res,
57130
+ `lotics app workflow set ${args.alias}`,
57131
+ args.acknowledgeBreakingApi
57132
+ )
57133
+ );
56937
57134
  process.exit(1);
56938
57135
  }
56939
57136
  const parsed = zod_default.object({
@@ -57007,11 +57204,16 @@ async function appQuerySet(client, args) {
57007
57204
  meta3.app_id,
57008
57205
  alias,
57009
57206
  declaration,
57010
- readSynced(projectDir).queries[alias]?.live
57207
+ readSynced(projectDir).queries[alias]?.live,
57208
+ // The author's answer to a published contract this declaration would
57209
+ // break. Absent, the server refuses such a write and names each change.
57210
+ args.acknowledgeBreakingApi
57011
57211
  );
57012
57212
  if (res.error) {
57013
57213
  if (pushed.length > 0) console.error(`Pushed before the failure: ${pushed.join(", ")}.`);
57014
- console.error(`Failed to set query "${alias}": ${res.error}`);
57214
+ console.error(
57215
+ `Failed to set query "${alias}": ` + refusalAtThisSurface(res, `lotics app query set ${alias}`, args.acknowledgeBreakingApi)
57216
+ );
57015
57217
  process.exit(1);
57016
57218
  }
57017
57219
  const querySha = declarationSha(declaration);
@@ -57405,6 +57607,7 @@ function parseArgs(argv) {
57405
57607
  cleanup: false,
57406
57608
  prune: false,
57407
57609
  pruneInvoked: [],
57610
+ acknowledgeBreakingApi: false,
57408
57611
  screens: false,
57409
57612
  screen: [],
57410
57613
  width: [],
@@ -57582,6 +57785,9 @@ function parseArgs(argv) {
57582
57785
  case "--prune":
57583
57786
  flags.prune = true;
57584
57787
  break;
57788
+ case "--acknowledge-breaking-api":
57789
+ flags.acknowledgeBreakingApi = true;
57790
+ break;
57585
57791
  case "--screens":
57586
57792
  flags.screens = true;
57587
57793
  break;
@@ -57691,6 +57897,8 @@ function saveApproval(state, pending, options) {
57691
57897
  requireNonEmpty(state.workspace_id, "workspace_id");
57692
57898
  upsertProfile(state.organization_id, {
57693
57899
  api_key: state.api_key,
57900
+ // A person pressed Confirm: this row IS their terminal's session.
57901
+ kind: "sign_in",
57694
57902
  org_name: state.organization_name,
57695
57903
  // The host that minted the key, taken from the request rather than re-read:
57696
57904
  // this is the whole of "which Lotics does this registration belong to".
@@ -58370,7 +58578,7 @@ async function signupAccount(args) {
58370
58578
  const orgId = data.organization_id;
58371
58579
  const workspaceId = data.workspace_id;
58372
58580
  const orgName = args.name || args.email.split("@")[0];
58373
- upsertProfile(orgId, { api_key: apiKey, org_name: orgName, api_url: apiUrl, workspace_id: workspaceId });
58581
+ upsertProfile(orgId, { api_key: apiKey, kind: "sign_in", org_name: orgName, api_url: apiUrl, workspace_id: workspaceId });
58374
58582
  const existing = loadGlobalConfig() ?? {};
58375
58583
  saveGlobalConfig({ ...existing, email: args.email });
58376
58584
  setActiveOrg(orgId, args.local ? "local" : "global");
@@ -58540,24 +58748,54 @@ Multiple workspaces in ${info.organization_name}. Run "lotics workspace select <
58540
58748
  }
58541
58749
  upsertProfile(info.organization_id, {
58542
58750
  api_key: apiKey,
58751
+ // Pasted in rather than signed in: nobody signs in as an API key, and
58752
+ // `auth whoami` says which this machine is holding.
58753
+ kind: "api_key",
58543
58754
  org_name: info.organization_name,
58544
58755
  api_url: apiUrl,
58545
58756
  workspace_id: workspaceId,
58546
58757
  workspace_name: workspaceName
58547
58758
  });
58548
58759
  noteServerOrgName(info.organization_id, info.organization_name);
58549
- const existing = loadGlobalConfig() ?? {};
58550
- saveGlobalConfig({ ...existing, email: info.email });
58760
+ if (info.email !== null) {
58761
+ saveGlobalConfig({ ...loadGlobalConfig(), email: info.email });
58762
+ }
58551
58763
  setActiveOrg(info.organization_id, local ? "local" : "global");
58552
58764
  clearPendingLogin();
58765
+ const who = info.email ?? info.name;
58553
58766
  if (local) {
58554
- console.error(`Authenticated as ${info.email} in ${info.organization_name}.`);
58767
+ console.error(`Authenticated as ${who} in ${info.organization_name}.`);
58555
58768
  console.error(` Pinned this directory: ${getConfigPath("local")}`);
58556
58769
  } else {
58557
- console.error(`Authenticated as ${info.email} in ${info.organization_name} (now active).`);
58770
+ console.error(`Authenticated as ${who} in ${info.organization_name} (now active).`);
58558
58771
  console.error(` Saved to the "${info.organization_name}" profile in ~/.lotics`);
58559
58772
  }
58560
58773
  }
58774
+ async function credentialIsSignIn(client, profile) {
58775
+ if (profile.kind) return profile.kind === "sign_in";
58776
+ try {
58777
+ return (await client.whoami()).credential_origin === "sign_in";
58778
+ } catch {
58779
+ return false;
58780
+ }
58781
+ }
58782
+ async function revokeCredential(orgId, profile) {
58783
+ const client = new LoticsClient({ apiKey: profile.api_key, apiUrl: profile.api_url });
58784
+ if (!await credentialIsSignIn(client, profile)) {
58785
+ console.error(
58786
+ `The "${profile.org_name}" key (${orgId}) was removed from this machine and is still active \u2014 revoke it yourself under Settings \u2192 Security \u2192 "Keys and terminals" if it acts as you, or ask an admin (Settings \u2192 API keys).`
58787
+ );
58788
+ return;
58789
+ }
58790
+ try {
58791
+ await client.revokeSelfCredential();
58792
+ } catch (error51) {
58793
+ const reason = (error51 instanceof Error ? error51.message : String(error51)).replace(/\s+/g, " ");
58794
+ console.error(
58795
+ `Could not revoke the "${profile.org_name}" credential (${orgId}) server-side (${reason}) \u2014 it is forgotten here, so revoke it under Settings \u2192 Security \u2192 "Keys and terminals" if it is still live.`
58796
+ );
58797
+ }
58798
+ }
58561
58799
  async function resolveContextOrClaim(flags, options) {
58562
58800
  const appWorkspaceId = options?.appWorkspaceId;
58563
58801
  const resolved = resolveContext(flags, appWorkspaceId);
@@ -59205,7 +59443,7 @@ async function main() {
59205
59443
  const client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl });
59206
59444
  const info = await client2.whoami();
59207
59445
  const existing = loadGlobalConfig() ?? {};
59208
- saveGlobalConfig({ ...existing, email: info.email });
59446
+ if (info.email !== null) saveGlobalConfig({ ...existing, email: info.email });
59209
59447
  const cached2 = existing.profiles?.[info.organization_id];
59210
59448
  if (cached2 && (cached2.server_name ?? cached2.org_name) !== info.organization_name) {
59211
59449
  noteServerOrgName(info.organization_id, info.organization_name);
@@ -59216,14 +59454,17 @@ async function main() {
59216
59454
  }
59217
59455
  }
59218
59456
  const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
59457
+ const kind = cached2?.kind ?? info.credential_origin ?? void 0;
59458
+ const kindLabel = kind === "sign_in" ? "sign-in (lotics auth login)" : kind === "api_key" ? "API key" : "unknown";
59219
59459
  if (flags.json) {
59220
- console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, api_url: ctx2.apiUrl, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
59460
+ console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, api_url: ctx2.apiUrl, source: ctx2.source, credential_kind: kind ?? null, view_as: viewAs ?? null }, null, 2));
59221
59461
  } else {
59222
59462
  console.log(`Name: ${info.name}`);
59223
- console.log(`Email: ${info.email}`);
59463
+ console.log(`Email: ${info.email ?? "(none: nobody signs in as this key)"}`);
59224
59464
  console.log(`Org: ${info.organization_name} (${info.organization_id})`);
59225
59465
  console.log(`Workspace: ${ctx2.workspaceId ?? "(none selected)"}`);
59226
59466
  console.log(`API: ${ctx2.apiUrl}`);
59467
+ console.log(`Credential: ${kindLabel}`);
59227
59468
  console.log(`Source: ${SOURCE_LABELS[ctx2.source]}`);
59228
59469
  if (viewAs) console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
59229
59470
  }
@@ -59238,6 +59479,9 @@ async function main() {
59238
59479
  }
59239
59480
  if (flags.all) {
59240
59481
  const g2 = loadGlobalConfig();
59482
+ for (const [orgId2, profile2] of Object.entries(g2?.profiles ?? {})) {
59483
+ await revokeCredential(orgId2, profile2);
59484
+ }
59241
59485
  saveGlobalConfig({ last_update_check: g2?.last_update_check, latest_version: g2?.latest_version });
59242
59486
  console.error("Removed all saved credentials.");
59243
59487
  return;
@@ -59257,6 +59501,7 @@ async function main() {
59257
59501
  process.exit(1);
59258
59502
  }
59259
59503
  const [orgId, profile] = resolved;
59504
+ await revokeCredential(orgId, profile);
59260
59505
  removeProfile(orgId);
59261
59506
  clearPendingLogin();
59262
59507
  console.error(`Removed the "${profile.org_name}" credential (${orgId}).`);
@@ -59417,6 +59662,9 @@ async function main() {
59417
59662
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
59418
59663
  console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
59419
59664
  console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
59665
+ console.error(" lotics app api publish|unpublish|status|spec [app_id]");
59666
+ console.error(" The app's API: publish the promise its queries and");
59667
+ console.error(" workflows make to callers outside it, or read it back");
59420
59668
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)");
59421
59669
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
59422
59670
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -59738,10 +59986,40 @@ Available workspaces:`);
59738
59986
  await appDeploy(client, {
59739
59987
  message: (flags.message ?? toolArgs)?.trim() || void 0,
59740
59988
  prune: flags.prune,
59741
- pruneInvoked: flags.pruneInvoked
59989
+ pruneInvoked: flags.pruneInvoked,
59990
+ acknowledgeBreakingApi: flags.acknowledgeBreakingApi
59742
59991
  });
59743
59992
  return;
59744
59993
  }
59994
+ if (subcommand === "api") {
59995
+ const action = toolArgs;
59996
+ const appId = restArgs.find((a) => !a.startsWith("-"));
59997
+ if (flags.json) setMachineOutput(true);
59998
+ if (action === "publish") {
59999
+ await appApiPublish(client, { app_id: appId });
60000
+ return;
60001
+ }
60002
+ if (action === "unpublish") {
60003
+ await appApiUnpublish(client, { app_id: appId });
60004
+ return;
60005
+ }
60006
+ if (action === "status") {
60007
+ await appApiStatus(client, { app_id: appId });
60008
+ return;
60009
+ }
60010
+ if (action === "spec") {
60011
+ await appApiSpec(client, { app_id: appId, outputPath: flags.output });
60012
+ return;
60013
+ }
60014
+ console.error("Usage:");
60015
+ console.error(" lotics app api publish [app_id] Publish the app's API and snapshot its contract");
60016
+ console.error(" lotics app api unpublish [app_id] Stop publishing it");
60017
+ console.error(" lotics app api status [app_id] Whether it publishes one, and which version");
60018
+ console.error(" lotics app api spec [app_id] [-o <file.json>]");
60019
+ console.error(" The OpenAPI document its callers read");
60020
+ console.error(" --json One object on stdout, and nothing else");
60021
+ process.exit(1);
60022
+ }
59745
60023
  if (subcommand === "subdomain") {
59746
60024
  const newSubdomain = toolArgs;
59747
60025
  if (!newSubdomain) {
@@ -59768,7 +60046,10 @@ Available workspaces:`);
59768
60046
  return;
59769
60047
  }
59770
60048
  if (subcommand === "upgrade") {
59771
- await appUpgrade(client, { app_id: toolArgs || void 0 });
60049
+ await appUpgrade(client, {
60050
+ app_id: toolArgs || void 0,
60051
+ acknowledgeBreakingApi: flags.acknowledgeBreakingApi
60052
+ });
59772
60053
  return;
59773
60054
  }
59774
60055
  if (subcommand === "check") {
@@ -59808,7 +60089,10 @@ Available workspaces:`);
59808
60089
  console.error("Pushes the edited src/workflows/<alias>.ts body via set_app_workflow.");
59809
60090
  process.exit(1);
59810
60091
  }
59811
- await appWorkflowSet(client, { alias });
60092
+ await appWorkflowSet(client, {
60093
+ alias,
60094
+ acknowledgeBreakingApi: flags.acknowledgeBreakingApi
60095
+ });
59812
60096
  return;
59813
60097
  }
59814
60098
  if (action === "pull") {
@@ -59844,7 +60128,10 @@ Available workspaces:`);
59844
60128
  const action = toolArgs;
59845
60129
  if (action === "set") {
59846
60130
  if (flags.all) {
59847
- await appQuerySet(client, { all: true });
60131
+ await appQuerySet(client, {
60132
+ all: true,
60133
+ acknowledgeBreakingApi: flags.acknowledgeBreakingApi
60134
+ });
59848
60135
  return;
59849
60136
  }
59850
60137
  const alias = restArgs.find((a) => !a.startsWith("--"));
@@ -59855,7 +60142,7 @@ Available workspaces:`);
59855
60142
  );
59856
60143
  process.exit(1);
59857
60144
  }
59858
- await appQuerySet(client, { alias });
60145
+ await appQuerySet(client, { alias, acknowledgeBreakingApi: flags.acknowledgeBreakingApi });
59859
60146
  return;
59860
60147
  }
59861
60148
  console.error("Usage:");