@primitivedotdev/cli 0.32.1 → 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.
Files changed (2) hide show
  1. package/dist/oclif/index.js +268 -19
  2. package/package.json +1 -1
@@ -1,7 +1,7 @@
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";
4
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
5
  import { hostname } from "node:os";
6
6
  import process$1 from "node:process";
7
7
  import { createInterface } from "node:readline/promises";
@@ -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",
@@ -16274,6 +16294,129 @@ async function waitForFunctionDeploy(params) {
16274
16294
  }
16275
16295
  }
16276
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
16277
16420
  //#region src/oclif/lint/raw-send-mail-fetch.ts
16278
16421
  const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
16279
16422
  const SNIPPET_PADDING = 60;
@@ -16740,6 +16883,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16740
16883
  static summary = "Deploy a new function from a bundled handler file";
16741
16884
  static examples = [
16742
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",
16743
16888
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --wait",
16744
16889
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
16745
16890
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
@@ -16765,10 +16910,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16765
16910
  description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
16766
16911
  required: true
16767
16912
  }),
16768
- file: Flags.string({
16769
- description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
16770
- required: true
16771
- }),
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." }),
16772
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." }),
16773
16916
  secret: Flags.string({
16774
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}`,
@@ -16810,6 +16953,17 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16810
16953
  process.exitCode = 1;
16811
16954
  return;
16812
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;
16813
16967
  const parsedSecrets = resolveSecretFlags({
16814
16968
  fromEnv: flags["secret-from-env"] ?? [],
16815
16969
  fromEnvFile: flags["secret-from-env-file"] ?? [],
@@ -16822,7 +16976,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16822
16976
  process.exitCode = 1;
16823
16977
  return;
16824
16978
  }
16825
- const code = readTextFileFlag(flags.file, "--file");
16979
+ const code = readTextFileFlag(file, "--file");
16826
16980
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
16827
16981
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
16828
16982
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
@@ -16928,6 +17082,101 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16928
17082
  this.log(JSON.stringify(payload, null, 2));
16929
17083
  });
16930
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
+ }
16931
17180
  };
16932
17181
  //#endregion
16933
17182
  //#region src/oclif/function-templates.ts
@@ -16937,8 +17186,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
16937
17186
  name: "Primitive Team",
16938
17187
  url: "https://primitive.dev"
16939
17188
  };
16940
- const SDK_VERSION_RANGE = "^0.32.0";
16941
- const CLI_VERSION_RANGE = "^0.32.1";
17189
+ const SDK_VERSION_RANGE = "^0.33.0";
17190
+ const CLI_VERSION_RANGE = "^0.33.0";
16942
17191
  const ESBUILD_VERSION_RANGE = "^0.27.0";
16943
17192
  function renderHandler() {
16944
17193
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,