@primitivedotdev/cli 0.24.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/README.md +47 -0
- package/bin/run.js +5 -0
- package/dist/oclif/api-command.js +755 -0
- package/dist/oclif/auth.js +223 -0
- package/dist/oclif/commands/emails-latest.js +184 -0
- 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 +123 -0
- package/dist/oclif/commands/functions-init.js +262 -0
- package/dist/oclif/commands/functions-redeploy.js +112 -0
- package/dist/oclif/commands/functions-set-secret.js +212 -0
- package/dist/oclif/commands/login.js +236 -0
- package/dist/oclif/commands/logout.js +87 -0
- package/dist/oclif/commands/send.js +221 -0
- package/dist/oclif/commands/whoami.js +94 -0
- package/dist/oclif/fish-completion.js +87 -0
- package/dist/oclif/index.js +167 -0
- package/dist/oclif/lint/raw-send-mail-fetch.js +98 -0
- package/oclif.manifest.json +4287 -0
- package/package.json +108 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { openapiDocument, operationManifest, } from "@primitivedotdev/sdk/openapi";
|
|
2
|
+
function fishEscape(value) {
|
|
3
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
4
|
+
}
|
|
5
|
+
function toKebabCase(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
8
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
9
|
+
.replace(/^-+|-+$/g, "")
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function tagDescriptions() {
|
|
13
|
+
const descriptions = new Map();
|
|
14
|
+
const tags = (openapiDocument.tags ??
|
|
15
|
+
[]);
|
|
16
|
+
for (const tag of tags) {
|
|
17
|
+
if (tag.name) {
|
|
18
|
+
descriptions.set(toKebabCase(tag.name), tag.description ?? tag.name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return descriptions;
|
|
22
|
+
}
|
|
23
|
+
function operationCondition(operation) {
|
|
24
|
+
return `__fish_${fishEscape(BIN_PLACEHOLDER)}_using_operation ${fishEscape(operation.tagCommand)} ${fishEscape(operation.command)}`;
|
|
25
|
+
}
|
|
26
|
+
const BIN_PLACEHOLDER = "__BIN__";
|
|
27
|
+
export function renderFishCompletion(binName) {
|
|
28
|
+
const tagDescriptionByCommand = tagDescriptions();
|
|
29
|
+
const topLevelTopics = [
|
|
30
|
+
...new Set(operationManifest.map((operation) => operation.tagCommand)),
|
|
31
|
+
];
|
|
32
|
+
const lines = [
|
|
33
|
+
`function __fish_${binName}_needs_command`,
|
|
34
|
+
" set -l cmd (commandline -opc)",
|
|
35
|
+
" test (count $cmd) -le 1",
|
|
36
|
+
"end",
|
|
37
|
+
"",
|
|
38
|
+
`function __fish_${binName}_topic_needs_subcommand`,
|
|
39
|
+
" set -l cmd (commandline -opc)",
|
|
40
|
+
" test (count $cmd) -eq 2",
|
|
41
|
+
' and test "$cmd[2]" = "$argv[1]"',
|
|
42
|
+
"end",
|
|
43
|
+
"",
|
|
44
|
+
`function __fish_${binName}_using_operation`,
|
|
45
|
+
" set -l cmd (commandline -opc)",
|
|
46
|
+
" test (count $cmd) -ge 3",
|
|
47
|
+
' and test "$cmd[2]" = "$argv[1]"',
|
|
48
|
+
' and test "$cmd[3]" = "$argv[2]"',
|
|
49
|
+
"end",
|
|
50
|
+
"",
|
|
51
|
+
`function __fish_${binName}_using_root_command`,
|
|
52
|
+
" set -l cmd (commandline -opc)",
|
|
53
|
+
" test (count $cmd) -eq 2",
|
|
54
|
+
' and test "$cmd[2]" = "$argv[1]"',
|
|
55
|
+
"end",
|
|
56
|
+
"",
|
|
57
|
+
`complete -c ${binName} -f -n '__fish_${binName}_needs_command' -a 'list-operations' -d 'List all generated API operations'`,
|
|
58
|
+
`complete -c ${binName} -f -n '__fish_${binName}_needs_command' -a 'completion' -d 'Show shell completion output or installation instructions'`,
|
|
59
|
+
`complete -c ${binName} -f -n '__fish_${binName}_needs_command' -a 'autocomplete' -d 'Install or display shell autocomplete for bash, zsh, and powershell'`,
|
|
60
|
+
`complete -c ${binName} -f -n '__fish_${binName}_needs_command' -a 'help' -d 'Display help for ${binName}'`,
|
|
61
|
+
];
|
|
62
|
+
for (const topic of topLevelTopics) {
|
|
63
|
+
lines.push(`complete -c ${binName} -f -n '__fish_${binName}_needs_command' -a '${fishEscape(topic)}' -d '${fishEscape(tagDescriptionByCommand.get(topic) ?? topic)}'`);
|
|
64
|
+
}
|
|
65
|
+
lines.push(`complete -c ${binName} -f -n '__fish_${binName}_using_root_command completion' -a 'bash zsh powershell fish' -d 'Shell type'`);
|
|
66
|
+
for (const topic of topLevelTopics) {
|
|
67
|
+
const topicOperations = operationManifest.filter((operation) => operation.tagCommand === topic);
|
|
68
|
+
for (const operation of topicOperations) {
|
|
69
|
+
lines.push(`complete -c ${binName} -f -n '__fish_${binName}_topic_needs_subcommand ${fishEscape(topic)}' -a '${fishEscape(operation.command)}' -d '${fishEscape(operation.summary ?? `${operation.method} ${operation.path}`)}'`);
|
|
70
|
+
for (const parameter of [
|
|
71
|
+
...operation.pathParams,
|
|
72
|
+
...operation.queryParams,
|
|
73
|
+
]) {
|
|
74
|
+
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l '${fishEscape(parameter.name.replace(/_/g, "-"))}' -r -d '${fishEscape(parameter.description ?? parameter.name)}'`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY or saved primitive login credentials)'`);
|
|
77
|
+
if (operation.hasJsonBody) {
|
|
78
|
+
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body' -r -d 'JSON request body'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body-file' -r -d 'Path to a JSON file used as the request body'`);
|
|
79
|
+
}
|
|
80
|
+
if (operation.binaryResponse) {
|
|
81
|
+
lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'output' -r -d 'Write binary response bytes to a file'`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
lines.push(`complete -c ${binName} -l help -d 'Show help for ${binName}'`, `complete -c ${binName} -l version -d 'Show version for ${binName}'`);
|
|
86
|
+
return `${lines.join("\n")}\n`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Args, Command, Errors } from "@oclif/core";
|
|
2
|
+
import { operationManifest, } from "@primitivedotdev/sdk/openapi";
|
|
3
|
+
import { createOperationCommand } from "./api-command.js";
|
|
4
|
+
import EmailsLatestCommand from "./commands/emails-latest.js";
|
|
5
|
+
import EmailsWaitCommand from "./commands/emails-wait.js";
|
|
6
|
+
import EmailsWatchCommand from "./commands/emails-watch.js";
|
|
7
|
+
import FunctionsDeployCommand from "./commands/functions-deploy.js";
|
|
8
|
+
import FunctionsInitCommand from "./commands/functions-init.js";
|
|
9
|
+
import FunctionsRedeployCommand from "./commands/functions-redeploy.js";
|
|
10
|
+
import FunctionsSetSecretCommand from "./commands/functions-set-secret.js";
|
|
11
|
+
import LoginCommand from "./commands/login.js";
|
|
12
|
+
import LogoutCommand from "./commands/logout.js";
|
|
13
|
+
import SendCommand from "./commands/send.js";
|
|
14
|
+
import WhoamiCommand from "./commands/whoami.js";
|
|
15
|
+
import { renderFishCompletion } from "./fish-completion.js";
|
|
16
|
+
class ListOperationsCommand extends Command {
|
|
17
|
+
static description = "List all generated API operations as JSON. Useful for piping to `jq` to discover available commands, their request/response schemas, and per-field descriptions. For inspecting a single operation in detail, prefer `primitive describe <command>`.";
|
|
18
|
+
static summary = "List all generated API operations (JSON)";
|
|
19
|
+
async run() {
|
|
20
|
+
this.log(JSON.stringify(operationManifest, null, 2));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Looks up an operation manifest entry by its `<topic>:<command>` id
|
|
24
|
+
// (e.g. `emails:get-email`). On miss, returns up to 5 closest
|
|
25
|
+
// candidates by substring match so the caller can render a
|
|
26
|
+
// "did you mean" hint. Pure function: no oclif config dependency,
|
|
27
|
+
// so it's also unit-testable in isolation.
|
|
28
|
+
export function lookupOperation(id) {
|
|
29
|
+
const trimmed = id.trim();
|
|
30
|
+
const sep = trimmed.indexOf(":");
|
|
31
|
+
const tag = sep === -1 ? "" : trimmed.slice(0, sep);
|
|
32
|
+
const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
|
|
33
|
+
const match = operationManifest.find((op) => op.command === cmd && op.tagCommand === tag) ?? null;
|
|
34
|
+
if (match)
|
|
35
|
+
return { match, candidates: [] };
|
|
36
|
+
const candidates = operationManifest
|
|
37
|
+
.filter((op) => op.command.includes(cmd) || op.tagCommand.includes(tag))
|
|
38
|
+
.slice(0, 5)
|
|
39
|
+
.map((op) => op.tagCommand ? `${op.tagCommand}:${op.command}` : op.command);
|
|
40
|
+
return { match: null, candidates };
|
|
41
|
+
}
|
|
42
|
+
// `primitive describe <command>` is the operation-detail inspector
|
|
43
|
+
// the AGX walkthrough kept wanting. The information is already in
|
|
44
|
+
// the operation manifest emitted by `list-operations`, but agents
|
|
45
|
+
// don't intuitively reach for `list-operations | jq '.[] | select(...)'`
|
|
46
|
+
// when they want to know "what does the from_email field on this
|
|
47
|
+
// response actually mean." A direct command is more discoverable.
|
|
48
|
+
//
|
|
49
|
+
// Lookup is by the colon-joined command id (e.g. `emails:get-email`,
|
|
50
|
+
// `sending:send-email`, `account:get-account`). For top-level
|
|
51
|
+
// generated commands (without a topic), pass the bare command id.
|
|
52
|
+
class DescribeCommand extends Command {
|
|
53
|
+
static args = {
|
|
54
|
+
command: Args.string({
|
|
55
|
+
description: "Command id to describe, in `<topic>:<command>` form (e.g. `emails:get-email`). Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate.",
|
|
56
|
+
required: true,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
static description = `Print the full operation manifest entry for a single API command, including the path, request schema, response schema, and per-field descriptions sourced from the OpenAPI spec.
|
|
60
|
+
|
|
61
|
+
The manifest entry's \`responseSchema\` carries the inlined JSON Schema for the operation's 200/201 \`data\` envelope contents (\`$ref\`s resolved). Use it to look up what specific response fields mean. Examples:
|
|
62
|
+
|
|
63
|
+
# Which of EmailDetail's sender-shaped fields is canonical?
|
|
64
|
+
primitive describe emails:get-email | jq '.responseSchema.properties | keys'
|
|
65
|
+
primitive describe emails:get-email | jq -r '.responseSchema.properties.from_email.description'
|
|
66
|
+
|
|
67
|
+
# What does each value of SentEmailStatus mean?
|
|
68
|
+
primitive describe sending:get-sent-email | jq -r '.responseSchema.properties.status.description'
|
|
69
|
+
|
|
70
|
+
\`requestSchema\` is the same shape for the request body when one exists. For a single field across many operations at once, use \`primitive list-operations | jq\` instead.`;
|
|
71
|
+
static summary = "Describe a single API operation in detail";
|
|
72
|
+
static examples = [
|
|
73
|
+
"<%= config.bin %> describe emails:get-email",
|
|
74
|
+
"<%= config.bin %> describe sending:send-email",
|
|
75
|
+
];
|
|
76
|
+
async run() {
|
|
77
|
+
const { args } = await this.parse(DescribeCommand);
|
|
78
|
+
const { match, candidates } = lookupOperation(args.command);
|
|
79
|
+
if (!match) {
|
|
80
|
+
const hint = candidates.length > 0
|
|
81
|
+
? `Did you mean: ${candidates.join(", ")}?`
|
|
82
|
+
: "Run `primitive list-operations` to enumerate.";
|
|
83
|
+
throw new Errors.CLIError(`Unknown operation \`${args.command.trim()}\`. ${hint}`, { exit: 1 });
|
|
84
|
+
}
|
|
85
|
+
this.log(JSON.stringify(match, null, 2));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class CompletionCommand extends Command {
|
|
89
|
+
static args = {
|
|
90
|
+
shell: Args.string({
|
|
91
|
+
description: "Shell type",
|
|
92
|
+
options: ["bash", "zsh", "powershell", "fish"],
|
|
93
|
+
required: true,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
static description = "Show shell completion output or installation instructions for supported shells";
|
|
97
|
+
static summary = "Show shell completion output or installation instructions";
|
|
98
|
+
async run() {
|
|
99
|
+
const { args } = await this.parse(CompletionCommand);
|
|
100
|
+
if (args.shell === "fish") {
|
|
101
|
+
this.log(renderFishCompletion(this.config.bin));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await this.config.runCommand("autocomplete", [args.shell]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function commandId(operation) {
|
|
108
|
+
return `${operation.tagCommand}:${operation.command}`;
|
|
109
|
+
}
|
|
110
|
+
const generatedCommands = Object.fromEntries(operationManifest.map((operation) => [
|
|
111
|
+
commandId(operation),
|
|
112
|
+
createOperationCommand(operation),
|
|
113
|
+
]));
|
|
114
|
+
export const COMMANDS = {
|
|
115
|
+
completion: CompletionCommand,
|
|
116
|
+
"list-operations": ListOperationsCommand,
|
|
117
|
+
// `describe` prints a single operation's full manifest entry
|
|
118
|
+
// (path, request schema, response schema, per-field descriptions).
|
|
119
|
+
// The same data is in `list-operations` but agents don't reach for
|
|
120
|
+
// `list-operations | jq` when they want to clarify a field meaning.
|
|
121
|
+
describe: DescribeCommand,
|
|
122
|
+
// `send` is the agent-grade shortcut for sending:send-email with
|
|
123
|
+
// sensible defaults (auto from-address, auto subject). The full
|
|
124
|
+
// operation stays available under sending:send-email for callers
|
|
125
|
+
// who want every flag.
|
|
126
|
+
send: SendCommand,
|
|
127
|
+
// `login` creates and stores an org-scoped CLI API key via browser approval.
|
|
128
|
+
login: LoginCommand,
|
|
129
|
+
// `logout` revokes the saved CLI API key and removes local credentials.
|
|
130
|
+
logout: LogoutCommand,
|
|
131
|
+
// `whoami` is the credentials smoke test. Prints the account the
|
|
132
|
+
// current API key authenticates as. AGX walkthroughs kept
|
|
133
|
+
// wanting this before risking a real call against a possibly-
|
|
134
|
+
// bad key.
|
|
135
|
+
whoami: WhoamiCommand,
|
|
136
|
+
// `emails:latest` is the inbox-triage shortcut: the most recent N
|
|
137
|
+
// inbound emails as a compact text table. emails:list-emails stays
|
|
138
|
+
// available for the full JSON envelope + cursor pagination.
|
|
139
|
+
"emails:latest": EmailsLatestCommand,
|
|
140
|
+
// `emails:watch` and `emails:wait` poll the search API for new matching
|
|
141
|
+
// inbound mail. `watch` defaults to a human table; `wait` defaults to JSONL.
|
|
142
|
+
"emails:watch": EmailsWatchCommand,
|
|
143
|
+
"emails:wait": EmailsWaitCommand,
|
|
144
|
+
// `functions:init` scaffolds a deployable Function project so a
|
|
145
|
+
// new author can go zero-to-deployed without writing the handler,
|
|
146
|
+
// package.json, build script, and tsconfig from scratch. The
|
|
147
|
+
// scaffolded handler imports from @primitivedotdev/sdk/api (the
|
|
148
|
+
// runtime-client subpath) and demonstrates client.send() so the
|
|
149
|
+
// first thing the author sees is the SDK pattern, not raw fetch.
|
|
150
|
+
"functions:init": FunctionsInitCommand,
|
|
151
|
+
// `functions:deploy` and `functions:redeploy` are file-input
|
|
152
|
+
// shortcuts for create-function / update-function. The underlying
|
|
153
|
+
// ops take `code` as a body string, which is awkward at the CLI
|
|
154
|
+
// for multi-line bundles; these read the bundle off disk and pass
|
|
155
|
+
// it through. The auto-generated functions:* operations stay
|
|
156
|
+
// available for callers that want the full surface.
|
|
157
|
+
"functions:deploy": FunctionsDeployCommand,
|
|
158
|
+
"functions:redeploy": FunctionsRedeployCommand,
|
|
159
|
+
// `functions:set-secret` is the one-call shortcut for "write a
|
|
160
|
+
// secret AND (optionally) push it live." The raw
|
|
161
|
+
// functions:set-function-secret / functions:create-function-secret
|
|
162
|
+
// operations only do the secret upsert; making the new value
|
|
163
|
+
// visible to the running handler requires a separate redeploy,
|
|
164
|
+
// which this shortcut folds in via --redeploy.
|
|
165
|
+
"functions:set-secret": FunctionsSetSecretCommand,
|
|
166
|
+
...generatedCommands,
|
|
167
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Deploy-time lint for `functions:deploy --file <bundle>` and
|
|
2
|
+
// `functions:redeploy --file <bundle>`. Looks for a raw
|
|
3
|
+
// `fetch("...primitive.dev/.../send-mail", ...)` call in the bundle
|
|
4
|
+
// text and, on a match, emits a stderr warning telling the author
|
|
5
|
+
// to prefer `createPrimitiveClient` from `@primitivedotdev/sdk/api`.
|
|
6
|
+
//
|
|
7
|
+
// Why: a recurring pattern in agent-assisted Function deploys is to
|
|
8
|
+
// copy the REST snippet from the docs and call `fetch` directly
|
|
9
|
+
// against the Primitive send-mail endpoint, even after the docs
|
|
10
|
+
// switched to leading with the SDK and `functions:init` ships a
|
|
11
|
+
// scaffold that uses `createPrimitiveClient`. The SDK already
|
|
12
|
+
// handles dual-host routing, error envelopes, and send-permission
|
|
13
|
+
// gate denials; raw `fetch` re-discovers each of those by hand. The
|
|
14
|
+
// warning is the catch-net at deploy time: the deploy still
|
|
15
|
+
// proceeds.
|
|
16
|
+
//
|
|
17
|
+
// Scope by design:
|
|
18
|
+
// - We only flag the empirically observed footgun: `/send-mail`.
|
|
19
|
+
// Not arbitrary calls to api.primitive.dev. Other endpoints have
|
|
20
|
+
// not surfaced the same pattern.
|
|
21
|
+
// - We require the URL string literal to immediately follow the
|
|
22
|
+
// `fetch(` token (allowing whitespace) so we don't trip on a
|
|
23
|
+
// comment that merely mentions the URL. esbuild strips most
|
|
24
|
+
// comments anyway, but anchoring on `fetch(` keeps the rule
|
|
25
|
+
// honest without trying to parse JS.
|
|
26
|
+
// - We only look at the bundle text passed in. Source maps and
|
|
27
|
+
// sibling files are not scanned.
|
|
28
|
+
// - Variable-URL cases (`fetch(url, ...)` where `url` was assembled
|
|
29
|
+
// elsewhere) are accepted false negatives. The value here is
|
|
30
|
+
// catching the obvious inline-literal case, not full taint
|
|
31
|
+
// analysis.
|
|
32
|
+
// Match `fetch(` then a string literal (single, double, or backtick)
|
|
33
|
+
// whose contents include `primitive.dev` and end with `/send-mail`
|
|
34
|
+
// (optionally with a query string or trailing path boundary). Examples
|
|
35
|
+
// matched:
|
|
36
|
+
// fetch("https://api.primitive.dev/v1/send-mail", {...})
|
|
37
|
+
// fetch(`https://www.primitive.dev/api/v1/send-mail`, {...})
|
|
38
|
+
// fetch('https://primitive.dev/api/v1/send-mail?wait=1')
|
|
39
|
+
//
|
|
40
|
+
// The `[^`'"]*` inside the literal forbids the closing quote
|
|
41
|
+
// character itself so we can't accidentally span across two adjacent
|
|
42
|
+
// string literals. The trailing `(?![A-Za-z0-9_-])` forbids any
|
|
43
|
+
// letter, digit, underscore, or hyphen immediately after `send-mail`
|
|
44
|
+
// so `/send-mail-template-preview` does not trip the rule. (Plain
|
|
45
|
+
// `\b` does not help here because `-` is itself a non-word character,
|
|
46
|
+
// so `mail\b` still matches `mail-...`.)
|
|
47
|
+
const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
|
|
48
|
+
// How much surrounding text to include on either side of the match
|
|
49
|
+
// when building the sample snippet. Kept short on purpose: bundles
|
|
50
|
+
// are minified and a 120-char window is enough to spot the call
|
|
51
|
+
// without flooding stderr.
|
|
52
|
+
const SNIPPET_PADDING = 60;
|
|
53
|
+
export function detectRawSendMailFetch(bundleText) {
|
|
54
|
+
// Reset lastIndex defensively: this regex is module-scoped with
|
|
55
|
+
// the /g flag, so a prior call's state would skip the next match.
|
|
56
|
+
RAW_SEND_MAIL_FETCH_REGEX.lastIndex = 0;
|
|
57
|
+
const match = RAW_SEND_MAIL_FETCH_REGEX.exec(bundleText);
|
|
58
|
+
if (!match) {
|
|
59
|
+
return { found: false, sampleSnippet: null };
|
|
60
|
+
}
|
|
61
|
+
const start = Math.max(0, match.index - SNIPPET_PADDING);
|
|
62
|
+
const end = Math.min(bundleText.length, match.index + match[0].length + SNIPPET_PADDING);
|
|
63
|
+
const raw = bundleText.slice(start, end);
|
|
64
|
+
// Collapse newlines and runs of whitespace so the snippet is one
|
|
65
|
+
// readable line on stderr regardless of how the bundle was
|
|
66
|
+
// formatted.
|
|
67
|
+
const sampleSnippet = raw.replace(/\s+/g, " ").trim();
|
|
68
|
+
return { found: true, sampleSnippet };
|
|
69
|
+
}
|
|
70
|
+
// The stderr warning copy. Three beats: name the issue, name the
|
|
71
|
+
// SDK alternative, link the docs. Plus a one-line "deploy proceeds"
|
|
72
|
+
// reassurance. Kept punctuation simple (commas, periods, line
|
|
73
|
+
// breaks) so it doesn't trip the no-em-dashes hook and so the lines
|
|
74
|
+
// wrap predictably in a terminal.
|
|
75
|
+
export const RAW_SEND_MAIL_FETCH_WARNING_LINES = [
|
|
76
|
+
"warning: this bundle calls fetch(...) against /send-mail directly.",
|
|
77
|
+
"The Primitive SDK exposes createPrimitiveClient from",
|
|
78
|
+
"@primitivedotdev/sdk/api which handles host routing, error envelopes,",
|
|
79
|
+
"and gate denials for you. See https://www.primitive.dev/docs/functions",
|
|
80
|
+
"for the recommended in-handler pattern. Continuing with deploy.",
|
|
81
|
+
];
|
|
82
|
+
export function formatRawSendMailFetchWarning(finding) {
|
|
83
|
+
const lines = [...RAW_SEND_MAIL_FETCH_WARNING_LINES];
|
|
84
|
+
if (finding.sampleSnippet) {
|
|
85
|
+
lines.push(` found: ${finding.sampleSnippet}`);
|
|
86
|
+
}
|
|
87
|
+
return `${lines.join("\n")}\n`;
|
|
88
|
+
}
|
|
89
|
+
// Convenience: run the detector and, on a match, write the warning
|
|
90
|
+
// to a stderr-shaped writer. Pulled out so both deploy and redeploy
|
|
91
|
+
// share one code path and so the unit tests can pass a fake writer.
|
|
92
|
+
export function emitRawSendMailFetchWarning(bundleText, write) {
|
|
93
|
+
const finding = detectRawSendMailFetch(bundleText);
|
|
94
|
+
if (finding.found) {
|
|
95
|
+
write(formatRawSendMailFetchWarning(finding));
|
|
96
|
+
}
|
|
97
|
+
return finding;
|
|
98
|
+
}
|