@primitivedotdev/sdk 0.19.0 → 0.21.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.
@@ -0,0 +1,171 @@
1
+ import { Command, Errors, Flags } from "@oclif/core";
2
+ import { PrimitiveApiClient } from "../../api/index.js";
3
+ import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
4
+ import { resolveCliAuth } from "../auth.js";
5
+ import { formatHeader, formatRow, pickIdWidth } from "./emails-latest.js";
6
+ import { collectNewAcceptedEmails, DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, DEFAULT_EMAIL_POLL_PAGE_SIZE, fetchEmailSearchPage, filtersFromFlags, MAX_EMAIL_POLL_PAGE_SIZE, sinceFromFlags, sleep, } from "./emails-poll.js";
7
+ const DEFAULT_WAIT_TIMEOUT_SECONDS = 300;
8
+ function cliError(message) {
9
+ return new Errors.CLIError(message, { exit: 1 });
10
+ }
11
+ class EmailsWaitCommand extends Command {
12
+ static description = "Poll until matching inbound emails arrive, printing each match as it is found.";
13
+ static summary = "Wait for matching inbound emails";
14
+ static examples = [
15
+ "<%= config.bin %> emails wait --to test@example.com",
16
+ "<%= config.bin %> emails wait --subject verify --number 5 --timeout 120",
17
+ "<%= config.bin %> emails wait --q 'domain:example.com' --table",
18
+ ];
19
+ static flags = {
20
+ "api-key": Flags.string({
21
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
22
+ env: "PRIMITIVE_API_KEY",
23
+ }),
24
+ "api-base-url-1": Flags.string({
25
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
26
+ env: "PRIMITIVE_API_BASE_URL_1",
27
+ hidden: true,
28
+ }),
29
+ "api-base-url-2": Flags.string({
30
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
31
+ env: "PRIMITIVE_API_BASE_URL_2",
32
+ hidden: true,
33
+ }),
34
+ body: Flags.string({
35
+ description: "Full-text body filter",
36
+ }),
37
+ domain: Flags.string({
38
+ description: "Filter by inbound email domain",
39
+ }),
40
+ "domain-id": Flags.string({
41
+ description: "Filter by domain UUID",
42
+ }),
43
+ from: Flags.string({
44
+ description: "Filter by sender address or domain",
45
+ }),
46
+ "has-attachment": Flags.boolean({
47
+ description: "Only match emails with one or more attachments",
48
+ }),
49
+ "include-existing": Flags.boolean({
50
+ description: "Start from existing matching emails instead of only new arrivals",
51
+ }),
52
+ interval: Flags.integer({
53
+ default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
54
+ description: "Seconds to wait between empty polls",
55
+ min: 1,
56
+ }),
57
+ number: Flags.integer({
58
+ char: "n",
59
+ default: 1,
60
+ description: "Exit successfully after this many matching emails",
61
+ min: 1,
62
+ }),
63
+ "page-size": Flags.integer({
64
+ default: DEFAULT_EMAIL_POLL_PAGE_SIZE,
65
+ description: `Emails to fetch per poll (1-${MAX_EMAIL_POLL_PAGE_SIZE})`,
66
+ max: MAX_EMAIL_POLL_PAGE_SIZE,
67
+ min: 1,
68
+ }),
69
+ q: Flags.string({
70
+ description: "Full-text search DSL query",
71
+ }),
72
+ since: Flags.string({
73
+ description: "Only match emails received on or after this date/time",
74
+ }),
75
+ "spam-score-gte": Flags.integer({
76
+ description: "Only match emails with spam score greater than or equal to this value",
77
+ }),
78
+ "spam-score-lt": Flags.integer({
79
+ description: "Only match emails with spam score below this value",
80
+ }),
81
+ subject: Flags.string({
82
+ description: "Full-text subject filter",
83
+ }),
84
+ table: Flags.boolean({
85
+ description: "Print a human-readable table instead of JSONL",
86
+ }),
87
+ timeout: Flags.integer({
88
+ default: DEFAULT_WAIT_TIMEOUT_SECONDS,
89
+ description: "Seconds to wait before exiting nonzero; 0 waits forever",
90
+ min: 0,
91
+ }),
92
+ to: Flags.string({
93
+ description: "Filter by recipient address or domain",
94
+ }),
95
+ };
96
+ async run() {
97
+ const { flags } = await this.parse(EmailsWaitCommand);
98
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
99
+ flags["api-base-url-2"] !== undefined;
100
+ const auth = resolveCliAuth({
101
+ apiKey: flags["api-key"],
102
+ apiBaseUrl1: flags["api-base-url-1"],
103
+ apiBaseUrl2: flags["api-base-url-2"],
104
+ configDir: this.config.configDir,
105
+ });
106
+ const apiClient = new PrimitiveApiClient({
107
+ apiKey: auth.apiKey,
108
+ apiBaseUrl1: auth.apiBaseUrl1,
109
+ apiBaseUrl2: auth.apiBaseUrl2,
110
+ });
111
+ let since;
112
+ try {
113
+ since = sinceFromFlags(flags);
114
+ }
115
+ catch (error) {
116
+ throw cliError(error instanceof Error ? error.message : String(error));
117
+ }
118
+ const filters = filtersFromFlags(flags);
119
+ const deadline = flags.timeout === 0 ? null : Date.now() + flags.timeout * 1000;
120
+ const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
121
+ const seenIds = new Set();
122
+ let cursor = null;
123
+ let matched = 0;
124
+ let headerPrinted = false;
125
+ while (deadline === null || Date.now() < deadline) {
126
+ const page = await fetchEmailSearchPage({
127
+ apiClient,
128
+ cursor,
129
+ filters,
130
+ pageSize: flags["page-size"],
131
+ since,
132
+ });
133
+ if (!page.ok) {
134
+ const payload = extractErrorPayload(page.error);
135
+ writeErrorWithHints(payload);
136
+ removeStaleSavedCredentialOnUnauthorized({
137
+ auth,
138
+ baseUrlOverridden,
139
+ configDir: this.config.configDir,
140
+ payload,
141
+ });
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ cursor = page.cursor ?? cursor;
146
+ for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
147
+ if (flags.table) {
148
+ if (!headerPrinted) {
149
+ process.stderr.write(`${formatHeader(idWidth)}\n`);
150
+ headerPrinted = true;
151
+ }
152
+ this.log(formatRow(email, idWidth));
153
+ }
154
+ else {
155
+ this.log(JSON.stringify(email));
156
+ }
157
+ matched += 1;
158
+ if (matched >= flags.number)
159
+ return;
160
+ }
161
+ if (page.rows.length > 0)
162
+ continue;
163
+ if (deadline !== null && Date.now() >= deadline)
164
+ break;
165
+ await sleep(flags.interval * 1000);
166
+ }
167
+ process.stderr.write(`Timed out waiting for ${flags.number} matching email${flags.number === 1 ? "" : "s"}; received ${matched}.\n`);
168
+ process.exitCode = 1;
169
+ }
170
+ }
171
+ export default EmailsWaitCommand;
@@ -0,0 +1,165 @@
1
+ import { Command, Errors, Flags } from "@oclif/core";
2
+ import { PrimitiveApiClient } from "../../api/index.js";
3
+ import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
4
+ import { resolveCliAuth } from "../auth.js";
5
+ import { formatHeader, formatRow, pickIdWidth } from "./emails-latest.js";
6
+ import { collectNewAcceptedEmails, DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, DEFAULT_EMAIL_POLL_PAGE_SIZE, fetchEmailSearchPage, filtersFromFlags, MAX_EMAIL_POLL_PAGE_SIZE, sinceFromFlags, sleep, } from "./emails-poll.js";
7
+ function cliError(message) {
8
+ return new Errors.CLIError(message, { exit: 1 });
9
+ }
10
+ class EmailsWatchCommand extends Command {
11
+ static description = "Poll for new inbound emails and print matching messages as they arrive.";
12
+ static summary = "Watch inbound emails with filters";
13
+ static examples = [
14
+ "<%= config.bin %> emails watch --to support@example.com",
15
+ "<%= config.bin %> emails watch --subject verify --seconds 300",
16
+ "<%= config.bin %> emails watch --number 20 --jsonl",
17
+ ];
18
+ static flags = {
19
+ "api-key": Flags.string({
20
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
21
+ env: "PRIMITIVE_API_KEY",
22
+ }),
23
+ "api-base-url-1": Flags.string({
24
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
25
+ env: "PRIMITIVE_API_BASE_URL_1",
26
+ hidden: true,
27
+ }),
28
+ "api-base-url-2": Flags.string({
29
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
30
+ env: "PRIMITIVE_API_BASE_URL_2",
31
+ hidden: true,
32
+ }),
33
+ body: Flags.string({
34
+ description: "Full-text body filter",
35
+ }),
36
+ domain: Flags.string({
37
+ description: "Filter by inbound email domain",
38
+ }),
39
+ "domain-id": Flags.string({
40
+ description: "Filter by domain UUID",
41
+ }),
42
+ from: Flags.string({
43
+ description: "Filter by sender address or domain",
44
+ }),
45
+ "has-attachment": Flags.boolean({
46
+ description: "Only show emails with one or more attachments",
47
+ }),
48
+ "include-existing": Flags.boolean({
49
+ description: "Start from existing matching emails instead of only new arrivals",
50
+ }),
51
+ interval: Flags.integer({
52
+ default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
53
+ description: "Seconds to wait between empty polls",
54
+ min: 1,
55
+ }),
56
+ jsonl: Flags.boolean({
57
+ description: "Print each email as one JSON object per line",
58
+ }),
59
+ number: Flags.integer({
60
+ description: "Exit after printing this many matching emails",
61
+ min: 1,
62
+ }),
63
+ "page-size": Flags.integer({
64
+ default: DEFAULT_EMAIL_POLL_PAGE_SIZE,
65
+ description: `Emails to fetch per poll (1-${MAX_EMAIL_POLL_PAGE_SIZE})`,
66
+ max: MAX_EMAIL_POLL_PAGE_SIZE,
67
+ min: 1,
68
+ }),
69
+ q: Flags.string({
70
+ description: "Full-text search DSL query",
71
+ }),
72
+ seconds: Flags.integer({
73
+ description: "Exit after this many seconds",
74
+ min: 1,
75
+ }),
76
+ since: Flags.string({
77
+ description: "Only show emails received on or after this date/time",
78
+ }),
79
+ "spam-score-gte": Flags.integer({
80
+ description: "Only show emails with spam score greater than or equal to this value",
81
+ }),
82
+ "spam-score-lt": Flags.integer({
83
+ description: "Only show emails with spam score below this value",
84
+ }),
85
+ subject: Flags.string({
86
+ description: "Full-text subject filter",
87
+ }),
88
+ to: Flags.string({
89
+ description: "Filter by recipient address or domain",
90
+ }),
91
+ };
92
+ async run() {
93
+ const { flags } = await this.parse(EmailsWatchCommand);
94
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
95
+ flags["api-base-url-2"] !== undefined;
96
+ const auth = resolveCliAuth({
97
+ apiKey: flags["api-key"],
98
+ apiBaseUrl1: flags["api-base-url-1"],
99
+ apiBaseUrl2: flags["api-base-url-2"],
100
+ configDir: this.config.configDir,
101
+ });
102
+ const apiClient = new PrimitiveApiClient({
103
+ apiKey: auth.apiKey,
104
+ apiBaseUrl1: auth.apiBaseUrl1,
105
+ apiBaseUrl2: auth.apiBaseUrl2,
106
+ });
107
+ let since;
108
+ try {
109
+ since = sinceFromFlags(flags);
110
+ }
111
+ catch (error) {
112
+ throw cliError(error instanceof Error ? error.message : String(error));
113
+ }
114
+ const filters = filtersFromFlags(flags);
115
+ const deadline = flags.seconds ? Date.now() + flags.seconds * 1000 : null;
116
+ const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
117
+ const seenIds = new Set();
118
+ let cursor = null;
119
+ let printed = 0;
120
+ let headerPrinted = false;
121
+ while (deadline === null || Date.now() < deadline) {
122
+ const page = await fetchEmailSearchPage({
123
+ apiClient,
124
+ cursor,
125
+ filters,
126
+ pageSize: flags["page-size"],
127
+ since,
128
+ });
129
+ if (!page.ok) {
130
+ const payload = extractErrorPayload(page.error);
131
+ writeErrorWithHints(payload);
132
+ removeStaleSavedCredentialOnUnauthorized({
133
+ auth,
134
+ baseUrlOverridden,
135
+ configDir: this.config.configDir,
136
+ payload,
137
+ });
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ cursor = page.cursor ?? cursor;
142
+ for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
143
+ if (flags.jsonl) {
144
+ this.log(JSON.stringify(email));
145
+ }
146
+ else {
147
+ if (!headerPrinted) {
148
+ process.stderr.write(`${formatHeader(idWidth)}\n`);
149
+ headerPrinted = true;
150
+ }
151
+ this.log(formatRow(email, idWidth));
152
+ }
153
+ printed += 1;
154
+ if (flags.number && printed >= flags.number)
155
+ return;
156
+ }
157
+ if (page.rows.length > 0)
158
+ continue;
159
+ if (deadline !== null && Date.now() >= deadline)
160
+ break;
161
+ await sleep(flags.interval * 1000);
162
+ }
163
+ }
164
+ }
165
+ export default EmailsWatchCommand;
@@ -0,0 +1,117 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { createFunction } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
+ import { resolveCliAuth } from "../auth.js";
6
+ // `primitive functions:deploy` is the agent-grade shortcut for
7
+ // `functions:create-function`. The underlying operation takes `code`
8
+ // as a string in the JSON body, which is awkward at the CLI for
9
+ // multi-line bundles: agents would otherwise have to shell-escape an
10
+ // entire ESM file or write a temp body.json. This command reads the
11
+ // bundle straight off disk via --file, so the natural workflow is:
12
+ //
13
+ // esbuild handler.ts --bundle --format=esm --outfile=bundle.js
14
+ // primitive functions:deploy --name myfn --file bundle.js
15
+ //
16
+ // Source maps follow the same shape via --source-map-file. They are
17
+ // stored only on the runtime side (not in our database) so dropping
18
+ // them later in the pipeline is fine; the CLI just hands them through.
19
+ //
20
+ // For full control (raw body, --raw-body JSON, etc.) the underlying
21
+ // `functions:create-function` operation stays available.
22
+ class FunctionsDeployCommand extends Command {
23
+ static description = `Deploy a new function from a bundled handler file. Agent-grade shortcut for functions:create-function.
24
+
25
+ Reads the bundle off disk (--file) instead of forcing the caller to
26
+ serialize the source into a JSON body. Use the underlying operation
27
+ \`functions:create-function\` if you need the full flag surface
28
+ (raw-body JSON, etc.).`;
29
+ static summary = "Deploy a new function from a bundled handler file";
30
+ static examples = [
31
+ "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js",
32
+ "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
33
+ ];
34
+ static flags = {
35
+ "api-key": Flags.string({
36
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
37
+ env: "PRIMITIVE_API_KEY",
38
+ }),
39
+ "api-base-url-1": Flags.string({
40
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
41
+ env: "PRIMITIVE_API_BASE_URL_1",
42
+ hidden: true,
43
+ }),
44
+ "api-base-url-2": Flags.string({
45
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
46
+ env: "PRIMITIVE_API_BASE_URL_2",
47
+ hidden: true,
48
+ }),
49
+ name: Flags.string({
50
+ description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
51
+ required: true,
52
+ }),
53
+ file: Flags.string({
54
+ description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
55
+ required: true,
56
+ }),
57
+ "source-map-file": Flags.string({
58
+ description: "Optional path to a source map for the bundle. Stored only on the runtime side and used to symbolicate stack traces.",
59
+ }),
60
+ time: Flags.boolean({
61
+ description: TIME_FLAG_DESCRIPTION,
62
+ }),
63
+ };
64
+ async run() {
65
+ const { flags } = await this.parse(FunctionsDeployCommand);
66
+ await runWithTiming(flags.time, async () => {
67
+ // Reads are inside the timed block so --time captures disk I/O
68
+ // alongside the API call. A pathological filesystem (NFS, slow
69
+ // FUSE mount) showing up here is exactly the kind of latency
70
+ // surprise --time is meant to surface.
71
+ const code = readTextFileFlag(flags.file, "--file");
72
+ const sourceMap = flags["source-map-file"]
73
+ ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
74
+ : undefined;
75
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
76
+ flags["api-base-url-2"] !== undefined;
77
+ const auth = resolveCliAuth({
78
+ apiKey: flags["api-key"],
79
+ apiBaseUrl1: flags["api-base-url-1"],
80
+ apiBaseUrl2: flags["api-base-url-2"],
81
+ configDir: this.config.configDir,
82
+ });
83
+ const apiClient = new PrimitiveApiClient({
84
+ apiKey: auth.apiKey,
85
+ apiBaseUrl1: auth.apiBaseUrl1,
86
+ apiBaseUrl2: auth.apiBaseUrl2,
87
+ });
88
+ const authFailureContext = {
89
+ auth,
90
+ baseUrlOverridden,
91
+ configDir: this.config.configDir,
92
+ };
93
+ const result = await createFunction({
94
+ body: {
95
+ name: flags.name,
96
+ code,
97
+ ...(sourceMap !== undefined ? { sourceMap } : {}),
98
+ },
99
+ client: apiClient.client,
100
+ responseStyle: "fields",
101
+ });
102
+ if (result.error) {
103
+ const errorPayload = extractErrorPayload(result.error);
104
+ writeErrorWithHints(errorPayload);
105
+ removeStaleSavedCredentialOnUnauthorized({
106
+ ...authFailureContext,
107
+ payload: errorPayload,
108
+ });
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ const envelope = result.data;
113
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
114
+ });
115
+ }
116
+ }
117
+ export default FunctionsDeployCommand;
@@ -0,0 +1,106 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { updateFunction } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
+ import { resolveCliAuth } from "../auth.js";
6
+ // `primitive functions:redeploy` is the agent-grade shortcut for
7
+ // `functions:update-function`. Same file-reading ergonomic as
8
+ // functions:deploy but for an existing function. Use this to push a
9
+ // new bundle, OR to refresh secret bindings: passing the
10
+ // previously-deployed bundle (or any equivalent file) re-runs the
11
+ // deploy and refreshes env from the secrets table, which is how
12
+ // secret writes go live.
13
+ class FunctionsRedeployCommand extends Command {
14
+ static description = `Update or redeploy a function from a bundled handler file. Agent-grade shortcut for functions:update-function.
15
+
16
+ Use to push a new bundle OR to refresh secret bindings into the
17
+ running handler. The same file is fine for both: the deploy reads
18
+ the bindings table fresh on every call, so passing the existing
19
+ bundle picks up any secret writes since the last deploy.`;
20
+ static summary = "Redeploy a function from a bundled handler file";
21
+ static examples = [
22
+ "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
23
+ "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
24
+ ];
25
+ static flags = {
26
+ "api-key": Flags.string({
27
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
28
+ env: "PRIMITIVE_API_KEY",
29
+ }),
30
+ "api-base-url-1": Flags.string({
31
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
32
+ env: "PRIMITIVE_API_BASE_URL_1",
33
+ hidden: true,
34
+ }),
35
+ "api-base-url-2": Flags.string({
36
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
37
+ env: "PRIMITIVE_API_BASE_URL_2",
38
+ hidden: true,
39
+ }),
40
+ id: Flags.string({
41
+ description: "Function id (UUID). The function must already exist.",
42
+ required: true,
43
+ }),
44
+ file: Flags.string({
45
+ description: "Path to the bundled ESM handler file. Loaded as the `code` body field.",
46
+ required: true,
47
+ }),
48
+ "source-map-file": Flags.string({
49
+ description: "Optional path to a source map for the bundle. Used to symbolicate stack traces in the function's logs.",
50
+ }),
51
+ time: Flags.boolean({
52
+ description: TIME_FLAG_DESCRIPTION,
53
+ }),
54
+ };
55
+ async run() {
56
+ const { flags } = await this.parse(FunctionsRedeployCommand);
57
+ await runWithTiming(flags.time, async () => {
58
+ // Reads inside the timed block: --time captures disk I/O too,
59
+ // which is the latency the flag is meant to surface.
60
+ const code = readTextFileFlag(flags.file, "--file");
61
+ const sourceMap = flags["source-map-file"]
62
+ ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
63
+ : undefined;
64
+ const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
65
+ flags["api-base-url-2"] !== undefined;
66
+ const auth = resolveCliAuth({
67
+ apiKey: flags["api-key"],
68
+ apiBaseUrl1: flags["api-base-url-1"],
69
+ apiBaseUrl2: flags["api-base-url-2"],
70
+ configDir: this.config.configDir,
71
+ });
72
+ const apiClient = new PrimitiveApiClient({
73
+ apiKey: auth.apiKey,
74
+ apiBaseUrl1: auth.apiBaseUrl1,
75
+ apiBaseUrl2: auth.apiBaseUrl2,
76
+ });
77
+ const authFailureContext = {
78
+ auth,
79
+ baseUrlOverridden,
80
+ configDir: this.config.configDir,
81
+ };
82
+ const result = await updateFunction({
83
+ path: { id: flags.id },
84
+ body: {
85
+ code,
86
+ ...(sourceMap !== undefined ? { sourceMap } : {}),
87
+ },
88
+ client: apiClient.client,
89
+ responseStyle: "fields",
90
+ });
91
+ if (result.error) {
92
+ const errorPayload = extractErrorPayload(result.error);
93
+ writeErrorWithHints(errorPayload);
94
+ removeStaleSavedCredentialOnUnauthorized({
95
+ ...authFailureContext,
96
+ payload: errorPayload,
97
+ });
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ const envelope = result.data;
102
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
103
+ });
104
+ }
105
+ }
106
+ export default FunctionsRedeployCommand;
@@ -4,7 +4,7 @@ import { Command, Errors, Flags } from "@oclif/core";
4
4
  import { getAccount, pollCliLogin, startCliLogin, } from "../../api/generated/sdk.gen.js";
5
5
  import { PrimitiveApiClient } from "../../api/index.js";
6
6
  import { API_ERROR_CODES, extractErrorCode, extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
7
- import { acquireCliCredentialsLock, credentialsPath, loadCliCredentials, normalizeBaseUrl, saveCliCredentials, } from "../auth.js";
7
+ import { acquireCliCredentialsLock, credentialsPath, loadCliCredentials, normalizeApiBaseUrl1, normalizeApiBaseUrl2, saveCliCredentials, } from "../auth.js";
8
8
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
9
9
  function cliError(message) {
10
10
  return new Errors.CLIError(message, { exit: 1 });
@@ -36,13 +36,13 @@ function retryAfterSeconds(result) {
36
36
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
37
37
  }
38
38
  export async function checkExistingLogin(params) {
39
- const baseUrlOverridden = params.baseUrl !== undefined;
40
- const probeBaseUrl = baseUrlOverridden
41
- ? normalizeBaseUrl(params.baseUrl)
42
- : params.credentials.base_url;
39
+ const baseUrlOverridden = params.apiBaseUrl1 !== undefined;
40
+ const probeApiBaseUrl1 = baseUrlOverridden
41
+ ? normalizeApiBaseUrl1(params.apiBaseUrl1)
42
+ : params.credentials.api_base_url_1;
43
43
  const apiClient = new PrimitiveApiClient({
44
44
  apiKey: params.credentials.api_key,
45
- baseUrl: probeBaseUrl,
45
+ apiBaseUrl1: probeApiBaseUrl1,
46
46
  });
47
47
  const result = await (params.checkAccount ??
48
48
  ((client) => getAccount({
@@ -54,7 +54,10 @@ export async function checkExistingLogin(params) {
54
54
  const payload = extractErrorPayload(result.error);
55
55
  const auth = {
56
56
  apiKey: params.credentials.api_key,
57
- baseUrl: probeBaseUrl,
57
+ apiBaseUrl1: probeApiBaseUrl1,
58
+ // Host-2 isn't relevant to checkExistingLogin (login is on host-1
59
+ // only), but the auth shape requires it. Use the default.
60
+ apiBaseUrl2: normalizeApiBaseUrl2(undefined),
58
61
  credentials: params.credentials,
59
62
  source: "stored",
60
63
  };
@@ -84,9 +87,10 @@ class LoginCommand extends Command {
84
87
  "<%= config.bin %> login --force",
85
88
  ];
86
89
  static flags = {
87
- "base-url": Flags.string({
88
- description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
89
- env: "PRIMITIVE_API_URL",
90
+ "api-base-url-1": Flags.string({
91
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
92
+ env: "PRIMITIVE_API_BASE_URL_1",
93
+ hidden: true,
90
94
  }),
91
95
  "device-name": Flags.string({
92
96
  description: "Device name shown in the browser approval screen",
@@ -117,7 +121,7 @@ class LoginCommand extends Command {
117
121
  }
118
122
  }
119
123
  async runWithCredentialLock(flags) {
120
- const baseUrl = normalizeBaseUrl(flags["base-url"]);
124
+ const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
121
125
  let existing;
122
126
  try {
123
127
  existing = loadCliCredentials(this.config.configDir);
@@ -134,7 +138,7 @@ class LoginCommand extends Command {
134
138
  }
135
139
  else if (existing) {
136
140
  const existingStatus = await checkExistingLogin({
137
- baseUrl: flags["base-url"],
141
+ apiBaseUrl1: flags["api-base-url-1"],
138
142
  configDir: this.config.configDir,
139
143
  credentials: existing,
140
144
  });
@@ -150,7 +154,7 @@ class LoginCommand extends Command {
150
154
  throw cliError(`Already logged in${org}. Run \`primitive logout\` before logging in again.`);
151
155
  }
152
156
  }
153
- const apiClient = new PrimitiveApiClient({ baseUrl });
157
+ const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
154
158
  const deviceName = flags["device-name"] ?? hostname();
155
159
  const started = await startCliLogin({
156
160
  body: {
@@ -192,7 +196,7 @@ class LoginCommand extends Command {
192
196
  }
193
197
  saveCliCredentials(this.config.configDir, {
194
198
  api_key: login.api_key,
195
- base_url: baseUrl,
199
+ api_base_url_1: apiBaseUrl1,
196
200
  created_at: new Date().toISOString(),
197
201
  key_id: login.key_id,
198
202
  key_prefix: login.key_prefix,