@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.
- package/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +235 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +40 -5
- package/dist/{api-C5VR_Opg.js → api-BjzvA2Fy.js} +308 -6
- package/dist/{index-oRkCqj6u.d.ts → index-QTYQpSFt.d.ts} +999 -60
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oclif/api-command.js +87 -13
- package/dist/oclif/auth.js +65 -10
- package/dist/oclif/commands/emails-latest.js +23 -12
- package/dist/oclif/commands/emails-poll.js +121 -0
- package/dist/oclif/commands/emails-wait.js +171 -0
- package/dist/oclif/commands/emails-watch.js +165 -0
- package/dist/oclif/commands/functions-deploy.js +117 -0
- package/dist/oclif/commands/functions-redeploy.js +106 -0
- package/dist/oclif/commands/login.js +18 -14
- package/dist/oclif/commands/logout.js +9 -8
- package/dist/oclif/commands/send.js +21 -7
- package/dist/oclif/commands/whoami.js +15 -6
- package/dist/oclif/fish-completion.js +1 -1
- package/dist/oclif/index.js +16 -0
- package/dist/openapi/openapi.generated.js +1317 -51
- package/dist/openapi/operations.generated.js +995 -1
- package/oclif.manifest.json +2083 -335
- package/package.json +4 -1
|
@@ -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,
|
|
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.
|
|
40
|
-
const
|
|
41
|
-
?
|
|
42
|
-
: params.credentials.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
env: "
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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,
|