@primitivedotdev/cli 0.32.0 → 0.33.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.
@@ -1,11 +1,11 @@
1
1
  import { Args, Command, Errors, Flags } from "@oclif/core";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { randomUUID } from "node:crypto";
4
- import { basename, dirname, join, resolve } from "node:path";
5
- import { spawn } from "node:child_process";
4
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
5
  import { hostname } from "node:os";
7
6
  import process$1 from "node:process";
8
7
  import { createInterface } from "node:readline/promises";
8
+ import { spawn } from "node:child_process";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __defProp = Object.defineProperty;
11
11
  var __exportAll = (all, no_symbols) => {
@@ -6769,16 +6769,21 @@ const openapiDocument = {
6769
6769
  "type": "string",
6770
6770
  "minLength": 1,
6771
6771
  "maxLength": 1048576,
6772
- "description": "Bundled handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject.\n"
6772
+ "description": "Pre-built handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject. Provide either `code` or `files`, not both.\n"
6773
6773
  },
6774
6774
  "sourceMap": {
6775
6775
  "type": "string",
6776
6776
  "minLength": 1,
6777
6777
  "maxLength": 5242880,
6778
- "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
6778
+ "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs. Only\nvalid with `code`.\n"
6779
+ },
6780
+ "files": {
6781
+ "type": "object",
6782
+ "additionalProperties": { "type": "string" },
6783
+ "description": "Source files for a managed build, as a map of path to file\ncontents (for example {\"package.json\": \"...\",\n\"src/index.ts\": \"...\"}). Provide this INSTEAD of `code` to\nhave the server install dependencies and bundle the source\nfor the Workers runtime before deploying. Include a\npackage.json (its `dependencies` are installed). Provide\neither `code` or `files`, not both.\n"
6779
6784
  }
6780
6785
  },
6781
- "required": ["name", "code"]
6786
+ "required": ["name"]
6782
6787
  },
6783
6788
  "CreateFunctionResult": {
6784
6789
  "type": "object",
@@ -6805,15 +6810,20 @@ const openapiDocument = {
6805
6810
  "type": "string",
6806
6811
  "minLength": 1,
6807
6812
  "maxLength": 1048576,
6808
- "description": "New bundled handler. Same rules as CreateFunctionInput.code."
6813
+ "description": "New pre-built handler. Same rules as CreateFunctionInput.code. Provide either `code` or `files`, not both."
6809
6814
  },
6810
6815
  "sourceMap": {
6811
6816
  "type": "string",
6812
6817
  "minLength": 1,
6813
6818
  "maxLength": 5242880
6819
+ },
6820
+ "files": {
6821
+ "type": "object",
6822
+ "additionalProperties": { "type": "string" },
6823
+ "description": "Source files for a managed build, as a map of path to file\ncontents. Provide this INSTEAD of `code` to rebuild and\nredeploy from source. Same rules as CreateFunctionInput.files.\n"
6814
6824
  }
6815
6825
  },
6816
- "required": ["code"]
6826
+ "required": []
6817
6827
  },
6818
6828
  "TestInvocationResult": {
6819
6829
  "type": "object",
@@ -10512,16 +10522,21 @@ const operationManifest = [
10512
10522
  "type": "string",
10513
10523
  "minLength": 1,
10514
10524
  "maxLength": 1048576,
10515
- "description": "Bundled handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject.\n"
10525
+ "description": "Pre-built handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject. Provide either `code` or `files`, not both.\n"
10516
10526
  },
10517
10527
  "sourceMap": {
10518
10528
  "type": "string",
10519
10529
  "minLength": 1,
10520
10530
  "maxLength": 5242880,
10521
- "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
10531
+ "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs. Only\nvalid with `code`.\n"
10532
+ },
10533
+ "files": {
10534
+ "type": "object",
10535
+ "additionalProperties": { "type": "string" },
10536
+ "description": "Source files for a managed build, as a map of path to file\ncontents (for example {\"package.json\": \"...\",\n\"src/index.ts\": \"...\"}). Provide this INSTEAD of `code` to\nhave the server install dependencies and bundle the source\nfor the Workers runtime before deploying. Include a\npackage.json (its `dependencies` are installed). Provide\neither `code` or `files`, not both.\n"
10522
10537
  }
10523
10538
  },
10524
- "required": ["name", "code"]
10539
+ "required": ["name"]
10525
10540
  },
10526
10541
  "responseSchema": {
10527
10542
  "type": "object",
@@ -11574,15 +11589,20 @@ const operationManifest = [
11574
11589
  "type": "string",
11575
11590
  "minLength": 1,
11576
11591
  "maxLength": 1048576,
11577
- "description": "New bundled handler. Same rules as CreateFunctionInput.code."
11592
+ "description": "New pre-built handler. Same rules as CreateFunctionInput.code. Provide either `code` or `files`, not both."
11578
11593
  },
11579
11594
  "sourceMap": {
11580
11595
  "type": "string",
11581
11596
  "minLength": 1,
11582
11597
  "maxLength": 5242880
11598
+ },
11599
+ "files": {
11600
+ "type": "object",
11601
+ "additionalProperties": { "type": "string" },
11602
+ "description": "Source files for a managed build, as a map of path to file\ncontents. Provide this INSTEAD of `code` to rebuild and\nredeploy from source. Same rules as CreateFunctionInput.files.\n"
11583
11603
  }
11584
11604
  },
11585
- "required": ["code"]
11605
+ "required": []
11586
11606
  },
11587
11607
  "responseSchema": {
11588
11608
  "type": "object",
@@ -13133,8 +13153,14 @@ var PrimitiveApiClient = class {
13133
13153
  //#region src/oclif/auth.ts
13134
13154
  const CREDENTIALS_FILE = "credentials.json";
13135
13155
  const CREDENTIALS_LOCK_DIR = "credentials.lock";
13156
+ const CREDENTIALS_LOCK_OWNER_FILE = "owner.json";
13136
13157
  const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
13137
13158
  const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive signin`.";
13159
+ const CREDENTIALS_LOCK_CLEANUP_SIGNALS = [
13160
+ "SIGINT",
13161
+ "SIGTERM",
13162
+ "SIGHUP"
13163
+ ];
13138
13164
  function isRecord$2(value) {
13139
13165
  return value !== null && typeof value === "object" && !Array.isArray(value);
13140
13166
  }
@@ -13181,6 +13207,9 @@ function parseCredentials(raw) {
13181
13207
  function credentialsPath(configDir) {
13182
13208
  return join(configDir, CREDENTIALS_FILE);
13183
13209
  }
13210
+ function credentialsLockPath(configDir) {
13211
+ return join(configDir, CREDENTIALS_LOCK_DIR);
13212
+ }
13184
13213
  function normalize(url, fallback) {
13185
13214
  const trimmed = url?.trim();
13186
13215
  if (!trimmed) return fallback;
@@ -13239,6 +13268,12 @@ function saveCliCredentials(configDir, credentials) {
13239
13268
  function deleteCliCredentials(configDir) {
13240
13269
  rmSync(credentialsPath(configDir), { force: true });
13241
13270
  }
13271
+ function deleteCliCredentialsLock(configDir) {
13272
+ rmSync(credentialsLockPath(configDir), {
13273
+ force: true,
13274
+ recursive: true
13275
+ });
13276
+ }
13242
13277
  function errorCode(error) {
13243
13278
  return error && typeof error === "object" ? error.code : void 0;
13244
13279
  }
@@ -13256,12 +13291,85 @@ function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
13256
13291
  });
13257
13292
  return true;
13258
13293
  }
13294
+ function readCliCredentialsLockOwner(lockPath) {
13295
+ let raw;
13296
+ try {
13297
+ raw = readFileSync(join(lockPath, CREDENTIALS_LOCK_OWNER_FILE), "utf8");
13298
+ } catch (error) {
13299
+ if (errorCode(error) === "ENOENT") return null;
13300
+ throw error;
13301
+ }
13302
+ try {
13303
+ const pid = JSON.parse(raw)?.pid;
13304
+ return Number.isInteger(pid) && pid > 0 ? { pid } : null;
13305
+ } catch {
13306
+ return null;
13307
+ }
13308
+ }
13309
+ function processIsRunning(pid) {
13310
+ try {
13311
+ process.kill(pid, 0);
13312
+ return true;
13313
+ } catch (error) {
13314
+ if (errorCode(error) === "ESRCH") return false;
13315
+ return true;
13316
+ }
13317
+ }
13318
+ function removeRecoverableCliCredentialsLock(params) {
13319
+ const owner = readCliCredentialsLockOwner(params.lockPath);
13320
+ if (owner && params.isRunning(owner.pid)) return false;
13321
+ if (owner) {
13322
+ rmSync(params.lockPath, {
13323
+ force: true,
13324
+ recursive: true
13325
+ });
13326
+ return true;
13327
+ }
13328
+ return removeStaleCliCredentialsLock(params.lockPath, params.staleMs, params.now);
13329
+ }
13330
+ function writeCliCredentialsLockOwner(lockPath) {
13331
+ const ownerPath = join(lockPath, CREDENTIALS_LOCK_OWNER_FILE);
13332
+ writeFileSync(ownerPath, `${JSON.stringify({
13333
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
13334
+ pid: process.pid
13335
+ })}\n`, { mode: 384 });
13336
+ chmodSync(ownerPath, 384);
13337
+ }
13338
+ function installCredentialsLockSignalCleanup(lockPath) {
13339
+ let active = true;
13340
+ const listeners = CREDENTIALS_LOCK_CLEANUP_SIGNALS.map((signal) => {
13341
+ const listener = () => {
13342
+ if (!active) return;
13343
+ active = false;
13344
+ rmSync(lockPath, {
13345
+ force: true,
13346
+ recursive: true
13347
+ });
13348
+ process.exit(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129);
13349
+ };
13350
+ process.once(signal, listener);
13351
+ return {
13352
+ listener,
13353
+ signal
13354
+ };
13355
+ });
13356
+ return () => {
13357
+ if (!active) return;
13358
+ active = false;
13359
+ for (const { listener, signal } of listeners) process.removeListener(signal, listener);
13360
+ };
13361
+ }
13362
+ function credentialsLockInProgressMessage(lockPath) {
13363
+ return `Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry. If no Primitive auth command is still running, run \`primitive logout --force\` to clear local CLI auth state and remove ${lockPath}.`;
13364
+ }
13259
13365
  function acquireCliCredentialsLock(configDir, options = {}) {
13260
13366
  mkdirSync(configDir, {
13261
13367
  mode: 448,
13262
13368
  recursive: true
13263
13369
  });
13264
- const lockPath = join(configDir, CREDENTIALS_LOCK_DIR);
13370
+ const lockPath = credentialsLockPath(configDir);
13371
+ const installSignalHandlers = options.installSignalHandlers ?? true;
13372
+ const isRunning = options.isProcessRunning ?? processIsRunning;
13265
13373
  const now = options.now ?? Date.now;
13266
13374
  const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
13267
13375
  let acquired = false;
@@ -13271,14 +13379,30 @@ function acquireCliCredentialsLock(configDir, options = {}) {
13271
13379
  break;
13272
13380
  } catch (error) {
13273
13381
  if (errorCode(error) !== "EEXIST") throw error;
13274
- if (removeStaleCliCredentialsLock(lockPath, staleMs, now)) continue;
13275
- throw new Error("Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.");
13382
+ if (removeRecoverableCliCredentialsLock({
13383
+ isRunning,
13384
+ lockPath,
13385
+ now,
13386
+ staleMs
13387
+ })) continue;
13388
+ throw new Error(credentialsLockInProgressMessage(lockPath));
13389
+ }
13390
+ if (!acquired) throw new Error(credentialsLockInProgressMessage(lockPath));
13391
+ try {
13392
+ writeCliCredentialsLockOwner(lockPath);
13393
+ } catch (error) {
13394
+ rmSync(lockPath, {
13395
+ force: true,
13396
+ recursive: true
13397
+ });
13398
+ throw error;
13276
13399
  }
13277
- if (!acquired) throw new Error("Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.");
13400
+ const removeSignalCleanup = installSignalHandlers ? installCredentialsLockSignalCleanup(lockPath) : () => void 0;
13278
13401
  let released = false;
13279
13402
  return () => {
13280
13403
  if (released) return;
13281
13404
  released = true;
13405
+ removeSignalCleanup();
13282
13406
  rmSync(lockPath, {
13283
13407
  force: true,
13284
13408
  recursive: true
@@ -13298,7 +13422,7 @@ function resolveCliAuth(params) {
13298
13422
  const credentials = loadCliCredentials(params.configDir);
13299
13423
  if (credentials) return {
13300
13424
  apiKey: credentials.access_token,
13301
- apiBaseUrl1: params.apiBaseUrl1 ? normalizeApiBaseUrl1(params.apiBaseUrl1) : credentials.api_base_url_1,
13425
+ apiBaseUrl1: credentials.api_base_url_1,
13302
13426
  apiBaseUrl2,
13303
13427
  credentials,
13304
13428
  source: "stored"
@@ -14051,7 +14175,10 @@ const RESERVED_FLAG_NAMES = new Set([
14051
14175
  ]);
14052
14176
  function bodyFieldFlag(field) {
14053
14177
  const common = { description: field.description || field.name };
14054
- if (field.kind === "boolean") return Flags.boolean(common);
14178
+ if (field.kind === "boolean") return Flags.boolean({
14179
+ ...common,
14180
+ allowNo: true
14181
+ });
14055
14182
  if (field.kind === "integer") return Flags.integer({
14056
14183
  ...common,
14057
14184
  ...numericFlagOptions(field)
@@ -14084,7 +14211,10 @@ function buildFlags(operation) {
14084
14211
  }),
14085
14212
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14086
14213
  };
14087
- if (!operation.binaryResponse) flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
14214
+ if (!operation.binaryResponse) {
14215
+ flags.json = Flags.boolean({ description: "Accepted for consistency with task-focused commands. Generated API commands already print JSON by default." });
14216
+ flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
14217
+ }
14088
14218
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
14089
14219
  const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
14090
14220
  if (operation.hasJsonBody) {
@@ -14126,11 +14256,21 @@ function collectValues(parameters, flags) {
14126
14256
  function operationOutputPayload(envelope, includeEnvelope) {
14127
14257
  return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
14128
14258
  }
14259
+ function isIncompleteDomainVerification(operation, envelope) {
14260
+ if (operation.sdkName !== "verifyDomain") return false;
14261
+ const data = envelope?.data;
14262
+ if (!data || typeof data !== "object") return false;
14263
+ return data.verified === false;
14264
+ }
14265
+ function writeIncompleteDomainVerificationHint() {
14266
+ process.stderr.write("Domain verification is incomplete. Add or fix the DNS records shown above, or run `primitive domains zone-file --id <domain-id>` to download the complete zone file, then retry `primitive domains verify --id <domain-id>`.\n");
14267
+ }
14129
14268
  const OPERATION_HINTS = {
14130
14269
  addDomain: "Tip: after this returns a domain id, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` when the user wants an importable DNS zone file.",
14131
14270
  verifyDomain: "Tip: if DNS is still missing, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` to give the user an importable DNS zone file.",
14132
14271
  downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
14133
14272
  getInboxStatus: "Tip: prefer `primitive inbox status` for a compact readiness summary and next-step commands.",
14273
+ getSendPermissions: "Tip: this command answers where you may send mail to. To find usable sender domains for --from, run `primitive domains list` or `primitive inbox status` and use an address at an active verified domain.",
14134
14274
  sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
14135
14275
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
14136
14276
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
@@ -14233,6 +14373,10 @@ function createOperationCommand(operation) {
14233
14373
  process.stderr.write(chunk);
14234
14374
  } });
14235
14375
  this.log(JSON.stringify(operationOutputPayload(envelope, parsedFlags.envelope === true), null, 2));
14376
+ if (isIncompleteDomainVerification(operation, envelope)) {
14377
+ writeIncompleteDomainVerificationHint();
14378
+ process.exitCode = 1;
14379
+ }
14236
14380
  });
14237
14381
  }
14238
14382
  }
@@ -14348,6 +14492,13 @@ function cursorFromRows(rows) {
14348
14492
  const last = rows.at(-1);
14349
14493
  return last ? encodeReceivedAtSearchCursor(last) : null;
14350
14494
  }
14495
+ function cursorFromAcceptedRows(rows) {
14496
+ for (let i = rows.length - 1; i >= 0; i--) {
14497
+ const row = rows[i];
14498
+ if (row.status === "accepted" || row.status === "completed") return encodeReceivedAtSearchCursor(row);
14499
+ }
14500
+ return null;
14501
+ }
14351
14502
  function collectNewAcceptedEmails(rows, seenIds) {
14352
14503
  const fresh = [];
14353
14504
  for (const row of rows) {
@@ -14613,41 +14764,40 @@ function buildChatFollowUpCommands(context) {
14613
14764
  return commands;
14614
14765
  }
14615
14766
  function buildChatRecoveryCommands(context) {
14616
- return [
14617
- buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
14618
- "primitive",
14619
- "emails",
14620
- "wait",
14621
- "--reply-to-sent-email-id",
14622
- context.sent.id,
14623
- "--to",
14624
- context.from,
14625
- "--since",
14626
- context.sentAtIso,
14627
- "--timeout",
14628
- String(context.timeoutSeconds)
14629
- ]),
14630
- buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
14631
- "primitive",
14632
- "emails",
14633
- "wait",
14634
- "--from",
14635
- context.recipient,
14636
- "--to",
14637
- context.from,
14638
- "--since",
14639
- context.sentAtIso,
14640
- "--timeout",
14641
- String(context.timeoutSeconds)
14642
- ]),
14643
- buildCommand("inspect_sent_email", "Inspect the outbound send", [
14644
- "primitive",
14645
- "sent",
14646
- "get",
14647
- "--id",
14648
- context.sent.id
14649
- ])
14650
- ];
14767
+ const commands = [buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
14768
+ "primitive",
14769
+ "emails",
14770
+ "wait",
14771
+ "--reply-to-sent-email-id",
14772
+ context.sent.id,
14773
+ "--to",
14774
+ context.from,
14775
+ "--since",
14776
+ context.sentAtIso,
14777
+ "--timeout",
14778
+ String(context.timeoutSeconds)
14779
+ ])];
14780
+ if (!context.strictOnly) commands.push(buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
14781
+ "primitive",
14782
+ "emails",
14783
+ "wait",
14784
+ "--from",
14785
+ context.recipient,
14786
+ "--to",
14787
+ context.from,
14788
+ "--since",
14789
+ context.sentAtIso,
14790
+ "--timeout",
14791
+ String(context.timeoutSeconds)
14792
+ ]));
14793
+ commands.push(buildCommand("inspect_sent_email", "Inspect the outbound send", [
14794
+ "primitive",
14795
+ "sent",
14796
+ "get",
14797
+ "--id",
14798
+ context.sent.id
14799
+ ]));
14800
+ return commands;
14651
14801
  }
14652
14802
  function buildChatJsonEnvelope(context) {
14653
14803
  const responseBody = resolveChatResponseBody(context.reply);
@@ -14794,7 +14944,7 @@ var ChatCommand = class ChatCommand extends Command {
14794
14944
  static summary = "Chat with an agent over email (send and wait for the reply)";
14795
14945
  static examples = [
14796
14946
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14797
- "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
14947
+ "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14798
14948
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14799
14949
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14800
14950
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
@@ -14823,7 +14973,10 @@ var ChatCommand = class ChatCommand extends Command {
14823
14973
  hidden: true
14824
14974
  }),
14825
14975
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
14826
- subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
14976
+ subject: Flags.string({
14977
+ description: "Advanced email transport override. Usually omit; chat threading does not depend on the subject.",
14978
+ hidden: true
14979
+ }),
14827
14980
  reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
14828
14981
  "reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
14829
14982
  "in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
@@ -15130,6 +15283,38 @@ function redactConfig(config) {
15130
15283
  environments: Object.fromEntries(Object.entries(config.environments).map(([name, environment]) => [name, redactCliEnvironment(environment)]))
15131
15284
  };
15132
15285
  }
15286
+ function upsertCliEnvironmentAndClearCredentialsIfSwitched(params) {
15287
+ const previousConfig = loadOrCreateConfig(params.configDir);
15288
+ const previousActiveEnvironment = resolveConfigEnvironment(previousConfig);
15289
+ const previousEnvironment = previousActiveEnvironment?.name ?? null;
15290
+ const config = upsertCliEnvironment({
15291
+ apiBaseUrl1: params.apiBaseUrl1,
15292
+ apiBaseUrl2: params.apiBaseUrl2,
15293
+ config: previousConfig,
15294
+ environmentName: params.environmentName,
15295
+ headers: params.headers,
15296
+ unsetHeaders: params.unsetHeaders
15297
+ });
15298
+ const activeEnvironment = resolveConfigEnvironment(config);
15299
+ const environment = activeEnvironment?.name ?? null;
15300
+ const shouldClearCredentials = existsSync(credentialsPath(params.configDir)) && (previousEnvironment !== environment || previousActiveEnvironment?.config.api_base_url_1 !== activeEnvironment?.config.api_base_url_1);
15301
+ let removedCredentials = false;
15302
+ if (shouldClearCredentials) {
15303
+ const releaseLock = acquireCliCredentialsLock(params.configDir);
15304
+ try {
15305
+ saveCliConfig(params.configDir, config);
15306
+ removedCredentials = existsSync(credentialsPath(params.configDir));
15307
+ deleteCliCredentials(params.configDir);
15308
+ } finally {
15309
+ releaseLock();
15310
+ }
15311
+ } else saveCliConfig(params.configDir, config);
15312
+ return {
15313
+ environment,
15314
+ previousEnvironment,
15315
+ removedCredentials
15316
+ };
15317
+ }
15133
15318
  function switchCliEnvironment(configDir, environmentName) {
15134
15319
  const environment = normalizeCliEnvironmentName(environmentName);
15135
15320
  const config = loadOrCreateConfig(configDir);
@@ -15179,16 +15364,16 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
15179
15364
  const { flags } = await this.parse(ConfigSetCommand);
15180
15365
  const headers = flags.header ?? [];
15181
15366
  if (flags["api-base-url-1"] === void 0 && flags["api-base-url-2"] === void 0 && headers.length === 0 && (flags["unset-header"] ?? []).length === 0) throw new Errors.CLIError("Nothing to set. Pass an API base URL, --header, or --unset-header.", { exit: 1 });
15182
- const config = upsertCliEnvironment({
15367
+ const { environment, removedCredentials } = upsertCliEnvironmentAndClearCredentialsIfSwitched({
15183
15368
  apiBaseUrl1: flags["api-base-url-1"],
15184
15369
  apiBaseUrl2: flags["api-base-url-2"],
15185
- config: loadOrCreateConfig(this.config.configDir),
15370
+ configDir: this.config.configDir,
15186
15371
  environmentName: flags.environment,
15187
15372
  headers,
15188
15373
  unsetHeaders: flags["unset-header"]
15189
15374
  });
15190
- saveCliConfig(this.config.configDir, config);
15191
- process.stderr.write(`Primitive CLI environment ${config.current_environment} is active.\n`);
15375
+ process.stderr.write(`Primitive CLI environment ${environment} is active.\n`);
15376
+ if (removedCredentials) process.stderr.write("Removed saved Primitive CLI credentials. Run `primitive signin` to authenticate in the active environment.\n");
15192
15377
  }
15193
15378
  };
15194
15379
  var ConfigUseCommand = class ConfigUseCommand extends Command {
@@ -15878,7 +16063,9 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
15878
16063
  process.exitCode = 1;
15879
16064
  return;
15880
16065
  }
15881
- cursor = page.cursor ?? cursor;
16066
+ const nextCursor = cursorFromAcceptedRows(page.rows);
16067
+ const cursorAdvanced = Boolean(nextCursor && nextCursor !== cursor);
16068
+ if (nextCursor) cursor = nextCursor;
15882
16069
  for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
15883
16070
  if (flags.table) {
15884
16071
  if (!headerPrinted) {
@@ -15890,7 +16077,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
15890
16077
  matched += 1;
15891
16078
  if (matched >= flags.number) return;
15892
16079
  }
15893
- if (page.rows.length > 0) continue;
16080
+ if (cursorAdvanced) continue;
15894
16081
  if (deadline !== null && Date.now() >= deadline) break;
15895
16082
  await sleep$1(flags.interval * 1e3);
15896
16083
  }
@@ -16000,7 +16187,9 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
16000
16187
  process.exitCode = 1;
16001
16188
  return;
16002
16189
  }
16003
- cursor = page.cursor ?? cursor;
16190
+ const nextCursor = cursorFromAcceptedRows(page.rows);
16191
+ const cursorAdvanced = Boolean(nextCursor && nextCursor !== cursor);
16192
+ if (nextCursor) cursor = nextCursor;
16004
16193
  for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
16005
16194
  if (flags.jsonl) this.log(JSON.stringify(email));
16006
16195
  else {
@@ -16013,7 +16202,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
16013
16202
  printed += 1;
16014
16203
  if (flags.number && printed >= flags.number) return;
16015
16204
  }
16016
- if (page.rows.length > 0) continue;
16205
+ if (cursorAdvanced) continue;
16017
16206
  if (deadline !== null && Date.now() >= deadline) break;
16018
16207
  await sleep$1(flags.interval * 1e3);
16019
16208
  }
@@ -16105,6 +16294,129 @@ async function waitForFunctionDeploy(params) {
16105
16294
  }
16106
16295
  }
16107
16296
  //#endregion
16297
+ //#region src/oclif/function-source.ts
16298
+ function collectSourceFiles(dir) {
16299
+ let pkgRaw;
16300
+ try {
16301
+ pkgRaw = readFileSync(join(dir, "package.json"), "utf8");
16302
+ } catch {
16303
+ return {
16304
+ kind: "error",
16305
+ message: `No package.json found in ${dir}. A managed build needs a package.json (its "dependencies" are installed).`
16306
+ };
16307
+ }
16308
+ let pkg;
16309
+ try {
16310
+ pkg = JSON.parse(pkgRaw);
16311
+ } catch (error) {
16312
+ return {
16313
+ kind: "error",
16314
+ message: `package.json in ${dir} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
16315
+ };
16316
+ }
16317
+ delete pkg.devDependencies;
16318
+ const files = { "package.json": `${JSON.stringify(pkg, null, 2)}\n` };
16319
+ const srcDir = join(dir, "src");
16320
+ if (isDirectory(srcDir)) for (const abs of walk(srcDir)) files[relative(dir, abs).split(sep).join("/")] = readFileSync(abs, "utf8");
16321
+ if (Object.keys(files).length === 1) return {
16322
+ kind: "error",
16323
+ message: `No source files found under ${srcDir}. Put your handler at src/index.ts.`
16324
+ };
16325
+ return {
16326
+ kind: "ok",
16327
+ files
16328
+ };
16329
+ }
16330
+ function isDirectory(path) {
16331
+ try {
16332
+ return statSync(path).isDirectory();
16333
+ } catch {
16334
+ return false;
16335
+ }
16336
+ }
16337
+ function* walk(dir) {
16338
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
16339
+ const abs = join(dir, entry.name);
16340
+ if (entry.isDirectory()) yield* walk(abs);
16341
+ else yield abs;
16342
+ }
16343
+ }
16344
+ async function runSourceDeploy(api, params) {
16345
+ const listed = await api.listFunctions();
16346
+ if (listed.error) return {
16347
+ kind: "error",
16348
+ payload: extractErrorPayload(listed.error),
16349
+ stage: "lookup"
16350
+ };
16351
+ const foundId = (listed.data?.data ?? []).find((f) => f.name === params.name)?.id ?? null;
16352
+ if (foundId !== null) {
16353
+ const updated = await api.updateFunction({
16354
+ files: params.files,
16355
+ id: foundId
16356
+ });
16357
+ if (updated.error) return {
16358
+ kind: "error",
16359
+ payload: extractErrorPayload(updated.error),
16360
+ stage: "redeploy"
16361
+ };
16362
+ const data = updated.data?.data;
16363
+ if (!data) return {
16364
+ kind: "error",
16365
+ payload: {
16366
+ code: "client_error",
16367
+ message: "Redeploy returned no data"
16368
+ },
16369
+ stage: "redeploy"
16370
+ };
16371
+ return {
16372
+ action: "redeployed",
16373
+ kind: "ok",
16374
+ result: data
16375
+ };
16376
+ }
16377
+ const created = await api.createFunction({
16378
+ files: params.files,
16379
+ name: params.name
16380
+ });
16381
+ if (created.error) return {
16382
+ kind: "error",
16383
+ payload: extractErrorPayload(created.error),
16384
+ stage: "create"
16385
+ };
16386
+ const data = created.data?.data;
16387
+ if (!data) return {
16388
+ kind: "error",
16389
+ payload: {
16390
+ code: "client_error",
16391
+ message: "Create returned no data"
16392
+ },
16393
+ stage: "create"
16394
+ };
16395
+ return {
16396
+ action: "created",
16397
+ kind: "ok",
16398
+ result: data
16399
+ };
16400
+ }
16401
+ function renderBuildFailure(payload, write) {
16402
+ if (typeof payload !== "object" || payload === null) return false;
16403
+ const error = payload.error ?? payload;
16404
+ if (typeof error !== "object" || error === null) return false;
16405
+ if (error.code !== "build_failed") return false;
16406
+ const details = error.details;
16407
+ const phase = typeof details === "object" && details !== null ? details.phase : void 0;
16408
+ write(`Build failed${typeof phase === "string" ? ` during ${phase}` : ""}.\n`);
16409
+ const errors = typeof details === "object" && details !== null ? details.errors : void 0;
16410
+ if (Array.isArray(errors)) for (const e of errors) {
16411
+ if (typeof e !== "object" || e === null) continue;
16412
+ const item = e;
16413
+ const loc = typeof item.file === "string" ? ` (${item.file}${typeof item.line === "number" ? `:${item.line}` : ""})` : "";
16414
+ write(` [${String(item.code)}] ${String(item.message)}${loc}\n`);
16415
+ if (typeof item.hint === "string") write(` hint: ${item.hint}\n`);
16416
+ }
16417
+ return true;
16418
+ }
16419
+ //#endregion
16108
16420
  //#region src/oclif/lint/raw-send-mail-fetch.ts
16109
16421
  const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
16110
16422
  const SNIPPET_PADDING = 60;
@@ -16571,6 +16883,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16571
16883
  static summary = "Deploy a new function from a bundled handler file";
16572
16884
  static examples = [
16573
16885
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
16886
+ "<%= config.bin %> functions deploy --name triage --source ./triage-agent",
16887
+ "<%= config.bin %> functions deploy --name triage --source . --wait",
16574
16888
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --wait",
16575
16889
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
16576
16890
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
@@ -16596,10 +16910,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16596
16910
  description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
16597
16911
  required: true
16598
16912
  }),
16599
- file: Flags.string({
16600
- description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
16601
- required: true
16602
- }),
16913
+ file: Flags.string({ description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field. Exactly one of --file or --source is required." }),
16914
+ source: Flags.string({ description: "Path to a project directory (containing package.json and src/) to deploy via managed build: the source is uploaded and the server installs dependencies, bundles for the Workers runtime, and deploys. Idempotent by name (creates the function, or redeploys it if --name already exists), so it is safe to run on every push. Exactly one of --file or --source is required." }),
16603
16915
  "source-map-file": Flags.string({ description: "Optional path to a source map for the bundle. Stored with the deployment attempt and used to symbolicate stack traces in function logs." }),
16604
16916
  secret: Flags.string({
16605
16917
  description: `Secret KEY=VALUE to seed on the deployed function. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out the deploy to create-function, set-secret per pair, then a final redeploy so the running handler picks up the bindings. ${SECRET_FLAG_SECURITY_NOTE}`,
@@ -16641,6 +16953,17 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16641
16953
  process.exitCode = 1;
16642
16954
  return;
16643
16955
  }
16956
+ if (flags.file === void 0 === (flags.source === void 0)) {
16957
+ process.stderr.write("Provide exactly one of --file (a pre-built bundle) or --source (a project directory for managed build).\n");
16958
+ process.exitCode = 1;
16959
+ return;
16960
+ }
16961
+ if (flags.source !== void 0) {
16962
+ await this.runSourceMode(flags, flags.source);
16963
+ return;
16964
+ }
16965
+ const file = flags.file;
16966
+ if (file === void 0) return;
16644
16967
  const parsedSecrets = resolveSecretFlags({
16645
16968
  fromEnv: flags["secret-from-env"] ?? [],
16646
16969
  fromEnvFile: flags["secret-from-env-file"] ?? [],
@@ -16653,7 +16976,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16653
16976
  process.exitCode = 1;
16654
16977
  return;
16655
16978
  }
16656
- const code = readTextFileFlag(flags.file, "--file");
16979
+ const code = readTextFileFlag(file, "--file");
16657
16980
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
16658
16981
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
16659
16982
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
@@ -16759,6 +17082,101 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16759
17082
  this.log(JSON.stringify(payload, null, 2));
16760
17083
  });
16761
17084
  }
17085
+ async runSourceMode(flags, sourceDir) {
17086
+ if ((flags.secret?.length ?? 0) > 0 || (flags["secret-from-env"]?.length ?? 0) > 0 || (flags["secret-from-file"]?.length ?? 0) > 0 || (flags["secret-from-env-file"]?.length ?? 0) > 0 || flags["secret-from-stdin"] !== void 0) {
17087
+ process.stderr.write("Secret flags are not supported with --source yet. Deploy from source first, then set secrets with `primitive functions set-secret` and redeploy.\n");
17088
+ process.exitCode = 1;
17089
+ return;
17090
+ }
17091
+ const collected = collectSourceFiles(sourceDir);
17092
+ if (collected.kind === "error") {
17093
+ process.stderr.write(`${collected.message}\n`);
17094
+ process.exitCode = 1;
17095
+ return;
17096
+ }
17097
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
17098
+ apiKey: flags["api-key"],
17099
+ apiBaseUrl1: flags["api-base-url-1"],
17100
+ apiBaseUrl2: flags["api-base-url-2"],
17101
+ configDir: this.config.configDir
17102
+ });
17103
+ const authFailureContext = {
17104
+ auth,
17105
+ baseUrlOverridden,
17106
+ configDir: this.config.configDir
17107
+ };
17108
+ const outcome = await runSourceDeploy({
17109
+ createFunction: (p) => createFunction({
17110
+ body: {
17111
+ files: p.files,
17112
+ name: p.name
17113
+ },
17114
+ client: apiClient.client,
17115
+ responseStyle: "fields"
17116
+ }),
17117
+ listFunctions: () => listFunctions({
17118
+ client: apiClient.client,
17119
+ responseStyle: "fields"
17120
+ }),
17121
+ updateFunction: (p) => updateFunction({
17122
+ body: { files: p.files },
17123
+ client: apiClient.client,
17124
+ path: { id: p.id },
17125
+ responseStyle: "fields"
17126
+ })
17127
+ }, {
17128
+ files: collected.files,
17129
+ name: flags.name
17130
+ });
17131
+ if (outcome.kind === "error") {
17132
+ renderBuildFailure(outcome.payload, (chunk) => process.stderr.write(chunk));
17133
+ writeErrorWithHints(outcome.payload);
17134
+ surfaceUnauthorizedHint({
17135
+ ...authFailureContext,
17136
+ payload: outcome.payload
17137
+ });
17138
+ process.exitCode = 1;
17139
+ return;
17140
+ }
17141
+ const payload = outcome.result;
17142
+ if (flags.wait) {
17143
+ const waitResult = await waitForFunctionDeploy({
17144
+ getFunction: (p) => getFunction({
17145
+ client: apiClient.client,
17146
+ path: { id: p.id },
17147
+ responseStyle: "fields"
17148
+ }),
17149
+ id: payload.id,
17150
+ initial: payload,
17151
+ pollIntervalSeconds: flags["poll-interval"],
17152
+ timeoutSeconds: flags.timeout,
17153
+ writeStderr: (chunk) => process.stderr.write(chunk)
17154
+ });
17155
+ if (waitResult.kind === "error") {
17156
+ writeErrorWithHints(waitResult.payload);
17157
+ surfaceUnauthorizedHint({
17158
+ ...authFailureContext,
17159
+ payload: waitResult.payload
17160
+ });
17161
+ process.exitCode = 1;
17162
+ return;
17163
+ }
17164
+ if (waitResult.kind === "timeout") {
17165
+ const status = waitResult.lastFunction?.deploy_status ?? "unknown";
17166
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for function ${payload.id} deploy to finish (last status: ${status}).\n`);
17167
+ process.exitCode = 2;
17168
+ return;
17169
+ }
17170
+ this.log(JSON.stringify(waitResult.function, null, 2));
17171
+ if (waitResult.kind === "failed") {
17172
+ const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
17173
+ process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
17174
+ process.exitCode = 1;
17175
+ }
17176
+ return;
17177
+ }
17178
+ this.log(JSON.stringify(payload, null, 2));
17179
+ }
16762
17180
  };
16763
17181
  //#endregion
16764
17182
  //#region src/oclif/function-templates.ts
@@ -16768,8 +17186,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
16768
17186
  name: "Primitive Team",
16769
17187
  url: "https://primitive.dev"
16770
17188
  };
16771
- const SDK_VERSION_RANGE = "^0.32.0";
16772
- const CLI_VERSION_RANGE = "^0.32.0";
17189
+ const SDK_VERSION_RANGE = "^0.33.0";
17190
+ const CLI_VERSION_RANGE = "^0.33.0";
16773
17191
  const ESBUILD_VERSION_RANGE = "^0.27.0";
16774
17192
  function renderHandler() {
16775
17193
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -16795,49 +17213,93 @@ interface Env {
16795
17213
  PRIMITIVE_WEBHOOK_SECRET: string;
16796
17214
  }
16797
17215
 
16798
- // Loop-protection knob. Only used by the isLoop helper below; the
16799
- // handler's outbound reply address is server-defaulted (no
16800
- // from-address parameter is passed to client.reply). Update this if
16801
- // you later switch to sending from a non-managed-domain address so
16802
- // the loop guard recognizes mail returning to it as self-traffic.
16803
- const REPLY_FROM = "you@your-domain.primitive.email";
17216
+ // Optional loop-protection knob. client.reply() server-defaults the
17217
+ // outbound from-address from the inbound recipient, so most handlers
17218
+ // do not need to fill this in. Add any extra addresses your handler
17219
+ // sends from if you later switch to client.send or a custom domain.
17220
+ const EXTRA_SELF_ADDRESSES: string[] = [
17221
+ // "bot@your-domain.example",
17222
+ ];
17223
+
17224
+ function extractEmailAddresses(value: string | null | undefined): string[] {
17225
+ return (
17226
+ value?.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi)?.map((address) =>
17227
+ address.toLowerCase(),
17228
+ ) ?? []
17229
+ );
17230
+ }
17231
+
17232
+ function domainPart(address: string): string | null {
17233
+ const at = address.lastIndexOf("@");
17234
+ return at === -1 ? null : address.slice(at + 1).toLowerCase();
17235
+ }
17236
+
17237
+ function localPart(address: string): string {
17238
+ const at = address.lastIndexOf("@");
17239
+ return at === -1 ? address.toLowerCase() : address.slice(0, at).toLowerCase();
17240
+ }
17241
+
17242
+ function inboundRecipientAddresses(event: EmailReceivedEvent): string[] {
17243
+ return [
17244
+ ...event.email.smtp.rcpt_to.flatMap(extractEmailAddresses),
17245
+ ...extractEmailAddresses(event.email.headers.to),
17246
+ ];
17247
+ }
17248
+
17249
+ function inboundRecipientDomains(event: EmailReceivedEvent): Set<string> {
17250
+ return new Set(
17251
+ inboundRecipientAddresses(event)
17252
+ .map(domainPart)
17253
+ .filter((domain): domain is string => domain !== null),
17254
+ );
17255
+ }
16804
17256
 
16805
17257
  // Loop protection. A newly deployed Function starts as a fallback
16806
- // endpoint for managed *.primitive.email domains that do not have a
16807
- // domain-scoped endpoint. That can include bounces and auto-replies
16808
- // generated by the handler's own outbound traffic. Without this guard
16809
- // the handler can respond to its own bounces and create a fan-out loop.
17258
+ // endpoint for managed domains and can receive bounces or auto-replies
17259
+ // generated by its own outbound mail. Do not hardcode a managed suffix:
17260
+ // staging, production, and custom domains all arrive through the same
17261
+ // webhook shape. Derive the handler's inbound domains from the actual
17262
+ // SMTP recipients instead.
16810
17263
  //
16811
- // The default check returns true when From is on any *.primitive.email
16812
- // address (covers managed-domain fallback mail, the simple
16813
- // self-reply case, and bounces from mailer-daemon@*.primitive.email)
16814
- // or when From contains REPLY_FROM as a case-insensitive substring.
16815
- // Substring matching is deliberate so display-name forms like
16816
- // "Support <support@example.com>" match a bare-address REPLY_FROM,
16817
- // but it also accepts false positives where REPLY_FROM is a suffix
16818
- // of another address (e.g. REPLY_FROM="info@x.com" matches
16819
- // "mr.info@x.com"). For strict equality, parse the address out of the
16820
- // header and exact-match against REPLY_FROM.
17264
+ // The default check skips:
17265
+ // - direct self-mail where From equals one of the inbound recipients;
17266
+ // - mailer-daemon/postmaster bounces from the same domain as the inbound;
17267
+ // - any address explicitly listed in EXTRA_SELF_ADDRESSES.
16821
17268
  //
16822
17269
  // Extend this helper if you need stricter detection. Common additions:
16823
- // - Match the org's signup / account-owner email (not auto-injected
16824
- // into env today; either bake it into a SIGNUP_EMAIL const or read
16825
- // it from a secret you set via \`primitive functions:set-secret\`).
16826
17270
  // - Honor RFC 3834 auto-response headers: skip when
16827
- // \`event.email.headers["auto-submitted"]\` is anything other than
16828
- // "no", or when a \`List-Unsubscribe\` / \`Precedence: bulk\` header
16829
- // is present.
17271
+ // event.email.headers["auto-submitted"] is anything other than "no",
17272
+ // or when a List-Unsubscribe / Precedence: bulk header is present.
16830
17273
  // - Track Message-ID / In-Reply-To chains to break ping-pong loops
16831
17274
  // between two cooperating handlers on different domains.
16832
17275
  export function isLoop(event: EmailReceivedEvent): boolean {
16833
- // event.email.headers.from is the raw RFC 2822 header value, so it
16834
- // may be a bare address ("alice@example.com") or a display-name form
16835
- // ("Alice <alice@example.com>"). Lowercase substring checks match
16836
- // both shapes without needing to parse the bracketed address.
16837
- const from = event.email.headers.from?.toLowerCase() ?? "";
16838
- if (!from) return false;
16839
- if (from.includes(".primitive.email")) return true;
16840
- if (from.includes(REPLY_FROM.toLowerCase())) return true;
17276
+ const fromAddresses = [
17277
+ ...extractEmailAddresses(event.email.headers.from),
17278
+ ...extractEmailAddresses(event.email.smtp.mail_from),
17279
+ ];
17280
+ if (fromAddresses.length === 0) return false;
17281
+
17282
+ const inboundAddresses = new Set(inboundRecipientAddresses(event));
17283
+ const inboundDomains = inboundRecipientDomains(event);
17284
+ const extraSelfAddresses = new Set(
17285
+ EXTRA_SELF_ADDRESSES.map((address) => address.toLowerCase()),
17286
+ );
17287
+
17288
+ for (const from of fromAddresses) {
17289
+ if (inboundAddresses.has(from)) return true;
17290
+ if (extraSelfAddresses.has(from)) return true;
17291
+
17292
+ const fromDomain = domainPart(from);
17293
+ const fromLocal = localPart(from);
17294
+ const sameInboundDomain = fromDomain ? inboundDomains.has(fromDomain) : false;
17295
+ if (
17296
+ sameInboundDomain &&
17297
+ (fromLocal === "mailer-daemon" || fromLocal === "postmaster")
17298
+ ) {
17299
+ return true;
17300
+ }
17301
+ }
17302
+
16841
17303
  return false;
16842
17304
  }
16843
17305
 
@@ -16885,11 +17347,16 @@ export default {
16885
17347
  apiBaseUrl1: env.PRIMITIVE_API_BASE_URL,
16886
17348
  });
16887
17349
 
17350
+ // To add an LLM or another API, store its key as a Function secret.
17351
+ // Example:
17352
+ // export OPENAI_KEY=sk-...
17353
+ // primitive functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy
17354
+
16888
17355
  // Recipient gate
16889
17356
  // https://www.primitive.dev/docs/sending#who-you-can-send-to
16890
17357
  // Even via client.reply, sends to the original sender are
16891
17358
  // subject to the recipient gate. New accounts can send to
16892
- // *.primitive.email addresses, verified domains, addresses that
17359
+ // Primitive-managed domains, verified domains, addresses that
16893
17360
  // have authenticated to you, and other org-member signup emails.
16894
17361
  // Sends to arbitrary external addresses return 403
16895
17362
  // recipient_not_allowed with a structured gates[] array until
@@ -16939,8 +17406,10 @@ function renderPackageJson(name) {
16939
17406
  type: "module",
16940
17407
  scripts: {
16941
17408
  build: "node build.mjs",
16942
- deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js`,
16943
- redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
17409
+ deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js --wait`,
17410
+ "test:function": "primitive functions test --id $PRIMITIVE_FUNCTION_ID --wait --show-sends",
17411
+ logs: "primitive functions logs --id $PRIMITIVE_FUNCTION_ID",
17412
+ redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js --wait"
16944
17413
  },
16945
17414
  dependencies: { "@primitivedotdev/sdk": SDK_VERSION_RANGE },
16946
17415
  devDependencies: {
@@ -17011,23 +17480,57 @@ npm run build
17011
17480
  npm run deploy
17012
17481
  \`\`\`
17013
17482
 
17014
- The deploy step calls \`primitive functions deploy\` (provided by the
17015
- \`@primitivedotdev/cli\` package; install with
17483
+ The deploy step calls \`primitive functions deploy --wait\` (provided
17484
+ by the \`@primitivedotdev/cli\` package; install with
17016
17485
  \`npm install -g @primitivedotdev/cli\` or run via
17017
17486
  \`npx @primitivedotdev/cli@latest <command>\`). It requires
17018
17487
  \`PRIMITIVE_API_KEY\` to be set in your shell (or pass \`--api-key\`).
17019
17488
  Run \`primitive signin\` once to save a key in your CLI config if you
17020
17489
  prefer that to an env var.
17021
- `;
17022
- }
17023
- function renderEmailReplyTemplateFiles(name) {
17024
- return [
17025
- {
17026
- contents: renderHandler(),
17027
- relativePath: "handler.ts"
17028
- },
17029
- {
17030
- contents: renderPackageJson(name),
17490
+
17491
+ After the first deploy, copy the returned function id into your shell:
17492
+
17493
+ \`\`\`
17494
+ export PRIMITIVE_FUNCTION_ID=<fn-id>
17495
+ \`\`\`
17496
+
17497
+ ## Prove it works
17498
+
17499
+ \`\`\`
17500
+ primitive inbox status
17501
+ npm run test:function
17502
+ npm run logs
17503
+ \`\`\`
17504
+
17505
+ \`npm run test:function\` sends a real test email through MX, waits for
17506
+ the Function to process it, and prints any outbound replies emitted by
17507
+ the handler.
17508
+
17509
+ ## Redeploy
17510
+
17511
+ \`\`\`
17512
+ npm run redeploy
17513
+ \`\`\`
17514
+
17515
+ ## Secrets
17516
+
17517
+ Use secrets for API keys used by your handler. \`--redeploy\` makes the
17518
+ new value visible to the running Function immediately.
17519
+
17520
+ \`\`\`
17521
+ export OPENAI_KEY=sk-...
17522
+ primitive functions set-secret --id "$PRIMITIVE_FUNCTION_ID" --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy
17523
+ \`\`\`
17524
+ `;
17525
+ }
17526
+ function renderEmailReplyTemplateFiles(name) {
17527
+ return [
17528
+ {
17529
+ contents: renderHandler(),
17530
+ relativePath: "handler.ts"
17531
+ },
17532
+ {
17533
+ contents: renderPackageJson(name),
17031
17534
  relativePath: "package.json"
17032
17535
  },
17033
17536
  {
@@ -17192,8 +17695,9 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
17192
17695
  this.log("Next:");
17193
17696
  this.log(` cd ${outDir}`);
17194
17697
  this.log(" npm install");
17195
- this.log(" npm run build");
17196
- this.log(` primitive functions deploy --name ${args.name} --file ./dist/handler.js`);
17698
+ this.log(" npm run deploy");
17699
+ this.log(" export PRIMITIVE_FUNCTION_ID=<id-from-deploy-output>");
17700
+ this.log(" npm run test:function");
17197
17701
  }
17198
17702
  };
17199
17703
  //#endregion
@@ -17823,26 +18327,32 @@ var FunctionsTemplatesCommand = class FunctionsTemplatesCommand extends Command
17823
18327
  //#endregion
17824
18328
  //#region src/oclif/commands/functions-test-function.ts
17825
18329
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
17826
- const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
18330
+ const TERMINAL_TEST_TRACE_STATES = new Set([
18331
+ "completed",
18332
+ "failed",
18333
+ "send_failed"
18334
+ ]);
17827
18335
  function buildFunctionTestOutcome(params) {
18336
+ const inbound = params.trace.inbound_email;
17828
18337
  const outcome = {
17829
18338
  elapsed_seconds: params.elapsedSeconds,
17830
18339
  function_id: params.functionId,
17831
18340
  inbound_domain: params.invocation.inbound_domain,
17832
- inbound_id: params.inboundId,
18341
+ inbound_id: inbound?.id ?? null,
17833
18342
  inbound_to: params.invocation.to,
17834
18343
  poll_since: params.invocation.poll_since,
18344
+ state: params.trace.state,
17835
18345
  test_run_id: params.invocation.test_run_id,
17836
18346
  test_send_id: params.invocation.send_id,
17837
18347
  test_subject: params.invocation.subject,
17838
18348
  trace_url: params.invocation.trace_url,
17839
18349
  watch_url: params.invocation.watch_url,
17840
- webhook_attempt_count: params.detail.webhook_attempt_count,
17841
- webhook_last_error: params.detail.webhook_last_error,
17842
- webhook_last_status_code: params.detail.webhook_last_status_code,
17843
- webhook_status: params.detail.webhook_status
18350
+ webhook_attempt_count: inbound?.webhook_attempt_count ?? null,
18351
+ webhook_last_error: inbound?.webhook_last_error ?? null,
18352
+ webhook_last_status_code: inbound?.webhook_last_status_code ?? null,
18353
+ webhook_status: inbound?.webhook_status ?? null
17844
18354
  };
17845
- if (params.showSends) outcome.sent_emails = params.detail.replies;
18355
+ if (params.showSends) outcome.sent_emails = params.trace.replies;
17846
18356
  return outcome;
17847
18357
  }
17848
18358
  function writeFunctionTestProgress(message, writeStderr = (chunk) => {
@@ -17941,7 +18451,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
17941
18451
  required: true
17942
18452
  }),
17943
18453
  "local-part": Flags.string({ description: "Override the synthetic local-part the test inbound is addressed to. Otherwise the runtime picks `__primitive_function_test+<random>`." }),
17944
- wait: Flags.boolean({ description: "Block until the function has processed the test inbound (webhook status is `fired` or `exhausted`) or --timeout elapses. Exits non-zero on timeout or on exhausted retries." }),
18454
+ wait: Flags.boolean({ description: "Block until the function test run reaches `completed`, `failed`, or `send_failed`, or --timeout elapses. Exits non-zero on timeout or terminal failure." }),
17945
18455
  "show-sends": Flags.boolean({ description: "When the wait resolves, also print the outbound emails the function emitted while processing the test inbound (id, status, to, subject). Implies --wait." }),
17946
18456
  timeout: Flags.integer({
17947
18457
  default: DEFAULT_WAIT_TIMEOUT_SECONDS,
@@ -18001,41 +18511,15 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18001
18511
  const timeoutMs = flags.timeout * 1e3;
18002
18512
  const pollIntervalMs = flags["poll-interval"] * 1e3;
18003
18513
  const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
18004
- writeFunctionTestProgress(`Waiting for test inbound to arrive at ${invocation.to}...`);
18005
- let inboundId;
18006
- while (!isExpired()) {
18007
- const page = await fetchEmailSearchPage({
18008
- apiClient,
18009
- filters: { to: invocation.to },
18010
- pageSize: 25,
18011
- since: invocation.poll_since
18012
- });
18013
- if (!page.ok) {
18014
- const payload = extractErrorPayload(page.error);
18015
- writeErrorWithHints(payload);
18016
- surfaceUnauthorizedHint({
18017
- auth,
18018
- baseUrlOverridden,
18019
- configDir: this.config.configDir,
18020
- payload
18021
- });
18022
- process.exitCode = 1;
18023
- return;
18024
- }
18025
- const found = page.rows[0];
18026
- if (found) {
18027
- inboundId = found.id;
18028
- break;
18029
- }
18030
- await sleep$1(pollIntervalMs);
18031
- }
18032
- if (!inboundId) this.error(`Timed out after ${flags.timeout}s waiting for test inbound ${invocation.to} to land. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
18033
- writeFunctionTestProgress(`Inbound landed (${inboundId}). Waiting for function to run...`);
18034
- let detail;
18514
+ writeFunctionTestProgress(`Waiting for test run ${invocation.test_run_id} to complete for ${invocation.to}...`);
18515
+ let trace;
18035
18516
  while (!isExpired()) {
18036
- const result = await getEmail({
18517
+ const result = await getFunctionTestRunTrace({
18037
18518
  client: apiClient.client,
18038
- path: { id: inboundId },
18519
+ path: {
18520
+ id: flags.id,
18521
+ run_id: invocation.test_run_id
18522
+ },
18039
18523
  responseStyle: "fields"
18040
18524
  });
18041
18525
  if (result.error) {
@@ -18051,24 +18535,22 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18051
18535
  return;
18052
18536
  }
18053
18537
  const fetched = result.data.data;
18054
- if (fetched.webhook_status && TERMINAL_WEBHOOK_STATUSES.has(fetched.webhook_status)) {
18055
- detail = fetched;
18538
+ if (TERMINAL_TEST_TRACE_STATES.has(fetched.state)) {
18539
+ trace = fetched;
18056
18540
  break;
18057
18541
  }
18058
18542
  await sleep$1(pollIntervalMs);
18059
18543
  }
18060
- if (!detail) this.error(`Timed out after ${flags.timeout}s waiting for function webhook to fire for inbound ${inboundId}. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
18061
- const elapsedSeconds = Math.round((Date.now() - startedAt) / 1e3);
18544
+ if (!trace) this.error(`Timed out after ${flags.timeout}s waiting for function test run ${invocation.test_run_id} to complete. Browse ${invocation.watch_url} for the live view, or inspect ${invocation.trace_url}.`, { exit: 2 });
18062
18545
  const outcome = buildFunctionTestOutcome({
18063
- detail,
18064
- elapsedSeconds,
18546
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1e3),
18065
18547
  functionId: flags.id,
18066
- inboundId,
18067
18548
  invocation,
18068
- showSends: shouldShowSends
18549
+ showSends: shouldShowSends,
18550
+ trace
18069
18551
  });
18070
18552
  this.log(JSON.stringify(outcome, null, 2));
18071
- if (detail.webhook_status === "exhausted") process.exitCode = 1;
18553
+ if (trace.state === "failed" || trace.state === "send_failed") process.exitCode = 1;
18072
18554
  });
18073
18555
  }
18074
18556
  };
@@ -18319,7 +18801,7 @@ async function checkExistingLogin(params) {
18319
18801
  message: code === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI OAuth credentials were rejected by an API URL different from the one they were saved with. Run `primitive logout` to remove them, or switch back to the original environment before logging in again." : "A saved Primitive CLI OAuth session exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
18320
18802
  };
18321
18803
  }
18322
- var LoginCommand = class extends Command {
18804
+ var LoginCommand$1 = class extends Command {
18323
18805
  static description = "Log in by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
18324
18806
  static summary = "Log in with browser approval";
18325
18807
  static examples = [
@@ -18343,6 +18825,9 @@ var LoginCommand = class extends Command {
18343
18825
  async run() {
18344
18826
  const commandClass = this.constructor;
18345
18827
  const { flags } = await this.parse(commandClass);
18828
+ await this.runBrowserLogin(flags, this.retryCommand());
18829
+ }
18830
+ async runBrowserLogin(flags, retryCommand = this.retryCommand()) {
18346
18831
  let releaseCredentialsLock;
18347
18832
  try {
18348
18833
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -18350,7 +18835,7 @@ var LoginCommand = class extends Command {
18350
18835
  throw cliError$3(error instanceof Error ? error.message : String(error));
18351
18836
  }
18352
18837
  try {
18353
- await this.runWithCredentialLock(flags, this.retryCommand());
18838
+ await this.runWithCredentialLock(flags, retryCommand);
18354
18839
  } finally {
18355
18840
  releaseCredentialsLock();
18356
18841
  }
@@ -18458,567 +18943,132 @@ var LoginCommand = class extends Command {
18458
18943
  }
18459
18944
  };
18460
18945
  //#endregion
18461
- //#region src/oclif/commands/logout.ts
18946
+ //#region src/oclif/commands/signup.ts
18947
+ const INVALID_VERIFICATION_CODE = "invalid_verification_code";
18948
+ const EXPIRED_TOKEN = "expired_token";
18949
+ const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
18950
+ const SLOW_DOWN = "slow_down";
18951
+ const PENDING_SIGNUP_FILE = "signup.json";
18952
+ const DEFAULT_SIGNUP_COMMAND_COPY = {
18953
+ actionNoun: "signup",
18954
+ actionGerund: "creating a new account",
18955
+ confirmCommand: (email) => `signup confirm ${email} <code>`,
18956
+ resendCommand: (email) => `signup resend ${email}`,
18957
+ startCommand: (email) => `signup ${email}`
18958
+ };
18462
18959
  function cliError$2(message) {
18463
18960
  return new Errors.CLIError(message, { exit: 1 });
18464
18961
  }
18465
18962
  function unwrapData$1(value) {
18466
18963
  return value?.data ?? null;
18467
18964
  }
18468
- function isSavedOAuthSessionExpiredError(error) {
18469
- return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
18965
+ function isRecord(value) {
18966
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18470
18967
  }
18471
- async function runLogoutWithCredentialLock(params) {
18472
- const deps = {
18473
- cliLogout,
18474
- createAuthenticatedCliApiClient,
18475
- ...params.deps
18968
+ function normalizeEmail(email) {
18969
+ return email.trim().toLowerCase();
18970
+ }
18971
+ function pendingSignupFromJson(value) {
18972
+ if (!isRecord(value)) return null;
18973
+ if (typeof value.signup_token !== "string" || typeof value.email !== "string" || typeof value.expires_in !== "number" || typeof value.resend_after !== "number" || typeof value.verification_code_length !== "number" || typeof value.api_base_url_1 !== "string" || typeof value.created_at !== "string" || typeof value.expires_at !== "string") return null;
18974
+ return {
18975
+ api_base_url_1: value.api_base_url_1,
18976
+ created_at: value.created_at,
18977
+ email: value.email,
18978
+ expires_at: value.expires_at,
18979
+ expires_in: value.expires_in,
18980
+ resend_after: value.resend_after,
18981
+ signup_token: value.signup_token,
18982
+ verification_code_length: value.verification_code_length
18476
18983
  };
18477
- let credentials;
18984
+ }
18985
+ function pendingSignupPath(configDir) {
18986
+ return join(configDir, PENDING_SIGNUP_FILE);
18987
+ }
18988
+ function deletePendingAgentSignup(configDir) {
18989
+ rmSync(pendingSignupPath(configDir), { force: true });
18990
+ }
18991
+ function pendingSignupFromStart(start, apiBaseUrl1) {
18992
+ return {
18993
+ ...start,
18994
+ api_base_url_1: apiBaseUrl1,
18995
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
18996
+ expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
18997
+ };
18998
+ }
18999
+ function savePendingAgentSignup(configDir, start, apiBaseUrl1) {
19000
+ mkdirSync(configDir, {
19001
+ mode: 448,
19002
+ recursive: true
19003
+ });
19004
+ const pending = pendingSignupFromStart(start, apiBaseUrl1);
19005
+ const path = pendingSignupPath(configDir);
19006
+ const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
18478
19007
  try {
18479
- credentials = loadCliCredentials(params.configDir);
19008
+ writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
19009
+ chmodSync(tempPath, 384);
19010
+ renameSync(tempPath, path);
19011
+ chmodSync(path, 384);
19012
+ return pending;
18480
19013
  } catch (error) {
18481
- deleteCliCredentials(params.configDir);
18482
- const detail = error instanceof Error ? error.message : String(error);
18483
- process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
18484
- process.exitCode = 1;
18485
- return;
19014
+ rmSync(tempPath, { force: true });
19015
+ throw error;
18486
19016
  }
18487
- if (!credentials) throw cliError$2("Not logged in. Run `primitive signin` to create saved CLI credentials.");
18488
- let authenticated;
19017
+ }
19018
+ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
19019
+ const path = pendingSignupPath(configDir);
19020
+ let contents;
18489
19021
  try {
18490
- authenticated = await deps.createAuthenticatedCliApiClient({
18491
- apiBaseUrl1: params.flags["api-base-url-1"],
18492
- configDir: params.configDir,
18493
- credentialsLockHeld: true
18494
- });
19022
+ contents = readFileSync(path, "utf8");
18495
19023
  } catch (error) {
18496
- if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
18497
- process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
18498
- return;
18499
- }
19024
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
18500
19025
  throw error;
18501
19026
  }
18502
- const freshCredentials = authenticated.auth.credentials ?? credentials;
18503
- const result = await deps.cliLogout({
18504
- body: { key_id: freshCredentials.oauth_grant_id },
18505
- client: authenticated.apiClient.client,
18506
- responseStyle: "fields"
18507
- });
18508
- if (result.error) {
18509
- const payload = extractErrorPayload(result.error);
18510
- const code = extractErrorCode(payload);
18511
- if (code === API_ERROR_CODES.unauthorized || code === API_ERROR_CODES.notFound) {
18512
- deleteCliCredentials(params.configDir);
18513
- writeErrorWithHints(payload);
18514
- process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
18515
- process.exitCode = 1;
18516
- return;
18517
- }
18518
- writeErrorWithHints(payload);
18519
- throw cliError$2("Could not revoke the saved Primitive CLI OAuth grant.");
19027
+ let pending;
19028
+ try {
19029
+ pending = pendingSignupFromJson(JSON.parse(contents));
19030
+ } catch {
19031
+ pending = null;
18520
19032
  }
18521
- const logout = unwrapData$1(result.data);
18522
- deleteCliCredentials(params.configDir);
18523
- const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
18524
- process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
18525
- }
18526
- var LogoutCommand = class LogoutCommand extends Command {
18527
- static description = "Log out by revoking the saved Primitive CLI OAuth grant and deleting local credentials.";
18528
- static summary = "Log out and revoke the saved CLI OAuth grant";
18529
- static examples = ["<%= config.bin %> logout"];
18530
- static flags = { "api-base-url-1": Flags.string({
18531
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18532
- env: "PRIMITIVE_API_BASE_URL_1",
18533
- hidden: true
18534
- }) };
18535
- async run() {
18536
- const { flags } = await this.parse(LogoutCommand);
18537
- let releaseCredentialsLock;
18538
- try {
18539
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
18540
- } catch (error) {
18541
- throw cliError$2(error instanceof Error ? error.message : String(error));
18542
- }
18543
- try {
18544
- await runLogoutWithCredentialLock({
18545
- configDir: this.config.configDir,
18546
- flags
18547
- });
18548
- } finally {
18549
- releaseCredentialsLock();
18550
- }
19033
+ if (!pending) {
19034
+ deletePendingAgentSignup(configDir);
19035
+ return null;
18551
19036
  }
18552
- };
18553
- //#endregion
18554
- //#region src/oclif/message-body-sources.ts
18555
- function defaultReadFile(path) {
18556
- return readFileSync(path, "utf8");
19037
+ if (pending.api_base_url_1 !== apiBaseUrl1) return null;
19038
+ if (new Date(pending.expires_at).getTime() <= Date.now()) {
19039
+ deletePendingAgentSignup(configDir);
19040
+ return null;
19041
+ }
19042
+ return {
19043
+ ...pending,
19044
+ expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
19045
+ };
18557
19046
  }
18558
- function defaultReadStdin() {
18559
- if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
18560
- return readFileSync(0, "utf8");
19047
+ function requirePendingSignupForEmail(params) {
19048
+ const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
19049
+ const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
19050
+ if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
19051
+ if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19052
+ return pending;
18561
19053
  }
18562
- function selectedSources(sources) {
18563
- return sources.filter(([, selected]) => selected).map(([label]) => label);
19054
+ function retryAfterSeconds(result) {
19055
+ const raw = result.response?.headers.get("retry-after");
19056
+ if (!raw) return null;
19057
+ const parsed = Number.parseInt(raw, 10);
19058
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
18564
19059
  }
18565
- function readTextFile(path, label, readFile) {
18566
- try {
18567
- return {
18568
- content: readFile(path),
18569
- kind: "ok"
18570
- };
18571
- } catch (error) {
18572
- return {
18573
- kind: "error",
18574
- message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
18575
- };
18576
- }
19060
+ function normalizeAnswer(value) {
19061
+ return value.trim();
18577
19062
  }
18578
- function readTextStdin(label, readStdin) {
19063
+ async function promptLine(question) {
19064
+ const rl = createInterface({
19065
+ input: process$1.stdin,
19066
+ output: process$1.stderr
19067
+ });
18579
19068
  try {
18580
- return {
18581
- content: readStdin(),
18582
- kind: "ok"
18583
- };
18584
- } catch (error) {
18585
- return {
18586
- kind: "error",
18587
- message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
18588
- };
18589
- }
18590
- }
18591
- function resolveMessageBodies(input) {
18592
- const bodySources = selectedSources([
18593
- ["--body", input.body !== void 0],
18594
- ["--body-file", input.bodyFile !== void 0],
18595
- ["--body-stdin", input.bodyStdin === true]
18596
- ]);
18597
- if (bodySources.length > 1) return {
18598
- kind: "error",
18599
- message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
18600
- };
18601
- const htmlSources = selectedSources([
18602
- ["--html", input.html !== void 0],
18603
- ["--html-file", input.htmlFile !== void 0],
18604
- ["--html-stdin", input.htmlStdin === true]
18605
- ]);
18606
- if (htmlSources.length > 1) return {
18607
- kind: "error",
18608
- message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
18609
- };
18610
- const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
18611
- if (stdinSources.length > 1) return {
18612
- kind: "error",
18613
- message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
18614
- };
18615
- if (bodySources.length === 0 && htmlSources.length === 0) return {
18616
- kind: "error",
18617
- message: "Either a plain-text body source or an HTML body source is required."
18618
- };
18619
- const readFile = input.readFile ?? defaultReadFile;
18620
- const readStdin = input.readStdin ?? defaultReadStdin;
18621
- let body = input.body;
18622
- let html = input.html;
18623
- if (input.bodyFile !== void 0) {
18624
- const result = readTextFile(input.bodyFile, "--body-file", readFile);
18625
- if (result.kind === "error") return result;
18626
- body = result.content;
18627
- }
18628
- if (input.bodyStdin === true) {
18629
- const result = readTextStdin("--body-stdin", readStdin);
18630
- if (result.kind === "error") return result;
18631
- body = result.content;
18632
- }
18633
- if (input.htmlFile !== void 0) {
18634
- const result = readTextFile(input.htmlFile, "--html-file", readFile);
18635
- if (result.kind === "error") return result;
18636
- html = result.content;
18637
- }
18638
- if (input.htmlStdin === true) {
18639
- const result = readTextStdin("--html-stdin", readStdin);
18640
- if (result.kind === "error") return result;
18641
- html = result.content;
18642
- }
18643
- if (!body && !html) return {
18644
- kind: "error",
18645
- message: "Either a non-empty plain-text body or a non-empty HTML body is required."
18646
- };
18647
- return {
18648
- ...body !== void 0 ? { body } : {},
18649
- ...html !== void 0 ? { html } : {},
18650
- kind: "ok"
18651
- };
18652
- }
18653
- //#endregion
18654
- //#region src/oclif/commands/reply.ts
18655
- var ReplyCommand = class ReplyCommand extends Command {
18656
- static description = `Reply to an inbound email.
18657
-
18658
- The API derives recipients, the Re: subject, and threading headers from the inbound email id. Use \`primitive send --in-reply-to <message-id>\` only when you need to thread against a raw Message-Id instead of an inbound email stored by Primitive.`;
18659
- static summary = "Reply to an inbound email";
18660
- static examples = [
18661
- "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
18662
- "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
18663
- "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
18664
- "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
18665
- ];
18666
- static flags = {
18667
- "api-key": Flags.string({
18668
- description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18669
- env: "PRIMITIVE_API_KEY"
18670
- }),
18671
- "api-base-url-1": Flags.string({
18672
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18673
- env: "PRIMITIVE_API_BASE_URL_1",
18674
- hidden: true
18675
- }),
18676
- "api-base-url-2": Flags.string({
18677
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18678
- env: "PRIMITIVE_API_BASE_URL_2",
18679
- hidden: true
18680
- }),
18681
- id: Flags.string({
18682
- description: "Inbound email id to reply to.",
18683
- required: true
18684
- }),
18685
- body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
18686
- "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
18687
- "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
18688
- html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
18689
- "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
18690
- "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
18691
- from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
18692
- wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
18693
- "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
18694
- time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18695
- };
18696
- async run() {
18697
- const { flags } = await this.parse(ReplyCommand);
18698
- const bodies = resolveMessageBodies({
18699
- body: flags.body,
18700
- bodyFile: flags["body-file"],
18701
- bodyStdin: flags["body-stdin"],
18702
- html: flags.html,
18703
- htmlFile: flags["html-file"],
18704
- htmlStdin: flags["html-stdin"]
18705
- });
18706
- if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
18707
- await runWithTiming(flags.time, async () => {
18708
- const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18709
- apiKey: flags["api-key"],
18710
- apiBaseUrl1: flags["api-base-url-1"],
18711
- apiBaseUrl2: flags["api-base-url-2"],
18712
- configDir: this.config.configDir
18713
- });
18714
- const result = await replyToEmail({
18715
- body: {
18716
- ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
18717
- ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
18718
- ...flags.from !== void 0 ? { from: flags.from } : {},
18719
- ...flags.wait !== void 0 ? { wait: flags.wait } : {},
18720
- ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
18721
- },
18722
- client: apiClient.client,
18723
- path: { id: flags.id },
18724
- responseStyle: "fields"
18725
- });
18726
- if (result.error) {
18727
- const errorPayload = extractErrorPayload(result.error);
18728
- writeErrorWithHints(errorPayload);
18729
- surfaceUnauthorizedHint({
18730
- auth,
18731
- baseUrlOverridden,
18732
- configDir: this.config.configDir,
18733
- payload: errorPayload
18734
- });
18735
- process.exitCode = 1;
18736
- return;
18737
- }
18738
- const envelope = result.data;
18739
- writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
18740
- process.stderr.write(chunk);
18741
- } });
18742
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
18743
- });
18744
- }
18745
- };
18746
- //#endregion
18747
- //#region src/oclif/attachments.ts
18748
- function readAttachmentBytes(path, readFile) {
18749
- try {
18750
- return Buffer.from(readFile(path));
18751
- } catch (error) {
18752
- const detail = error instanceof Error ? error.message : String(error);
18753
- throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
18754
- }
18755
- }
18756
- function hasControlCharacter(value) {
18757
- return Array.from(value).some((character) => {
18758
- const code = character.charCodeAt(0);
18759
- return code <= 31 || code >= 127 && code <= 159;
18760
- });
18761
- }
18762
- function validateAttachmentFilename(path, filename) {
18763
- if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
18764
- if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
18765
- }
18766
- function readAttachmentFiles(paths, readFile = readFileSync) {
18767
- if (!paths || paths.length === 0) return void 0;
18768
- return paths.map((path) => {
18769
- const filename = basename(path);
18770
- validateAttachmentFilename(path, filename);
18771
- const bytes = readAttachmentBytes(path, readFile);
18772
- if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
18773
- return {
18774
- content_base64: bytes.toString("base64"),
18775
- filename
18776
- };
18777
- });
18778
- }
18779
- //#endregion
18780
- //#region src/oclif/commands/send.ts
18781
- var SendCommand = class SendCommand extends Command {
18782
- static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
18783
-
18784
- --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
18785
- --subject defaults to the first line of the body when omitted.
18786
- --attachment attaches a file; repeat it to attach multiple files.
18787
-
18788
- For the full flag set (custom message-id threading on the wire,
18789
- references arrays, etc.), use \`primitive sending send\`.`;
18790
- static summary = "Send an email (simplified, agent-friendly)";
18791
- static examples = [
18792
- "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
18793
- "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
18794
- "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
18795
- "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
18796
- "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
18797
- "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
18798
- "<%= config.bin %> send --to inbox@your-managed-domain.primitive.email --body 'self-loop smoke test' --wait # any *.primitive.email address routes back to the sending account; useful for proving outbound + inbound work end-to-end"
18799
- ];
18800
- static flags = {
18801
- "api-key": Flags.string({
18802
- description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18803
- env: "PRIMITIVE_API_KEY"
18804
- }),
18805
- "api-base-url-1": Flags.string({
18806
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18807
- env: "PRIMITIVE_API_BASE_URL_1",
18808
- hidden: true
18809
- }),
18810
- "api-base-url-2": Flags.string({
18811
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18812
- env: "PRIMITIVE_API_BASE_URL_2",
18813
- hidden: true
18814
- }),
18815
- to: Flags.string({
18816
- description: "Recipient address (e.g. alice@example.com).",
18817
- required: true
18818
- }),
18819
- from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
18820
- subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
18821
- body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
18822
- "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. This does not attach the file; use --attachment for file attachments. Mutually exclusive with --body and --body-stdin." }),
18823
- "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
18824
- html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
18825
- "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
18826
- "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
18827
- attachment: Flags.string({
18828
- description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
18829
- multiple: true
18830
- }),
18831
- "in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
18832
- wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
18833
- "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
18834
- time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18835
- };
18836
- async run() {
18837
- const { flags } = await this.parse(SendCommand);
18838
- const bodies = resolveMessageBodies({
18839
- body: flags.body,
18840
- bodyFile: flags["body-file"],
18841
- bodyStdin: flags["body-stdin"],
18842
- html: flags.html,
18843
- htmlFile: flags["html-file"],
18844
- htmlStdin: flags["html-stdin"]
18845
- });
18846
- if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
18847
- const attachments = readAttachmentFiles(flags.attachment);
18848
- await runWithTiming(flags.time, async () => {
18849
- const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18850
- apiKey: flags["api-key"],
18851
- apiBaseUrl1: flags["api-base-url-1"],
18852
- apiBaseUrl2: flags["api-base-url-2"],
18853
- configDir: this.config.configDir
18854
- });
18855
- const authFailureContext = {
18856
- auth,
18857
- baseUrlOverridden,
18858
- configDir: this.config.configDir
18859
- };
18860
- const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
18861
- const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
18862
- const result = await sendEmail({
18863
- body: {
18864
- from,
18865
- to: flags.to,
18866
- subject,
18867
- ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
18868
- ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
18869
- ...attachments !== void 0 ? { attachments } : {},
18870
- ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
18871
- ...flags.wait !== void 0 ? { wait: flags.wait } : {},
18872
- ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
18873
- },
18874
- client: apiClient._sendClient,
18875
- responseStyle: "fields"
18876
- });
18877
- if (result.error) {
18878
- const errorPayload = extractErrorPayload(result.error);
18879
- writeErrorWithHints(errorPayload);
18880
- surfaceUnauthorizedHint({
18881
- ...authFailureContext,
18882
- payload: errorPayload
18883
- });
18884
- process.exitCode = 1;
18885
- return;
18886
- }
18887
- const envelope = result.data;
18888
- writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
18889
- process.stderr.write(chunk);
18890
- } });
18891
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
18892
- });
18893
- }
18894
- };
18895
- //#endregion
18896
- //#region src/oclif/commands/signup.ts
18897
- const INVALID_VERIFICATION_CODE = "invalid_verification_code";
18898
- const EXPIRED_TOKEN = "expired_token";
18899
- const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
18900
- const SLOW_DOWN = "slow_down";
18901
- const PENDING_SIGNUP_FILE = "signup.json";
18902
- const DEFAULT_SIGNUP_COMMAND_COPY = {
18903
- actionNoun: "signup",
18904
- actionGerund: "creating a new account",
18905
- confirmCommand: (email) => `signup confirm ${email} <code>`,
18906
- resendCommand: (email) => `signup resend ${email}`,
18907
- startCommand: (email) => `signup ${email}`
18908
- };
18909
- function cliError$1(message) {
18910
- return new Errors.CLIError(message, { exit: 1 });
18911
- }
18912
- function unwrapData(value) {
18913
- return value?.data ?? null;
18914
- }
18915
- function isRecord(value) {
18916
- return value !== null && typeof value === "object" && !Array.isArray(value);
18917
- }
18918
- function normalizeEmail(email) {
18919
- return email.trim().toLowerCase();
18920
- }
18921
- function pendingSignupFromJson(value) {
18922
- if (!isRecord(value)) return null;
18923
- if (typeof value.signup_token !== "string" || typeof value.email !== "string" || typeof value.expires_in !== "number" || typeof value.resend_after !== "number" || typeof value.verification_code_length !== "number" || typeof value.api_base_url_1 !== "string" || typeof value.created_at !== "string" || typeof value.expires_at !== "string") return null;
18924
- return {
18925
- api_base_url_1: value.api_base_url_1,
18926
- created_at: value.created_at,
18927
- email: value.email,
18928
- expires_at: value.expires_at,
18929
- expires_in: value.expires_in,
18930
- resend_after: value.resend_after,
18931
- signup_token: value.signup_token,
18932
- verification_code_length: value.verification_code_length
18933
- };
18934
- }
18935
- function pendingSignupPath(configDir) {
18936
- return join(configDir, PENDING_SIGNUP_FILE);
18937
- }
18938
- function deletePendingAgentSignup(configDir) {
18939
- rmSync(pendingSignupPath(configDir), { force: true });
18940
- }
18941
- function pendingSignupFromStart(start, apiBaseUrl1) {
18942
- return {
18943
- ...start,
18944
- api_base_url_1: apiBaseUrl1,
18945
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
18946
- expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
18947
- };
18948
- }
18949
- function savePendingAgentSignup(configDir, start, apiBaseUrl1) {
18950
- mkdirSync(configDir, {
18951
- mode: 448,
18952
- recursive: true
18953
- });
18954
- const pending = pendingSignupFromStart(start, apiBaseUrl1);
18955
- const path = pendingSignupPath(configDir);
18956
- const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
18957
- try {
18958
- writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
18959
- chmodSync(tempPath, 384);
18960
- renameSync(tempPath, path);
18961
- chmodSync(path, 384);
18962
- return pending;
18963
- } catch (error) {
18964
- rmSync(tempPath, { force: true });
18965
- throw error;
18966
- }
18967
- }
18968
- function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18969
- const path = pendingSignupPath(configDir);
18970
- let contents;
18971
- try {
18972
- contents = readFileSync(path, "utf8");
18973
- } catch (error) {
18974
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
18975
- throw error;
18976
- }
18977
- let pending;
18978
- try {
18979
- pending = pendingSignupFromJson(JSON.parse(contents));
18980
- } catch {
18981
- pending = null;
18982
- }
18983
- if (!pending) {
18984
- deletePendingAgentSignup(configDir);
18985
- return null;
18986
- }
18987
- if (pending.api_base_url_1 !== apiBaseUrl1) return null;
18988
- if (new Date(pending.expires_at).getTime() <= Date.now()) {
18989
- deletePendingAgentSignup(configDir);
18990
- return null;
18991
- }
18992
- return {
18993
- ...pending,
18994
- expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
18995
- };
18996
- }
18997
- function requirePendingSignupForEmail(params) {
18998
- const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
18999
- const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
19000
- if (!pending) throw cliError$1(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
19001
- if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$1(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19002
- return pending;
19003
- }
19004
- function retryAfterSeconds(result) {
19005
- const raw = result.response?.headers.get("retry-after");
19006
- if (!raw) return null;
19007
- const parsed = Number.parseInt(raw, 10);
19008
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
19009
- }
19010
- function normalizeAnswer(value) {
19011
- return value.trim();
19012
- }
19013
- async function promptLine(question) {
19014
- const rl = createInterface({
19015
- input: process$1.stdin,
19016
- output: process$1.stderr
19017
- });
19018
- try {
19019
- return normalizeAnswer(await rl.question(question));
19020
- } finally {
19021
- rl.close();
19069
+ return normalizeAnswer(await rl.question(question));
19070
+ } finally {
19071
+ rl.close();
19022
19072
  }
19023
19073
  }
19024
19074
  function formatSignupSeconds(seconds) {
@@ -19039,7 +19089,7 @@ async function confirmTerms() {
19039
19089
  process$1.stderr.write(" https://primitive.dev/terms\n");
19040
19090
  process$1.stderr.write(" https://primitive.dev/privacy\n");
19041
19091
  const answer = (await promptRequired("Type 'yes' to continue: ")).toLowerCase();
19042
- if (answer !== "yes" && answer !== "y") throw cliError$1("You must accept the terms to create an account.");
19092
+ if (answer !== "yes" && answer !== "y") throw cliError$2("You must accept the terms to create an account.");
19043
19093
  }
19044
19094
  async function checkExistingCredentials(params) {
19045
19095
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
@@ -19070,9 +19120,9 @@ async function checkExistingCredentials(params) {
19070
19120
  }
19071
19121
  if (existingStatus.status === "blocked") {
19072
19122
  writeErrorWithHints(existingStatus.payload);
19073
- throw cliError$1(existingStatus.message);
19123
+ throw cliError$2(existingStatus.message);
19074
19124
  }
19075
- throw cliError$1(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
19125
+ throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
19076
19126
  }
19077
19127
  function saveSignupCredentials(params) {
19078
19128
  saveCliCredentials(params.configDir, {
@@ -19106,7 +19156,7 @@ async function startSignup(params) {
19106
19156
  started: false
19107
19157
  };
19108
19158
  }
19109
- throw cliError$1(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19159
+ throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19110
19160
  }
19111
19161
  if (params.flags.force) deletePendingAgentSignup(params.configDir);
19112
19162
  const promptRequiredFn = params.deps.promptRequired ?? promptRequired;
@@ -19126,10 +19176,10 @@ async function startSignup(params) {
19126
19176
  });
19127
19177
  if (started.error) {
19128
19178
  writeErrorWithHints(extractErrorPayload(started.error));
19129
- throw cliError$1("Could not start Primitive agent signup.");
19179
+ throw cliError$2("Could not start Primitive agent signup.");
19130
19180
  }
19131
- const startResult = unwrapData(started.data);
19132
- if (!startResult) throw cliError$1("Primitive API returned an empty agent signup response.");
19181
+ const startResult = unwrapData$1(started.data);
19182
+ if (!startResult) throw cliError$2("Primitive API returned an empty agent signup response.");
19133
19183
  return {
19134
19184
  pending: savePendingAgentSignup(params.configDir, startResult, params.apiBaseUrl1),
19135
19185
  started: true
@@ -19142,7 +19192,7 @@ async function resendVerificationCode(params) {
19142
19192
  responseStyle: "fields"
19143
19193
  });
19144
19194
  if (resent.data) {
19145
- const resend = unwrapData(resent.data);
19195
+ const resend = unwrapData$1(resent.data);
19146
19196
  const next = resend ? {
19147
19197
  email: resend.email,
19148
19198
  expires_in: resend.expires_in,
@@ -19167,7 +19217,7 @@ async function resendVerificationCode(params) {
19167
19217
  }
19168
19218
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(params.configDir);
19169
19219
  writeErrorWithHints(payload);
19170
- throw cliError$1("Could not resend Primitive agent signup verification email.");
19220
+ throw cliError$2("Could not resend Primitive agent signup verification email.");
19171
19221
  }
19172
19222
  async function runSignupStartWithCredentialLock(params) {
19173
19223
  const { configDir, flags } = params;
@@ -19227,8 +19277,8 @@ async function runSignupConfirmWithCredentialLock(params) {
19227
19277
  responseStyle: "fields"
19228
19278
  });
19229
19279
  if (verified.data) {
19230
- const signup = unwrapData(verified.data);
19231
- if (!signup) throw cliError$1("Primitive API returned an empty agent signup verification response.");
19280
+ const signup = unwrapData$1(verified.data);
19281
+ if (!signup) throw cliError$2("Primitive API returned an empty agent signup verification response.");
19232
19282
  saveSignupCredentials({
19233
19283
  apiBaseUrl1,
19234
19284
  configDir,
@@ -19242,10 +19292,10 @@ async function runSignupConfirmWithCredentialLock(params) {
19242
19292
  }
19243
19293
  const payload = extractErrorPayload(verified.error);
19244
19294
  const code = extractErrorCode(payload);
19245
- if (code === INVALID_VERIFICATION_CODE) throw cliError$1(`Invalid verification code. Try again or run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}.`);
19295
+ if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again or run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}.`);
19246
19296
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(configDir);
19247
19297
  writeErrorWithHints(payload);
19248
- throw cliError$1("Primitive agent signup failed while verifying the account.");
19298
+ throw cliError$2("Primitive agent signup failed while verifying the account.");
19249
19299
  }
19250
19300
  async function runSignupResendWithCredentialLock(params) {
19251
19301
  const deps = params.deps ?? {};
@@ -19332,40 +19382,268 @@ async function runSignupInteractiveWithCredentialLock(params) {
19332
19382
  }
19333
19383
  }
19334
19384
  }
19335
- function commonStartFlags() {
19336
- return {
19337
- "accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
19385
+ function commonStartFlags() {
19386
+ return {
19387
+ "accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
19388
+ "api-base-url-1": Flags.string({
19389
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19390
+ env: "PRIMITIVE_API_BASE_URL_1",
19391
+ hidden: true
19392
+ }),
19393
+ "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19394
+ force: Flags.boolean({
19395
+ char: "f",
19396
+ description: "Replace saved credentials or pending signup state when needed"
19397
+ }),
19398
+ "signup-code": Flags.string({
19399
+ description: "Signup code required to create an account",
19400
+ env: "PRIMITIVE_SIGNUP_CODE"
19401
+ })
19402
+ };
19403
+ }
19404
+ var SignupCommand = class SignupCommand extends Command {
19405
+ static args = { email: Args.string({
19406
+ description: "Email address to sign up",
19407
+ required: false
19408
+ }) };
19409
+ static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
19410
+ static summary = "Start account signup";
19411
+ static examples = [
19412
+ "<%= config.bin %> signup user@example.com",
19413
+ "<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
19414
+ "<%= config.bin %> signup confirm user@example.com 123456"
19415
+ ];
19416
+ static flags = commonStartFlags();
19417
+ async run() {
19418
+ const { args, flags } = await this.parse(SignupCommand);
19419
+ let releaseCredentialsLock;
19420
+ try {
19421
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19422
+ } catch (error) {
19423
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19424
+ }
19425
+ try {
19426
+ await runSignupStartWithCredentialLock({
19427
+ configDir: this.config.configDir,
19428
+ email: args.email,
19429
+ flags
19430
+ });
19431
+ } finally {
19432
+ releaseCredentialsLock();
19433
+ }
19434
+ }
19435
+ };
19436
+ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
19437
+ static args = {
19438
+ email: Args.string({
19439
+ description: "Email address used to start signup",
19440
+ required: true
19441
+ }),
19442
+ code: Args.string({
19443
+ description: "Verification code from the signup email",
19444
+ required: true
19445
+ })
19446
+ };
19447
+ static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
19448
+ static summary = "Confirm account signup";
19449
+ static examples = ["<%= config.bin %> signup confirm user@example.com 123456", "<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
19450
+ static flags = {
19451
+ "api-base-url-1": Flags.string({
19452
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19453
+ env: "PRIMITIVE_API_BASE_URL_1",
19454
+ hidden: true
19455
+ }),
19456
+ force: Flags.boolean({
19457
+ char: "f",
19458
+ description: "Replace saved credentials after verification"
19459
+ }),
19460
+ "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19461
+ };
19462
+ async run() {
19463
+ const { args, flags } = await this.parse(SignupConfirmCommand);
19464
+ let releaseCredentialsLock;
19465
+ try {
19466
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19467
+ } catch (error) {
19468
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19469
+ }
19470
+ try {
19471
+ await runSignupConfirmWithCredentialLock({
19472
+ code: args.code,
19473
+ configDir: this.config.configDir,
19474
+ email: args.email,
19475
+ flags
19476
+ });
19477
+ } finally {
19478
+ releaseCredentialsLock();
19479
+ }
19480
+ }
19481
+ };
19482
+ var SignupResendCommand = class SignupResendCommand extends Command {
19483
+ static args = { email: Args.string({
19484
+ description: "Email address used to start signup",
19485
+ required: true
19486
+ }) };
19487
+ static description = "Resend the verification code for a pending signup.";
19488
+ static summary = "Resend signup verification code";
19489
+ static examples = ["<%= config.bin %> signup resend user@example.com"];
19490
+ static flags = { "api-base-url-1": Flags.string({
19491
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19492
+ env: "PRIMITIVE_API_BASE_URL_1",
19493
+ hidden: true
19494
+ }) };
19495
+ async run() {
19496
+ const { args, flags } = await this.parse(SignupResendCommand);
19497
+ let releaseCredentialsLock;
19498
+ try {
19499
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19500
+ } catch (error) {
19501
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19502
+ }
19503
+ try {
19504
+ await runSignupResendWithCredentialLock({
19505
+ configDir: this.config.configDir,
19506
+ email: args.email,
19507
+ flags
19508
+ });
19509
+ } finally {
19510
+ releaseCredentialsLock();
19511
+ }
19512
+ }
19513
+ };
19514
+ var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
19515
+ static description = "Run the full signup flow in one interactive terminal session.";
19516
+ static summary = "Run interactive account signup";
19517
+ static examples = ["<%= config.bin %> signup interactive"];
19518
+ static flags = commonStartFlags();
19519
+ async run() {
19520
+ const { flags } = await this.parse(SignupInteractiveCommand);
19521
+ let releaseCredentialsLock;
19522
+ try {
19523
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19524
+ } catch (error) {
19525
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19526
+ }
19527
+ try {
19528
+ await runSignupInteractiveWithCredentialLock({
19529
+ configDir: this.config.configDir,
19530
+ flags
19531
+ });
19532
+ } finally {
19533
+ releaseCredentialsLock();
19534
+ }
19535
+ }
19536
+ };
19537
+ //#endregion
19538
+ //#region src/oclif/commands/logout.ts
19539
+ function cliError$1(message) {
19540
+ return new Errors.CLIError(message, { exit: 1 });
19541
+ }
19542
+ function unwrapData(value) {
19543
+ return value?.data ?? null;
19544
+ }
19545
+ function isSavedOAuthSessionExpiredError(error) {
19546
+ return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
19547
+ }
19548
+ async function runLogoutWithCredentialLock(params) {
19549
+ const deps = {
19550
+ cliLogout,
19551
+ createAuthenticatedCliApiClient,
19552
+ ...params.deps
19553
+ };
19554
+ let credentials;
19555
+ try {
19556
+ credentials = loadCliCredentials(params.configDir);
19557
+ } catch (error) {
19558
+ deleteCliCredentials(params.configDir);
19559
+ const detail = error instanceof Error ? error.message : String(error);
19560
+ process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
19561
+ process.exitCode = 1;
19562
+ return;
19563
+ }
19564
+ if (!credentials) throw cliError$1("Not logged in. Run `primitive signin` to create saved CLI credentials.");
19565
+ let authenticated;
19566
+ try {
19567
+ authenticated = await deps.createAuthenticatedCliApiClient({
19568
+ apiBaseUrl1: params.flags["api-base-url-1"],
19569
+ configDir: params.configDir,
19570
+ credentialsLockHeld: true
19571
+ });
19572
+ } catch (error) {
19573
+ if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
19574
+ process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
19575
+ return;
19576
+ }
19577
+ throw error;
19578
+ }
19579
+ const freshCredentials = authenticated.auth.credentials ?? credentials;
19580
+ const result = await deps.cliLogout({
19581
+ body: { key_id: freshCredentials.oauth_grant_id },
19582
+ client: authenticated.apiClient.client,
19583
+ responseStyle: "fields"
19584
+ });
19585
+ if (result.error) {
19586
+ const payload = extractErrorPayload(result.error);
19587
+ const code = extractErrorCode(payload);
19588
+ if (code === API_ERROR_CODES.unauthorized || code === API_ERROR_CODES.notFound) {
19589
+ deleteCliCredentials(params.configDir);
19590
+ writeErrorWithHints(payload);
19591
+ process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
19592
+ process.exitCode = 1;
19593
+ return;
19594
+ }
19595
+ writeErrorWithHints(payload);
19596
+ throw cliError$1("Could not revoke the saved Primitive CLI OAuth grant.");
19597
+ }
19598
+ const logout = unwrapData(result.data);
19599
+ deleteCliCredentials(params.configDir);
19600
+ const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
19601
+ process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
19602
+ }
19603
+ function runForceLogout(params) {
19604
+ const localCredentialsPath = credentialsPath(params.configDir);
19605
+ const pendingPath = pendingSignupPath(params.configDir);
19606
+ const lockPath = credentialsLockPath(params.configDir);
19607
+ const removed = [
19608
+ existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
19609
+ existsSync(pendingPath) ? "pending email-code auth state" : null,
19610
+ existsSync(lockPath) ? "credential lock" : null
19611
+ ].filter((value) => value !== null);
19612
+ deleteCliCredentials(params.configDir);
19613
+ deletePendingAgentSignup(params.configDir);
19614
+ deleteCliCredentialsLock(params.configDir);
19615
+ if (removed.length === 0) {
19616
+ process.stderr.write("No local Primitive CLI auth state was present. Backing OAuth grant was not revoked.\n");
19617
+ return;
19618
+ }
19619
+ process.stderr.write(`Removed ${formatList(removed)}. Backing OAuth grant was not revoked.\n`);
19620
+ }
19621
+ function formatList(values) {
19622
+ if (values.length <= 1) return values[0] ?? "";
19623
+ if (values.length === 2) return `${values[0]} and ${values[1]}`;
19624
+ return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
19625
+ }
19626
+ var LogoutCommand = class LogoutCommand extends Command {
19627
+ static description = "Log out by revoking the saved Primitive CLI OAuth grant and deleting local credentials. Use --force to remove local credentials, pending email-code auth state, and stale credential locks without contacting Primitive.";
19628
+ static summary = "Log out and revoke the saved CLI OAuth grant";
19629
+ static examples = ["<%= config.bin %> logout", "<%= config.bin %> logout --force"];
19630
+ static flags = {
19338
19631
  "api-base-url-1": Flags.string({
19339
19632
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19340
19633
  env: "PRIMITIVE_API_BASE_URL_1",
19341
19634
  hidden: true
19342
19635
  }),
19343
- "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19344
19636
  force: Flags.boolean({
19345
19637
  char: "f",
19346
- description: "Replace saved credentials or pending signup state when needed"
19347
- }),
19348
- "signup-code": Flags.string({
19349
- description: "Signup code required to create an account",
19350
- env: "PRIMITIVE_SIGNUP_CODE"
19638
+ description: "Remove local CLI credentials, pending email-code auth state, and any credential lock without revoking the server OAuth grant"
19351
19639
  })
19352
19640
  };
19353
- }
19354
- var SignupCommand = class SignupCommand extends Command {
19355
- static args = { email: Args.string({
19356
- description: "Email address to sign up",
19357
- required: false
19358
- }) };
19359
- static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
19360
- static summary = "Start account signup";
19361
- static examples = [
19362
- "<%= config.bin %> signup user@example.com",
19363
- "<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
19364
- "<%= config.bin %> signup confirm user@example.com 123456"
19365
- ];
19366
- static flags = commonStartFlags();
19367
19641
  async run() {
19368
- const { args, flags } = await this.parse(SignupCommand);
19642
+ const { flags } = await this.parse(LogoutCommand);
19643
+ if (flags.force) {
19644
+ runForceLogout({ configDir: this.config.configDir });
19645
+ return;
19646
+ }
19369
19647
  let releaseCredentialsLock;
19370
19648
  try {
19371
19649
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -19373,9 +19651,8 @@ var SignupCommand = class SignupCommand extends Command {
19373
19651
  throw cliError$1(error instanceof Error ? error.message : String(error));
19374
19652
  }
19375
19653
  try {
19376
- await runSignupStartWithCredentialLock({
19654
+ await runLogoutWithCredentialLock({
19377
19655
  configDir: this.config.configDir,
19378
- email: args.email,
19379
19656
  flags
19380
19657
  });
19381
19658
  } finally {
@@ -19383,105 +19660,344 @@ var SignupCommand = class SignupCommand extends Command {
19383
19660
  }
19384
19661
  }
19385
19662
  };
19386
- var SignupConfirmCommand = class SignupConfirmCommand extends Command {
19387
- static args = {
19388
- email: Args.string({
19389
- description: "Email address used to start signup",
19390
- required: true
19663
+ //#endregion
19664
+ //#region src/oclif/message-body-sources.ts
19665
+ function defaultReadFile(path) {
19666
+ return readFileSync(path, "utf8");
19667
+ }
19668
+ function defaultReadStdin() {
19669
+ if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
19670
+ return readFileSync(0, "utf8");
19671
+ }
19672
+ function selectedSources(sources) {
19673
+ return sources.filter(([, selected]) => selected).map(([label]) => label);
19674
+ }
19675
+ function readTextFile(path, label, readFile) {
19676
+ try {
19677
+ return {
19678
+ content: readFile(path),
19679
+ kind: "ok"
19680
+ };
19681
+ } catch (error) {
19682
+ return {
19683
+ kind: "error",
19684
+ message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
19685
+ };
19686
+ }
19687
+ }
19688
+ function readTextStdin(label, readStdin) {
19689
+ try {
19690
+ return {
19691
+ content: readStdin(),
19692
+ kind: "ok"
19693
+ };
19694
+ } catch (error) {
19695
+ return {
19696
+ kind: "error",
19697
+ message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
19698
+ };
19699
+ }
19700
+ }
19701
+ function resolveMessageBodies(input) {
19702
+ const bodySources = selectedSources([
19703
+ ["--body", input.body !== void 0],
19704
+ ["--body-file", input.bodyFile !== void 0],
19705
+ ["--body-stdin", input.bodyStdin === true]
19706
+ ]);
19707
+ if (bodySources.length > 1) return {
19708
+ kind: "error",
19709
+ message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
19710
+ };
19711
+ const htmlSources = selectedSources([
19712
+ ["--html", input.html !== void 0],
19713
+ ["--html-file", input.htmlFile !== void 0],
19714
+ ["--html-stdin", input.htmlStdin === true]
19715
+ ]);
19716
+ if (htmlSources.length > 1) return {
19717
+ kind: "error",
19718
+ message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
19719
+ };
19720
+ const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
19721
+ if (stdinSources.length > 1) return {
19722
+ kind: "error",
19723
+ message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
19724
+ };
19725
+ if (bodySources.length === 0 && htmlSources.length === 0) return {
19726
+ kind: "error",
19727
+ message: "Either a plain-text body source or an HTML body source is required."
19728
+ };
19729
+ const readFile = input.readFile ?? defaultReadFile;
19730
+ const readStdin = input.readStdin ?? defaultReadStdin;
19731
+ let body = input.body;
19732
+ let html = input.html;
19733
+ if (input.bodyFile !== void 0) {
19734
+ const result = readTextFile(input.bodyFile, "--body-file", readFile);
19735
+ if (result.kind === "error") return result;
19736
+ body = result.content;
19737
+ }
19738
+ if (input.bodyStdin === true) {
19739
+ const result = readTextStdin("--body-stdin", readStdin);
19740
+ if (result.kind === "error") return result;
19741
+ body = result.content;
19742
+ }
19743
+ if (input.htmlFile !== void 0) {
19744
+ const result = readTextFile(input.htmlFile, "--html-file", readFile);
19745
+ if (result.kind === "error") return result;
19746
+ html = result.content;
19747
+ }
19748
+ if (input.htmlStdin === true) {
19749
+ const result = readTextStdin("--html-stdin", readStdin);
19750
+ if (result.kind === "error") return result;
19751
+ html = result.content;
19752
+ }
19753
+ if (!body && !html) return {
19754
+ kind: "error",
19755
+ message: "Either a non-empty plain-text body or a non-empty HTML body is required."
19756
+ };
19757
+ return {
19758
+ ...body !== void 0 ? { body } : {},
19759
+ ...html !== void 0 ? { html } : {},
19760
+ kind: "ok"
19761
+ };
19762
+ }
19763
+ //#endregion
19764
+ //#region src/oclif/commands/reply.ts
19765
+ var ReplyCommand = class ReplyCommand extends Command {
19766
+ static description = `Reply to an inbound email.
19767
+
19768
+ The API derives recipients, the Re: subject, and threading headers from the inbound email id. Use \`primitive send --in-reply-to <message-id>\` only when you need to thread against a raw Message-Id instead of an inbound email stored by Primitive.`;
19769
+ static summary = "Reply to an inbound email";
19770
+ static examples = [
19771
+ "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
19772
+ "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
19773
+ "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
19774
+ "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
19775
+ ];
19776
+ static flags = {
19777
+ "api-key": Flags.string({
19778
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
19779
+ env: "PRIMITIVE_API_KEY"
19391
19780
  }),
19392
- code: Args.string({
19393
- description: "Verification code from the signup email",
19781
+ "api-base-url-1": Flags.string({
19782
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19783
+ env: "PRIMITIVE_API_BASE_URL_1",
19784
+ hidden: true
19785
+ }),
19786
+ "api-base-url-2": Flags.string({
19787
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
19788
+ env: "PRIMITIVE_API_BASE_URL_2",
19789
+ hidden: true
19790
+ }),
19791
+ id: Flags.string({
19792
+ description: "Inbound email id to reply to.",
19394
19793
  required: true
19395
- })
19794
+ }),
19795
+ body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
19796
+ "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
19797
+ "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
19798
+ html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
19799
+ "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
19800
+ "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
19801
+ from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
19802
+ wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
19803
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
19396
19804
  };
19397
- static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
19398
- static summary = "Confirm account signup";
19399
- static examples = ["<%= config.bin %> signup confirm user@example.com 123456", "<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
19805
+ async run() {
19806
+ const { flags } = await this.parse(ReplyCommand);
19807
+ const bodies = resolveMessageBodies({
19808
+ body: flags.body,
19809
+ bodyFile: flags["body-file"],
19810
+ bodyStdin: flags["body-stdin"],
19811
+ html: flags.html,
19812
+ htmlFile: flags["html-file"],
19813
+ htmlStdin: flags["html-stdin"]
19814
+ });
19815
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
19816
+ await runWithTiming(flags.time, async () => {
19817
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
19818
+ apiKey: flags["api-key"],
19819
+ apiBaseUrl1: flags["api-base-url-1"],
19820
+ apiBaseUrl2: flags["api-base-url-2"],
19821
+ configDir: this.config.configDir
19822
+ });
19823
+ const result = await replyToEmail({
19824
+ body: {
19825
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
19826
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
19827
+ ...flags.from !== void 0 ? { from: flags.from } : {},
19828
+ ...flags.wait !== void 0 ? { wait: flags.wait } : {}
19829
+ },
19830
+ client: apiClient.client,
19831
+ path: { id: flags.id },
19832
+ responseStyle: "fields"
19833
+ });
19834
+ if (result.error) {
19835
+ const errorPayload = extractErrorPayload(result.error);
19836
+ writeErrorWithHints(errorPayload);
19837
+ surfaceUnauthorizedHint({
19838
+ auth,
19839
+ baseUrlOverridden,
19840
+ configDir: this.config.configDir,
19841
+ payload: errorPayload
19842
+ });
19843
+ process.exitCode = 1;
19844
+ return;
19845
+ }
19846
+ const envelope = result.data;
19847
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
19848
+ process.stderr.write(chunk);
19849
+ } });
19850
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
19851
+ });
19852
+ }
19853
+ };
19854
+ //#endregion
19855
+ //#region src/oclif/attachments.ts
19856
+ function readAttachmentBytes(path, readFile) {
19857
+ try {
19858
+ return Buffer.from(readFile(path));
19859
+ } catch (error) {
19860
+ const detail = error instanceof Error ? error.message : String(error);
19861
+ throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
19862
+ }
19863
+ }
19864
+ function hasControlCharacter(value) {
19865
+ return Array.from(value).some((character) => {
19866
+ const code = character.charCodeAt(0);
19867
+ return code <= 31 || code >= 127 && code <= 159;
19868
+ });
19869
+ }
19870
+ function validateAttachmentFilename(path, filename) {
19871
+ if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
19872
+ if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
19873
+ }
19874
+ function readAttachmentFiles(paths, readFile = readFileSync) {
19875
+ if (!paths || paths.length === 0) return void 0;
19876
+ return paths.map((path) => {
19877
+ const filename = basename(path);
19878
+ validateAttachmentFilename(path, filename);
19879
+ const bytes = readAttachmentBytes(path, readFile);
19880
+ if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
19881
+ return {
19882
+ content_base64: bytes.toString("base64"),
19883
+ filename
19884
+ };
19885
+ });
19886
+ }
19887
+ //#endregion
19888
+ //#region src/oclif/commands/send.ts
19889
+ var SendCommand = class SendCommand extends Command {
19890
+ static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
19891
+
19892
+ --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
19893
+ --subject defaults to the first line of the body when omitted.
19894
+ --attachment attaches a file; repeat it to attach multiple files.
19895
+
19896
+ For the full flag set (custom message-id threading on the wire,
19897
+ references arrays, etc.), use \`primitive sending send\`.`;
19898
+ static summary = "Send an email (simplified, agent-friendly)";
19899
+ static examples = [
19900
+ "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
19901
+ "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
19902
+ "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
19903
+ "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
19904
+ "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
19905
+ "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
19906
+ "<%= config.bin %> send --to inbox@your-managed-domain.primitive.email --body 'self-loop smoke test' --wait # any *.primitive.email address routes back to the sending account; useful for proving outbound + inbound work end-to-end"
19907
+ ];
19400
19908
  static flags = {
19909
+ "api-key": Flags.string({
19910
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
19911
+ env: "PRIMITIVE_API_KEY"
19912
+ }),
19401
19913
  "api-base-url-1": Flags.string({
19402
19914
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19403
19915
  env: "PRIMITIVE_API_BASE_URL_1",
19404
19916
  hidden: true
19405
19917
  }),
19406
- force: Flags.boolean({
19407
- char: "f",
19408
- description: "Replace saved credentials after verification"
19918
+ "api-base-url-2": Flags.string({
19919
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
19920
+ env: "PRIMITIVE_API_BASE_URL_2",
19921
+ hidden: true
19409
19922
  }),
19410
- "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19923
+ to: Flags.string({
19924
+ description: "Recipient address (e.g. alice@example.com).",
19925
+ required: true
19926
+ }),
19927
+ from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
19928
+ subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
19929
+ body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
19930
+ "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. This does not attach the file; use --attachment for file attachments. Mutually exclusive with --body and --body-stdin." }),
19931
+ "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
19932
+ html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
19933
+ "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
19934
+ "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
19935
+ attachment: Flags.string({
19936
+ description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
19937
+ multiple: true
19938
+ }),
19939
+ "in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
19940
+ wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
19941
+ "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
19942
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
19411
19943
  };
19412
19944
  async run() {
19413
- const { args, flags } = await this.parse(SignupConfirmCommand);
19414
- let releaseCredentialsLock;
19415
- try {
19416
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19417
- } catch (error) {
19418
- throw cliError$1(error instanceof Error ? error.message : String(error));
19419
- }
19420
- try {
19421
- await runSignupConfirmWithCredentialLock({
19422
- code: args.code,
19423
- configDir: this.config.configDir,
19424
- email: args.email,
19425
- flags
19426
- });
19427
- } finally {
19428
- releaseCredentialsLock();
19429
- }
19430
- }
19431
- };
19432
- var SignupResendCommand = class SignupResendCommand extends Command {
19433
- static args = { email: Args.string({
19434
- description: "Email address used to start signup",
19435
- required: true
19436
- }) };
19437
- static description = "Resend the verification code for a pending signup.";
19438
- static summary = "Resend signup verification code";
19439
- static examples = ["<%= config.bin %> signup resend user@example.com"];
19440
- static flags = { "api-base-url-1": Flags.string({
19441
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19442
- env: "PRIMITIVE_API_BASE_URL_1",
19443
- hidden: true
19444
- }) };
19445
- async run() {
19446
- const { args, flags } = await this.parse(SignupResendCommand);
19447
- let releaseCredentialsLock;
19448
- try {
19449
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19450
- } catch (error) {
19451
- throw cliError$1(error instanceof Error ? error.message : String(error));
19452
- }
19453
- try {
19454
- await runSignupResendWithCredentialLock({
19455
- configDir: this.config.configDir,
19456
- email: args.email,
19457
- flags
19945
+ const { flags } = await this.parse(SendCommand);
19946
+ const bodies = resolveMessageBodies({
19947
+ body: flags.body,
19948
+ bodyFile: flags["body-file"],
19949
+ bodyStdin: flags["body-stdin"],
19950
+ html: flags.html,
19951
+ htmlFile: flags["html-file"],
19952
+ htmlStdin: flags["html-stdin"]
19953
+ });
19954
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
19955
+ const attachments = readAttachmentFiles(flags.attachment);
19956
+ await runWithTiming(flags.time, async () => {
19957
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
19958
+ apiKey: flags["api-key"],
19959
+ apiBaseUrl1: flags["api-base-url-1"],
19960
+ apiBaseUrl2: flags["api-base-url-2"],
19961
+ configDir: this.config.configDir
19458
19962
  });
19459
- } finally {
19460
- releaseCredentialsLock();
19461
- }
19462
- }
19463
- };
19464
- var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
19465
- static description = "Run the full signup flow in one interactive terminal session.";
19466
- static summary = "Run interactive account signup";
19467
- static examples = ["<%= config.bin %> signup interactive"];
19468
- static flags = commonStartFlags();
19469
- async run() {
19470
- const { flags } = await this.parse(SignupInteractiveCommand);
19471
- let releaseCredentialsLock;
19472
- try {
19473
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19474
- } catch (error) {
19475
- throw cliError$1(error instanceof Error ? error.message : String(error));
19476
- }
19477
- try {
19478
- await runSignupInteractiveWithCredentialLock({
19479
- configDir: this.config.configDir,
19480
- flags
19963
+ const authFailureContext = {
19964
+ auth,
19965
+ baseUrlOverridden,
19966
+ configDir: this.config.configDir
19967
+ };
19968
+ const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
19969
+ const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
19970
+ const result = await sendEmail({
19971
+ body: {
19972
+ from,
19973
+ to: flags.to,
19974
+ subject,
19975
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
19976
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
19977
+ ...attachments !== void 0 ? { attachments } : {},
19978
+ ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
19979
+ ...flags.wait !== void 0 ? { wait: flags.wait } : {},
19980
+ ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
19981
+ },
19982
+ client: apiClient._sendClient,
19983
+ responseStyle: "fields"
19481
19984
  });
19482
- } finally {
19483
- releaseCredentialsLock();
19484
- }
19985
+ if (result.error) {
19986
+ const errorPayload = extractErrorPayload(result.error);
19987
+ writeErrorWithHints(errorPayload);
19988
+ surfaceUnauthorizedHint({
19989
+ ...authFailureContext,
19990
+ payload: errorPayload
19991
+ });
19992
+ process.exitCode = 1;
19993
+ return;
19994
+ }
19995
+ const envelope = result.data;
19996
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
19997
+ process.stderr.write(chunk);
19998
+ } });
19999
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
20000
+ });
19485
20001
  }
19486
20002
  };
19487
20003
  //#endregion
@@ -19496,6 +20012,34 @@ const SIGNIN_OTP_COPY = {
19496
20012
  resendCommand: (email) => `signin otp resend ${email}`,
19497
20013
  startCommand: (email) => `signin otp ${email}`
19498
20014
  };
20015
+ const SIGNIN_EMAIL_COPY = {
20016
+ actionNoun: "sign-in",
20017
+ actionGerund: "signing in",
20018
+ confirmCommand: (email) => `signin confirm ${email} <code>`,
20019
+ resendCommand: (email) => `signin resend ${email}`,
20020
+ startCommand: (email) => `signin ${email}`
20021
+ };
20022
+ const LOGIN_EMAIL_COPY = {
20023
+ actionNoun: "login",
20024
+ actionGerund: "logging in",
20025
+ confirmCommand: (email) => `login confirm ${email} <code>`,
20026
+ resendCommand: (email) => `login resend ${email}`,
20027
+ startCommand: (email) => `login ${email}`
20028
+ };
20029
+ const LOGIN_OTP_COPY = {
20030
+ actionNoun: "login",
20031
+ actionGerund: "logging in",
20032
+ confirmCommand: (email) => `login otp confirm ${email} <code>`,
20033
+ resendCommand: (email) => `login otp resend ${email}`,
20034
+ startCommand: (email) => `login otp ${email}`
20035
+ };
20036
+ const OTP_COPY = {
20037
+ actionNoun: "email-code auth",
20038
+ actionGerund: "authenticating",
20039
+ confirmCommand: (email) => `otp confirm ${email} <code>`,
20040
+ resendCommand: (email) => `otp resend ${email}`,
20041
+ startCommand: (email) => `otp ${email}`
20042
+ };
19499
20043
  function acquireCredentialsLock(configDir) {
19500
20044
  try {
19501
20045
  return acquireCliCredentialsLock(configDir);
@@ -19514,31 +20058,91 @@ function commonOtpStartFlags() {
19514
20058
  "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19515
20059
  force: Flags.boolean({
19516
20060
  char: "f",
19517
- description: "Replace saved credentials or pending sign-in state when needed"
20061
+ description: "Replace saved credentials or pending email-code auth state when needed"
19518
20062
  }),
19519
20063
  "signup-code": Flags.string({
19520
- description: "Signup code required to start OTP sign-in",
20064
+ description: "Signup code required to start email-code sign-in",
19521
20065
  env: "PRIMITIVE_SIGNUP_CODE"
19522
20066
  })
19523
20067
  };
19524
20068
  }
19525
- var SigninCommand = class extends LoginCommand {
19526
- static description = `Sign in to an existing Primitive account with browser approval and save an org-scoped OAuth session locally.
20069
+ var SigninCommand = class extends LoginCommand$1 {
20070
+ static args = { email: Args.string({
20071
+ description: "Email address for email-code sign-in. Omit it to use browser approval.",
20072
+ required: false
20073
+ }) };
20074
+ static description = `Sign in or log in to an existing Primitive account and save an org-scoped OAuth session locally.
19527
20075
 
19528
- This is the canonical sign-in command. It defaults to the same browser approval flow as \`primitive signin browser\`. For email-code sign-in, use \`primitive signin otp <email> --signup-code <code>\`, then \`primitive signin otp confirm <email> <code>\`. For new account creation, use \`primitive signup <email>\`.`;
20076
+ Run \`primitive signin <email> --signup-code <code> --accept-terms\` for email-code sign-in, then \`primitive signin confirm <email> <code>\`. Run \`primitive signin\` with no email to use browser approval; \`primitive signin browser\` is the explicit browser form. \`primitive login\` supports the same flows with login-shaped commands. \`primitive otp <email>\` is the shortest email-code auth form. For new account creation, use \`primitive signup <email>\`.`;
19529
20077
  static summary = "Sign in to an existing account";
19530
20078
  static examples = [
19531
20079
  "<%= config.bin %> signin",
19532
20080
  "<%= config.bin %> signin browser",
19533
20081
  "<%= config.bin %> signin --no-browser",
20082
+ "<%= config.bin %> signin user@example.com --signup-code invite-code --accept-terms",
20083
+ "<%= config.bin %> signin confirm user@example.com 123456",
19534
20084
  "<%= config.bin %> signin otp user@example.com --signup-code invite-code --accept-terms",
19535
20085
  "<%= config.bin %> signin otp confirm user@example.com 123456"
19536
20086
  ];
20087
+ static flags = {
20088
+ ...LoginCommand$1.flags,
20089
+ ...commonOtpStartFlags(),
20090
+ force: Flags.boolean({
20091
+ char: "f",
20092
+ description: "Replace saved credentials or pending email-code auth state when needed, without first verifying the existing session"
20093
+ })
20094
+ };
20095
+ async run() {
20096
+ const commandClass = this.constructor;
20097
+ const { args, flags } = await this.parse(commandClass);
20098
+ if (!args.email) {
20099
+ if (flags["signup-code"] || flags["accept-terms"]) throw cliError(`Email-code auth needs an email address. Run \`primitive ${this.emailCodeCopy().startCommand("<email>")} --signup-code <code> --accept-terms\`.`);
20100
+ await this.runBrowserLogin(flags, this.retryCommand());
20101
+ return;
20102
+ }
20103
+ const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
20104
+ try {
20105
+ await runSignupStartWithCredentialLock({
20106
+ configDir: this.config.configDir,
20107
+ copy: this.emailCodeCopy(),
20108
+ email: args.email,
20109
+ flags
20110
+ });
20111
+ } finally {
20112
+ releaseCredentialsLock();
20113
+ }
20114
+ }
19537
20115
  retryCommand() {
19538
20116
  return "signin";
19539
20117
  }
20118
+ emailCodeCopy() {
20119
+ return SIGNIN_EMAIL_COPY;
20120
+ }
20121
+ };
20122
+ var LoginCommand = class extends SigninCommand {
20123
+ static args = SigninCommand.args;
20124
+ static description = `Log in or sign in to an existing Primitive account and save an org-scoped OAuth session locally.
20125
+
20126
+ Run \`primitive login <email> --signup-code <code> --accept-terms\` for email-code login, then \`primitive login confirm <email> <code>\`. Run \`primitive login\` with no email to use browser approval; \`primitive login browser\` is the explicit browser form. \`primitive signin\` supports the same flows with signin-shaped commands. \`primitive otp <email>\` is the shortest email-code auth form. For new account creation, use \`primitive signup <email>\`.`;
20127
+ static summary = "Log in to an existing account";
20128
+ static examples = [
20129
+ "<%= config.bin %> login",
20130
+ "<%= config.bin %> login browser",
20131
+ "<%= config.bin %> login --no-browser",
20132
+ "<%= config.bin %> login user@example.com --signup-code invite-code --accept-terms",
20133
+ "<%= config.bin %> login confirm user@example.com 123456",
20134
+ "<%= config.bin %> login otp user@example.com --signup-code invite-code --accept-terms",
20135
+ "<%= config.bin %> login otp confirm user@example.com 123456"
20136
+ ];
20137
+ static flags = SigninCommand.flags;
20138
+ retryCommand() {
20139
+ return "login";
20140
+ }
20141
+ emailCodeCopy() {
20142
+ return LOGIN_EMAIL_COPY;
20143
+ }
19540
20144
  };
19541
- var SigninBrowserCommand = class extends LoginCommand {
20145
+ var SigninBrowserCommand = class extends LoginCommand$1 {
19542
20146
  static description = "Sign in to an existing Primitive account by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
19543
20147
  static summary = "Sign in with browser approval";
19544
20148
  static examples = [
@@ -19551,7 +20155,20 @@ var SigninBrowserCommand = class extends LoginCommand {
19551
20155
  return "signin browser";
19552
20156
  }
19553
20157
  };
19554
- var SigninOtpCommand = class SigninOtpCommand extends Command {
20158
+ var LoginBrowserCommand = class extends LoginCommand$1 {
20159
+ static description = "Log in to an existing Primitive account by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
20160
+ static summary = "Log in with browser approval";
20161
+ static examples = [
20162
+ "<%= config.bin %> login browser",
20163
+ "<%= config.bin %> login browser --device-name work-laptop",
20164
+ "<%= config.bin %> login browser --no-browser",
20165
+ "<%= config.bin %> login browser --force"
20166
+ ];
20167
+ retryCommand() {
20168
+ return "login browser";
20169
+ }
20170
+ };
20171
+ var SigninOtpCommand = class extends Command {
19555
20172
  static args = { email: Args.string({
19556
20173
  description: "Email address to sign in with",
19557
20174
  required: false
@@ -19561,12 +20178,13 @@ var SigninOtpCommand = class SigninOtpCommand extends Command {
19561
20178
  static examples = ["<%= config.bin %> signin otp user@example.com --signup-code invite-code --accept-terms", "<%= config.bin %> signin otp confirm user@example.com 123456"];
19562
20179
  static flags = commonOtpStartFlags();
19563
20180
  async run() {
19564
- const { args, flags } = await this.parse(SigninOtpCommand);
20181
+ const commandClass = this.constructor;
20182
+ const { args, flags } = await this.parse(commandClass);
19565
20183
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19566
20184
  try {
19567
20185
  await runSignupStartWithCredentialLock({
19568
20186
  configDir: this.config.configDir,
19569
- copy: SIGNIN_OTP_COPY,
20187
+ copy: this.emailCodeCopy(),
19570
20188
  email: args.email,
19571
20189
  flags
19572
20190
  });
@@ -19574,8 +20192,31 @@ var SigninOtpCommand = class SigninOtpCommand extends Command {
19574
20192
  releaseCredentialsLock();
19575
20193
  }
19576
20194
  }
20195
+ emailCodeCopy() {
20196
+ return SIGNIN_OTP_COPY;
20197
+ }
19577
20198
  };
19578
- var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
20199
+ var LoginOtpCommand = class extends SigninOtpCommand {
20200
+ static args = SigninOtpCommand.args;
20201
+ static description = "Start email-code login using Primitive's signup/auth OTP flow, send a verification code, and save the pending token locally. Requires a signup code.";
20202
+ static summary = "Start OTP login";
20203
+ static examples = ["<%= config.bin %> login otp user@example.com --signup-code invite-code --accept-terms", "<%= config.bin %> login otp confirm user@example.com 123456"];
20204
+ static flags = SigninOtpCommand.flags;
20205
+ emailCodeCopy() {
20206
+ return LOGIN_OTP_COPY;
20207
+ }
20208
+ };
20209
+ var OtpCommand = class extends SigninOtpCommand {
20210
+ static args = SigninOtpCommand.args;
20211
+ static description = "Start email-code authentication, send a verification code, and save the pending token locally. Requires a signup code.";
20212
+ static summary = "Start email-code auth";
20213
+ static examples = ["<%= config.bin %> otp user@example.com --signup-code invite-code --accept-terms", "<%= config.bin %> otp confirm user@example.com 123456"];
20214
+ static flags = SigninOtpCommand.flags;
20215
+ emailCodeCopy() {
20216
+ return OTP_COPY;
20217
+ }
20218
+ };
20219
+ var SigninOtpConfirmCommand = class extends Command {
19579
20220
  static args = {
19580
20221
  email: Args.string({
19581
20222
  description: "Email address used to start OTP sign-in",
@@ -19602,13 +20243,14 @@ var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19602
20243
  "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19603
20244
  };
19604
20245
  async run() {
19605
- const { args, flags } = await this.parse(SigninOtpConfirmCommand);
20246
+ const commandClass = this.constructor;
20247
+ const { args, flags } = await this.parse(commandClass);
19606
20248
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19607
20249
  try {
19608
20250
  await runSignupConfirmWithCredentialLock({
19609
20251
  code: args.code,
19610
20252
  configDir: this.config.configDir,
19611
- copy: SIGNIN_OTP_COPY,
20253
+ copy: this.emailCodeCopy(),
19612
20254
  email: args.email,
19613
20255
  flags
19614
20256
  });
@@ -19616,8 +20258,51 @@ var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19616
20258
  releaseCredentialsLock();
19617
20259
  }
19618
20260
  }
20261
+ emailCodeCopy() {
20262
+ return SIGNIN_OTP_COPY;
20263
+ }
20264
+ };
20265
+ var SigninConfirmCommand = class extends SigninOtpConfirmCommand {
20266
+ static args = SigninOtpConfirmCommand.args;
20267
+ static description = "Confirm a pending email-code sign-in, create an OAuth session, and save CLI credentials locally.";
20268
+ static summary = "Confirm email-code sign-in";
20269
+ static examples = ["<%= config.bin %> signin confirm user@example.com 123456", "<%= config.bin %> signin confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20270
+ static flags = SigninOtpConfirmCommand.flags;
20271
+ emailCodeCopy() {
20272
+ return SIGNIN_EMAIL_COPY;
20273
+ }
19619
20274
  };
19620
- var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
20275
+ var LoginConfirmCommand = class extends SigninOtpConfirmCommand {
20276
+ static args = SigninOtpConfirmCommand.args;
20277
+ static description = "Confirm a pending email-code login, create an OAuth session, and save CLI credentials locally.";
20278
+ static summary = "Confirm email-code login";
20279
+ static examples = ["<%= config.bin %> login confirm user@example.com 123456", "<%= config.bin %> login confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20280
+ static flags = SigninOtpConfirmCommand.flags;
20281
+ emailCodeCopy() {
20282
+ return LOGIN_EMAIL_COPY;
20283
+ }
20284
+ };
20285
+ var LoginOtpConfirmCommand = class extends SigninOtpConfirmCommand {
20286
+ static args = SigninOtpConfirmCommand.args;
20287
+ static description = "Confirm a pending OTP login, create an OAuth session, and save CLI credentials locally.";
20288
+ static summary = "Confirm OTP login";
20289
+ static examples = ["<%= config.bin %> login otp confirm user@example.com 123456", "<%= config.bin %> login otp confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20290
+ static flags = SigninOtpConfirmCommand.flags;
20291
+ emailCodeCopy() {
20292
+ return LOGIN_OTP_COPY;
20293
+ }
20294
+ };
20295
+ var OtpConfirmCommand = class extends SigninOtpConfirmCommand {
20296
+ static args = SigninOtpConfirmCommand.args;
20297
+ static description = "Confirm pending email-code authentication, create an OAuth session, and save CLI credentials locally.";
20298
+ static summary = "Confirm email-code auth";
20299
+ static examples = ["<%= config.bin %> otp confirm user@example.com 123456", "<%= config.bin %> otp confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20300
+ static flags = SigninOtpConfirmCommand.flags;
20301
+ emailCodeCopy() {
20302
+ return OTP_COPY;
20303
+ }
20304
+ };
20305
+ var SigninOtpResendCommand = class extends Command {
19621
20306
  static args = { email: Args.string({
19622
20307
  description: "Email address used to start OTP sign-in",
19623
20308
  required: true
@@ -19631,12 +20316,13 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
19631
20316
  hidden: true
19632
20317
  }) };
19633
20318
  async run() {
19634
- const { args, flags } = await this.parse(SigninOtpResendCommand);
20319
+ const commandClass = this.constructor;
20320
+ const { args, flags } = await this.parse(commandClass);
19635
20321
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19636
20322
  try {
19637
20323
  await runSignupResendWithCredentialLock({
19638
20324
  configDir: this.config.configDir,
19639
- copy: SIGNIN_OTP_COPY,
20325
+ copy: this.emailCodeCopy(),
19640
20326
  email: args.email,
19641
20327
  flags
19642
20328
  });
@@ -19644,6 +20330,49 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
19644
20330
  releaseCredentialsLock();
19645
20331
  }
19646
20332
  }
20333
+ emailCodeCopy() {
20334
+ return SIGNIN_OTP_COPY;
20335
+ }
20336
+ };
20337
+ var SigninResendCommand = class extends SigninOtpResendCommand {
20338
+ static args = SigninOtpResendCommand.args;
20339
+ static description = "Resend the verification code for a pending email-code sign-in.";
20340
+ static summary = "Resend email-code sign-in code";
20341
+ static examples = ["<%= config.bin %> signin resend user@example.com"];
20342
+ static flags = SigninOtpResendCommand.flags;
20343
+ emailCodeCopy() {
20344
+ return SIGNIN_EMAIL_COPY;
20345
+ }
20346
+ };
20347
+ var LoginResendCommand = class extends SigninOtpResendCommand {
20348
+ static args = SigninOtpResendCommand.args;
20349
+ static description = "Resend the verification code for a pending email-code login.";
20350
+ static summary = "Resend email-code login code";
20351
+ static examples = ["<%= config.bin %> login resend user@example.com"];
20352
+ static flags = SigninOtpResendCommand.flags;
20353
+ emailCodeCopy() {
20354
+ return LOGIN_EMAIL_COPY;
20355
+ }
20356
+ };
20357
+ var LoginOtpResendCommand = class extends SigninOtpResendCommand {
20358
+ static args = SigninOtpResendCommand.args;
20359
+ static description = "Resend the verification code for a pending OTP login.";
20360
+ static summary = "Resend OTP login code";
20361
+ static examples = ["<%= config.bin %> login otp resend user@example.com"];
20362
+ static flags = SigninOtpResendCommand.flags;
20363
+ emailCodeCopy() {
20364
+ return LOGIN_OTP_COPY;
20365
+ }
20366
+ };
20367
+ var OtpResendCommand = class extends SigninOtpResendCommand {
20368
+ static args = SigninOtpResendCommand.args;
20369
+ static description = "Resend the verification code for pending email-code authentication.";
20370
+ static summary = "Resend email-code auth code";
20371
+ static examples = ["<%= config.bin %> otp resend user@example.com"];
20372
+ static flags = SigninOtpResendCommand.flags;
20373
+ emailCodeCopy() {
20374
+ return OTP_COPY;
20375
+ }
19647
20376
  };
19648
20377
  //#endregion
19649
20378
  //#region src/oclif/commands/whoami.ts
@@ -19990,11 +20719,22 @@ const COMMANDS = {
19990
20719
  reply: ReplyCommand,
19991
20720
  chat: ChatCommand,
19992
20721
  login: LoginCommand,
20722
+ "login:browser": LoginBrowserCommand,
20723
+ "login:confirm": LoginConfirmCommand,
20724
+ "login:otp": LoginOtpCommand,
20725
+ "login:otp:confirm": LoginOtpConfirmCommand,
20726
+ "login:otp:resend": LoginOtpResendCommand,
20727
+ "login:resend": LoginResendCommand,
20728
+ otp: OtpCommand,
20729
+ "otp:confirm": OtpConfirmCommand,
20730
+ "otp:resend": OtpResendCommand,
19993
20731
  signin: SigninCommand,
19994
20732
  "signin:browser": SigninBrowserCommand,
20733
+ "signin:confirm": SigninConfirmCommand,
19995
20734
  "signin:otp": SigninOtpCommand,
19996
20735
  "signin:otp:confirm": SigninOtpConfirmCommand,
19997
20736
  "signin:otp:resend": SigninOtpResendCommand,
20737
+ "signin:resend": SigninResendCommand,
19998
20738
  signup: SignupCommand,
19999
20739
  "signup:confirm": SignupConfirmCommand,
20000
20740
  "signup:interactive": SignupInteractiveCommand,