@primitivedotdev/cli 1.1.0 → 1.2.1

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 CHANGED
@@ -16,6 +16,8 @@ primitive whoami
16
16
  prim whoami
17
17
  ```
18
18
 
19
+ The same CLI is also published unscoped as [`primcli`](https://www.npmjs.com/package/primcli) — `npm install -g primcli` installs an identical build with the same `primitive`/`prim` commands. Use whichever name you prefer; they track the same version.
20
+
19
21
  Or with no install:
20
22
 
21
23
  ```bash
@@ -2,7 +2,7 @@ import { A as PrimitiveApiClient, C as saveCliCredentials, D as loadActiveChatSt
2
2
  import { Args, Command, Errors, Flags, ux } from "@oclif/core";
3
3
  import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
5
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
+ import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { hostname } from "node:os";
7
7
  import process$1 from "node:process";
8
8
  import { createInterface } from "node:readline/promises";
@@ -17171,6 +17171,108 @@ async function runSourceDeploy(api, params) {
17171
17171
  result: data
17172
17172
  };
17173
17173
  }
17174
+ async function runSourceDeployWithSecrets(api, params) {
17175
+ const listed = await api.listFunctions();
17176
+ if (listed.error) return {
17177
+ kind: "error",
17178
+ payload: extractErrorPayload(listed.error),
17179
+ stage: "lookup"
17180
+ };
17181
+ const foundId = (listed.data?.data ?? []).find((f) => f.name === params.name)?.id ?? null;
17182
+ let functionId;
17183
+ let createPayload;
17184
+ if (foundId === null) {
17185
+ const created = await api.createFunction({
17186
+ files: params.files,
17187
+ name: params.name
17188
+ });
17189
+ if (created.error) return {
17190
+ kind: "error",
17191
+ payload: extractErrorPayload(created.error),
17192
+ stage: "create"
17193
+ };
17194
+ const data = created.data?.data;
17195
+ if (!data) return {
17196
+ kind: "error",
17197
+ payload: {
17198
+ code: "client_error",
17199
+ message: "Create returned no data"
17200
+ },
17201
+ stage: "create"
17202
+ };
17203
+ functionId = data.id;
17204
+ createPayload = data;
17205
+ } else functionId = foundId;
17206
+ const writtenSecrets = [];
17207
+ const succeededKeys = [];
17208
+ for (let i = 0; i < params.secrets.length; i++) {
17209
+ const pair = params.secrets[i];
17210
+ const pendingKeys = params.secrets.slice(i + 1).map((p) => p.key);
17211
+ const setResult = await api.setSecret({
17212
+ id: functionId,
17213
+ key: pair.key,
17214
+ value: pair.value
17215
+ });
17216
+ if (setResult.error) return {
17217
+ ...createPayload ? { created: createPayload } : {},
17218
+ failedKey: pair.key,
17219
+ functionId,
17220
+ kind: "error",
17221
+ payload: extractErrorPayload(setResult.error),
17222
+ pendingKeys,
17223
+ stage: "set-secret",
17224
+ succeededKeys
17225
+ };
17226
+ const secret = setResult.data?.data;
17227
+ if (!secret) return {
17228
+ ...createPayload ? { created: createPayload } : {},
17229
+ failedKey: pair.key,
17230
+ functionId,
17231
+ kind: "error",
17232
+ payload: {
17233
+ code: "client_error",
17234
+ message: "Secret write returned no data"
17235
+ },
17236
+ pendingKeys,
17237
+ stage: "set-secret",
17238
+ succeededKeys
17239
+ };
17240
+ writtenSecrets.push(secret);
17241
+ succeededKeys.push(pair.key);
17242
+ }
17243
+ const updated = await api.updateFunction({
17244
+ files: params.files,
17245
+ id: functionId
17246
+ });
17247
+ if (updated.error) return {
17248
+ ...createPayload ? { created: createPayload } : {},
17249
+ functionId,
17250
+ kind: "error",
17251
+ payload: extractErrorPayload(updated.error),
17252
+ stage: "secret-redeploy",
17253
+ succeededKeys
17254
+ };
17255
+ const redeployed = updated.data?.data;
17256
+ if (!redeployed) return {
17257
+ ...createPayload ? { created: createPayload } : {},
17258
+ functionId,
17259
+ kind: "error",
17260
+ payload: {
17261
+ code: "client_error",
17262
+ message: "Redeploy returned no data"
17263
+ },
17264
+ stage: "secret-redeploy",
17265
+ succeededKeys
17266
+ };
17267
+ return {
17268
+ kind: "ok",
17269
+ result: {
17270
+ action: foundId === null ? "created" : "redeployed",
17271
+ redeploy: redeployed,
17272
+ secrets: writtenSecrets
17273
+ }
17274
+ };
17275
+ }
17174
17276
  function renderBuildFailure(payload, write) {
17175
17277
  if (typeof payload !== "object" || payload === null) return false;
17176
17278
  const error = payload.error ?? payload;
@@ -17657,14 +17759,23 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
17657
17759
 
17658
17760
  Pass secret source flags to seed bindings in the same command. Keys
17659
17761
  must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
17660
- underscores; first character is a letter or underscore). With one
17661
- or more secrets the deploy fans out to multiple API calls:
17662
- create-function, set-secret per pair, then a final update-function
17663
- with the same bundle so the running handler picks up the bindings.
17664
- If a secret write fails after the create step the function exists
17665
- with whatever secrets succeeded and the redeploy has NOT fired;
17666
- re-run \`primitive functions set-secret\` for the missing keys, then
17667
- \`primitive functions redeploy\` to push them live. ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
17762
+ underscores; first character is a letter or underscore).
17763
+
17764
+ With one or more secrets the deploy fans out to multiple API calls.
17765
+ For --file (and for --source when no function with the given name
17766
+ exists yet): create-function, set-secret per pair, then a final
17767
+ update-function so the running handler picks up the bindings. For
17768
+ --source against an existing function name: the create-function step
17769
+ is replaced by an id lookup, then set-secret per pair, then a single
17770
+ update-function that binds the new code and the new secret env in
17771
+ one step (avoiding an intermediate redeploy that would briefly run
17772
+ the new code with the previous secret bindings).
17773
+
17774
+ If a secret write fails before the final redeploy, the function row
17775
+ carries whatever bindings landed but the running handler has NOT yet
17776
+ picked them up. Re-run \`primitive functions set-secret\` for the
17777
+ missing keys, then re-run \`primitive functions deploy\` (or
17778
+ \`functions redeploy\`) to push them live. ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
17668
17779
  static summary = "Deploy a new function from a bundled handler file";
17669
17780
  static examples = [
17670
17781
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
@@ -17674,6 +17785,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
17674
17785
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
17675
17786
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
17676
17787
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-env-file .env.local:OWNER_EMAIL",
17788
+ "<%= config.bin %> functions deploy --name triage --source . --secret-from-env ANTHROPIC_API_KEY",
17677
17789
  "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-stdin OPENAI_KEY"
17678
17790
  ];
17679
17791
  static flags = {
@@ -17863,8 +17975,15 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
17863
17975
  });
17864
17976
  }
17865
17977
  async runSourceMode(flags, sourceDir) {
17866
- 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) {
17867
- 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");
17978
+ const parsedSecrets = resolveSecretFlags({
17979
+ fromEnv: flags["secret-from-env"] ?? [],
17980
+ fromEnvFile: flags["secret-from-env-file"] ?? [],
17981
+ fromFile: flags["secret-from-file"] ?? [],
17982
+ fromStdin: flags["secret-from-stdin"],
17983
+ inline: flags.secret ?? []
17984
+ });
17985
+ if (parsedSecrets.kind === "error") {
17986
+ process.stderr.write(`${parsedSecrets.message}\n`);
17868
17987
  process.exitCode = 1;
17869
17988
  return;
17870
17989
  }
@@ -17884,7 +18003,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
17884
18003
  baseUrlOverridden,
17885
18004
  configDir: this.config.configDir
17886
18005
  };
17887
- const outcome = await runSourceDeploy({
18006
+ const apiSurface = {
17888
18007
  createFunction: (p) => createFunction({
17889
18008
  body: {
17890
18009
  files: p.files,
@@ -17897,27 +18016,80 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
17897
18016
  client: apiClient.client,
17898
18017
  responseStyle: "fields"
17899
18018
  }),
18019
+ setSecret: (p) => setFunctionSecret({
18020
+ body: { value: p.value },
18021
+ client: apiClient.client,
18022
+ path: {
18023
+ id: p.id,
18024
+ key: p.key
18025
+ },
18026
+ responseStyle: "fields"
18027
+ }),
17900
18028
  updateFunction: (p) => updateFunction({
17901
18029
  body: { files: p.files },
17902
18030
  client: apiClient.client,
17903
18031
  path: { id: p.id },
17904
18032
  responseStyle: "fields"
17905
18033
  })
17906
- }, {
18034
+ };
18035
+ if (parsedSecrets.secrets.length === 0) {
18036
+ const outcome = await runSourceDeploy(apiSurface, {
18037
+ files: collected.files,
18038
+ name: flags.name
18039
+ });
18040
+ if (outcome.kind === "error") {
18041
+ renderBuildFailure(outcome.payload, (chunk) => process.stderr.write(chunk));
18042
+ writeErrorWithHints(outcome.payload);
18043
+ surfaceUnauthorizedHint({
18044
+ ...authFailureContext,
18045
+ payload: outcome.payload
18046
+ });
18047
+ process.exitCode = 1;
18048
+ return;
18049
+ }
18050
+ await this.finishSourceDeploy({
18051
+ apiClient,
18052
+ authFailureContext,
18053
+ flags,
18054
+ payload: outcome.result
18055
+ });
18056
+ return;
18057
+ }
18058
+ const secretsOutcome = await runSourceDeployWithSecrets(apiSurface, {
17907
18059
  files: collected.files,
17908
- name: flags.name
18060
+ name: flags.name,
18061
+ secrets: parsedSecrets.secrets
17909
18062
  });
17910
- if (outcome.kind === "error") {
17911
- renderBuildFailure(outcome.payload, (chunk) => process.stderr.write(chunk));
17912
- writeErrorWithHints(outcome.payload);
18063
+ if (secretsOutcome.kind === "error") {
18064
+ if (secretsOutcome.stage === "set-secret") {
18065
+ const succeeded = secretsOutcome.succeededKeys.length > 0 ? secretsOutcome.succeededKeys.join(", ") : "(none)";
18066
+ const pending = secretsOutcome.pendingKeys.length > 0 ? secretsOutcome.pendingKeys.join(", ") : "(none)";
18067
+ const allMissing = [secretsOutcome.failedKey, ...secretsOutcome.pendingKeys].join(", ");
18068
+ const createdClause = secretsOutcome.created ? `Function ${secretsOutcome.created.name} (${secretsOutcome.functionId}) was created` : `Function ${flags.name} (${secretsOutcome.functionId}) already existed`;
18069
+ const stagingWarning = secretsOutcome.succeededKeys.length > 0 ? ` Note: [${succeeded}] are now staged on the function row and will bind on the next deploy of this function (including one that does not pass --secret).` : "";
18070
+ process.stderr.write(`${createdClause}, but writing secret ${secretsOutcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions set-secret\` for each of [${allMissing}], then \`primitive functions deploy --source ${sourceDir} --name ${flags.name}\` to push them live.${stagingWarning}\n`);
18071
+ } else if (secretsOutcome.stage === "secret-redeploy") {
18072
+ const succeeded = secretsOutcome.succeededKeys.length > 0 ? secretsOutcome.succeededKeys.join(", ") : "(none)";
18073
+ const createdClause = secretsOutcome.created ? `Function ${secretsOutcome.created.name} (${secretsOutcome.functionId}) was created and` : `Function ${flags.name} (${secretsOutcome.functionId}) already existed and`;
18074
+ process.stderr.write(`${createdClause} secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions deploy --source ${sourceDir} --name ${flags.name}\` once the cause is fixed.\n`);
18075
+ } else renderBuildFailure(secretsOutcome.payload, (chunk) => process.stderr.write(chunk));
18076
+ writeErrorWithHints(secretsOutcome.payload);
17913
18077
  surfaceUnauthorizedHint({
17914
18078
  ...authFailureContext,
17915
- payload: outcome.payload
18079
+ payload: secretsOutcome.payload
17916
18080
  });
17917
18081
  process.exitCode = 1;
17918
18082
  return;
17919
18083
  }
17920
- const payload = outcome.result;
18084
+ await this.finishSourceDeploy({
18085
+ apiClient,
18086
+ authFailureContext,
18087
+ flags,
18088
+ payload: secretsOutcome.result.redeploy
18089
+ });
18090
+ }
18091
+ async finishSourceDeploy(args) {
18092
+ const { apiClient, authFailureContext, flags, payload } = args;
17921
18093
  if (flags.wait) {
17922
18094
  const waitResult = await waitForFunctionDeploy({
17923
18095
  getFunction: (p) => getFunction({
@@ -17966,8 +18138,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
17966
18138
  name: "Primitive Team",
17967
18139
  url: "https://primitive.dev"
17968
18140
  };
17969
- const SDK_VERSION_RANGE = "^1.1.0";
17970
- const CLI_VERSION_RANGE = "^1.1.0";
18141
+ const SDK_VERSION_RANGE = "^1.2.0";
18142
+ const CLI_VERSION_RANGE = "^1.2.0";
17971
18143
  const ESBUILD_VERSION_RANGE = "^0.27.0";
17972
18144
  function renderHandler() {
17973
18145
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -20770,6 +20942,74 @@ var SignupCommand = class SignupCommand extends Command {
20770
20942
  }
20771
20943
  }
20772
20944
  };
20945
+ function resolveVerificationCode(input) {
20946
+ const sources = [
20947
+ input.positional !== void 0 ? "positional" : null,
20948
+ input.fromStdin === true ? "--code-from-stdin" : null,
20949
+ input.fromFile !== void 0 ? "--code-from-file" : null,
20950
+ input.fromEnv !== void 0 ? "--code-from-env" : null
20951
+ ].filter((v) => v !== null);
20952
+ if (sources.length === 0) return {
20953
+ kind: "error",
20954
+ message: "Pass the verification code as a positional argument or via one of --code-from-stdin, --code-from-file, or --code-from-env."
20955
+ };
20956
+ if (sources.length > 1) return {
20957
+ kind: "error",
20958
+ message: `Pass exactly one source for the verification code; got ${sources.join(", ")}.`
20959
+ };
20960
+ if (input.positional !== void 0) return {
20961
+ kind: "ok",
20962
+ code: input.positional
20963
+ };
20964
+ if (input.fromEnv !== void 0) {
20965
+ const value = (input.env ?? process$1.env)[input.fromEnv];
20966
+ if (value === void 0) return {
20967
+ kind: "error",
20968
+ message: `--code-from-env ${input.fromEnv}: environment variable is not set.`
20969
+ };
20970
+ return {
20971
+ kind: "ok",
20972
+ code: stripTrailingNewline(value)
20973
+ };
20974
+ }
20975
+ if (input.fromFile !== void 0) {
20976
+ const readFile = input.readFile ?? defaultReadCodeFile;
20977
+ try {
20978
+ return {
20979
+ kind: "ok",
20980
+ code: stripTrailingNewline(readFile(input.fromFile))
20981
+ };
20982
+ } catch (error) {
20983
+ const detail = error instanceof Error ? error.message : String(error);
20984
+ return {
20985
+ kind: "error",
20986
+ message: `--code-from-file ${input.fromFile}: could not read file: ${detail}`
20987
+ };
20988
+ }
20989
+ }
20990
+ const readStdin = input.readStdin ?? defaultReadCodeStdin;
20991
+ try {
20992
+ return {
20993
+ kind: "ok",
20994
+ code: stripTrailingNewline(readStdin())
20995
+ };
20996
+ } catch (error) {
20997
+ return {
20998
+ kind: "error",
20999
+ message: `--code-from-stdin: ${error instanceof Error ? error.message : String(error)}`
21000
+ };
21001
+ }
21002
+ }
21003
+ function stripTrailingNewline(value) {
21004
+ return value.replace(/\r?\n$/, "");
21005
+ }
21006
+ function defaultReadCodeFile(path) {
21007
+ return readFileSync(path, "utf8");
21008
+ }
21009
+ function defaultReadCodeStdin() {
21010
+ if (process$1.stdin.isTTY) throw new Error("stdin is a TTY; pipe the code into this command or use --code-from-file / --code-from-env instead.");
21011
+ return readFileSync(0, "utf8");
21012
+ }
20773
21013
  var SignupConfirmCommand = class SignupConfirmCommand extends Command {
20774
21014
  static args = {
20775
21015
  email: Args.string({
@@ -20777,19 +21017,28 @@ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
20777
21017
  required: true
20778
21018
  }),
20779
21019
  code: Args.string({
20780
- description: "Verification code from the signup email",
20781
- required: true
21020
+ description: "Verification code from the signup email. Optional when one of --code-from-stdin / --code-from-file / --code-from-env is passed; exactly one source must be set.",
21021
+ required: false
20782
21022
  })
20783
21023
  };
20784
21024
  static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
20785
21025
  static summary = "Confirm account signup";
20786
- static examples = ["<%= config.bin %> signup confirm user@example.com 123456", "<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
21026
+ static examples = [
21027
+ "<%= config.bin %> signup confirm user@example.com 123456",
21028
+ "<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000",
21029
+ "read -rs CODE && CODE=\"$CODE\" <%= config.bin %> signup confirm user@example.com --code-from-env CODE && unset CODE",
21030
+ "read -rs CODE && printf '%s' \"$CODE\" | <%= config.bin %> signup confirm user@example.com --code-from-stdin && unset CODE",
21031
+ "<%= config.bin %> signup confirm user@example.com --code-from-file /run/user/$(id -u)/verification-code"
21032
+ ];
20787
21033
  static flags = {
20788
21034
  "api-base-url": Flags.string({
20789
21035
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
20790
21036
  env: "PRIMITIVE_API_BASE_URL",
20791
21037
  hidden: true
20792
21038
  }),
21039
+ "code-from-stdin": Flags.boolean({ description: "Read the verification code from stdin instead of the positional argument. Use when an agent is constructing the command for the user to run, so the code never enters the agent's prompt context." }),
21040
+ "code-from-file": Flags.string({ description: "Read the verification code from a UTF-8 file at this path. Trailing newlines are stripped." }),
21041
+ "code-from-env": Flags.string({ description: "Read the verification code from this environment variable. Pair with `read -rs CODE && CODE=\"$CODE\" primitive signup confirm <email> --code-from-env CODE && unset CODE` so the value never appears on the command line or in shell history. Plain `read` creates a shell-local variable that child processes cannot see; the inline `CODE=\"$CODE\"` exports it for just the one command." }),
20793
21042
  force: Flags.boolean({
20794
21043
  char: "f",
20795
21044
  description: "Replace saved credentials after verification"
@@ -20798,6 +21047,13 @@ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
20798
21047
  };
20799
21048
  async run() {
20800
21049
  const { args, flags } = await this.parse(SignupConfirmCommand);
21050
+ const resolvedCode = resolveVerificationCode({
21051
+ positional: args.code,
21052
+ fromStdin: flags["code-from-stdin"] === true,
21053
+ fromFile: flags["code-from-file"],
21054
+ fromEnv: flags["code-from-env"]
21055
+ });
21056
+ if (resolvedCode.kind === "error") throw cliError$2(resolvedCode.message);
20801
21057
  let releaseCredentialsLock;
20802
21058
  try {
20803
21059
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -20806,7 +21062,7 @@ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
20806
21062
  }
20807
21063
  try {
20808
21064
  await runSignupConfirmWithCredentialLock({
20809
- code: args.code,
21065
+ code: resolvedCode.code,
20810
21066
  configDir: this.config.configDir,
20811
21067
  email: args.email,
20812
21068
  flags
@@ -22211,6 +22467,43 @@ function renderFishCompletion(binName) {
22211
22467
  return `${lines.join("\n")}\n`;
22212
22468
  }
22213
22469
  //#endregion
22470
+ //#region src/oclif/shell-completion-script.ts
22471
+ /**
22472
+ * Path to the sourceable completion *function* file that
22473
+ * `@oclif/plugin-autocomplete` writes under the CLI cache dir when its cache is
22474
+ * built. This is the artifact a shell is meant to source -- a package manager
22475
+ * dropping a file into `bash_completion.d/` or zsh's `site-functions/` wants
22476
+ * this, NOT the human-readable setup instructions printed by
22477
+ * `<bin> autocomplete <shell>`. The layout mirrors the plugin's own
22478
+ * `Create.bashCompletionFunctionPath` / `zshCompletionFunctionPath` getters:
22479
+ * <cacheDir>/autocomplete/functions/bash/<bin>.bash
22480
+ * <cacheDir>/autocomplete/functions/zsh/_<bin>
22481
+ *
22482
+ * This couples to a private path layout in `@oclif/plugin-autocomplete`
22483
+ * (pinned `^3.2.45` in package.json). If a major bump reorganises that cache
22484
+ * dir, `readCompletionFunction` will fail even after a successful
22485
+ * `--refresh-cache`; re-verify this layout when bumping the plugin.
22486
+ */
22487
+ function completionFunctionPath(cacheDir, bin, shell) {
22488
+ const functionsDir = path.join(cacheDir, "autocomplete", "functions", shell);
22489
+ const fileName = shell === "bash" ? `${bin}.bash` : `_${bin}`;
22490
+ return path.join(functionsDir, fileName);
22491
+ }
22492
+ /**
22493
+ * Read the generated completion function script, trimmed of trailing
22494
+ * whitespace so the caller can re-add a single newline. Throws an actionable
22495
+ * error (rather than a bare `ENOENT`) if the cached script is missing -- e.g.
22496
+ * the cache build failed, or the plugin changed its path layout.
22497
+ */
22498
+ function readCompletionFunction(cacheDir, bin, shell) {
22499
+ const filePath = completionFunctionPath(cacheDir, bin, shell);
22500
+ try {
22501
+ return readFileSync(filePath, "utf8").trimEnd();
22502
+ } catch (cause) {
22503
+ throw new Error(`Could not read the generated ${shell} completion script at ${filePath}. Run \`${bin} autocomplete ${shell} --refresh-cache\` and try again.`, { cause });
22504
+ }
22505
+ }
22506
+ //#endregion
22214
22507
  //#region src/oclif/index.ts
22215
22508
  var ListOperationsCommand = class extends Command {
22216
22509
  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-or-operation-name>`.";
@@ -22331,14 +22624,32 @@ var CompletionCommand = class CompletionCommand extends Command {
22331
22624
  ],
22332
22625
  required: true
22333
22626
  }) };
22334
- static description = "Show shell completion output or installation instructions for supported shells";
22335
- static summary = "Show shell completion output or installation instructions";
22627
+ static description = `Output a sourceable shell completion script, or print setup instructions.
22628
+
22629
+ For fish, and for bash/zsh when the output is piped or redirected (e.g. into a
22630
+ completion file under bash_completion.d or zsh's site-functions), this emits
22631
+ the raw completion script. For bash/zsh in an interactive terminal it prints
22632
+ the human-readable setup instructions instead. This keeps a redirected
22633
+ \`<%= config.bin %> completion bash > <file>\` safe to source -- the file holds an
22634
+ actual completion function, never instructional prose a shell would choke on.`;
22635
+ static summary = "Output a shell completion script or print setup instructions";
22636
+ static examples = [
22637
+ "<%= config.bin %> completion bash >> /etc/bash_completion.d/primitive",
22638
+ "<%= config.bin %> completion zsh > /usr/local/share/zsh/site-functions/_primitive",
22639
+ "<%= config.bin %> completion fish > ~/.config/fish/completions/primitive.fish"
22640
+ ];
22336
22641
  async run() {
22337
22642
  const { args } = await this.parse(CompletionCommand);
22338
- if (args.shell === "fish") {
22643
+ const shell = args.shell;
22644
+ if (shell === "fish") {
22339
22645
  this.log(renderFishCompletion(this.config.bin));
22340
22646
  return;
22341
22647
  }
22648
+ if ((shell === "bash" || shell === "zsh") && !process.stdout.isTTY) {
22649
+ await this.config.runCommand("autocomplete", [shell, "--refresh-cache"]);
22650
+ this.log(readCompletionFunction(this.config.cacheDir, this.config.bin, shell));
22651
+ return;
22652
+ }
22342
22653
  await this.config.runCommand("autocomplete", [args.shell]);
22343
22654
  }
22344
22655
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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,