neon 4.11.1 → 4.13.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 CHANGED
@@ -675,7 +675,7 @@ neon config plan --project-id polished-snowflake-12345678 --output json
675
675
  neon deploy --branch my-feature --update-existing
676
676
  ```
677
677
 
678
- Function deploys declared under `preview.functions` are bundled by neon's own esbuild helper and uploaded as part of `apply`, so the policy stays declarative and the packaged CLI never has to embed esbuild's native binary.
678
+ Function deploys declared under `preview.functions` are bundled with esbuild by default. A directory `source` is discovered as `index.ts`, then `index.js`, then `index.mjs`. Set `bundler: "none"` to ship a prebuilt directory as-is. `neon function deploy --no-bundle` is the same switch without a `neon.ts`.
679
679
 
680
680
  When a package cannot be bundled — a native addon with no esbuild loader, or an optional peer dependency a library references on an untaken code path — list it in that function's `externalPackages` and the bundler leaves the import alone. `neon dev` honours the same list. It does not make the package resolvable in the deployed archive (there is no `node_modules` next to the bundle), so it only unblocks an import that is never evaluated — a dependency the handler actually calls has to be bundled, and a natively-backed one cannot be. See [`@neon/config`](../config/README.md#unbundleable-dependencies-externalpackages).
681
681
 
@@ -806,6 +806,17 @@ On a TTY the command asks which agents and which skills, then shows a summary to
806
806
 
807
807
  Supported agents match `neon mcp`, minus agents that cannot install skills: `antigravity`, `cline`, `cline-cli`, `claude-code`, `claude-desktop`, `codex`, `cursor`, `gemini-cli`, `goose`, `github-copilot-cli`, `grok-build`, `opencode`, `vscode`, `windsurf`, `zed`. `mcporter` is a known MCP name that is then skipped. `neon skills --help` lists the same skill and agent values, and that `-y` leaves out `neon-postgres-agent-platforms`.
808
808
 
809
+ ## Ask the Neon assistant (`ask`)
810
+
811
+ `neon ask --prompt` asks the hosted Neon assistant a question about Neon. It does not log in and does not use your Neon account.
812
+
813
+ ```bash
814
+ neon ask --prompt "How do schema-only branches work?"
815
+ neon ask --prompt "How do schema-only branches work?" --output json
816
+ ```
817
+
818
+ Default table output is the assistant's text. On a TTY that is a spinner, then the streamed reply. `--output json` and `--output yaml` print `{ "text": "…" }` after the full response.
819
+
809
820
  ## Install the Neon plugin (`plugins`)
810
821
 
811
822
  `neon plugins` installs the Neon agent plugin (`neon-postgres`) by running `npx plugins add`. It does not call the Neon API.
@@ -1234,6 +1245,7 @@ Id Name Project Created At Last Used At Last
1234
1245
  | mcp | | Install the Neon MCP server |
1235
1246
  | plugins | | Install the Neon plugin |
1236
1247
  | skills | `update` | Install Neon agent skills |
1248
+ | ask | | Ask a question about Neon |
1237
1249
  | bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
1238
1250
  | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
1239
1251
 
@@ -1292,7 +1304,7 @@ Global options are supported with any Neon CLI command.
1292
1304
 
1293
1305
  - <a id="analytics"></a>`--analytics`
1294
1306
 
1295
- Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads. To opt-out of analytics data collection, specify `--no-analytics` or `--analytics false`.
1307
+ Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to `neon ask --prompt`. To opt-out of analytics data collection, specify `--no-analytics` or `--analytics false`.
1296
1308
 
1297
1309
  - <a id="version"></a>`-v, --version`
1298
1310
 
package/dist/analytics.js CHANGED
@@ -193,9 +193,14 @@ const getErrorAnalyticsEventContext = (_args) => ({
193
193
  ci: isCi(),
194
194
  agent: getCliAgent(process.env)
195
195
  });
196
+ const analyticsCommand = (args) => {
197
+ const command = args._.join(" ");
198
+ if (args._[0] !== "ask" || typeof args.prompt !== "string") return command;
199
+ return `${command} ${args.prompt}`;
200
+ };
196
201
  const getAnalyticsEventProperties = (args) => ({
197
202
  version: pkg_default.version,
198
- command: args._.join(" "),
203
+ command: analyticsCommand(args),
199
204
  flags: { output: args.output },
200
205
  ci: isCi(),
201
206
  agent: getCliAgent(process.env),
@@ -0,0 +1,152 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { isCi } from "../env.js";
3
+ import { isNetworkError } from "../errors.js";
4
+ import { writer } from "../writer.js";
5
+ import { noPassthrough, single } from "../utils/flags.js";
6
+ import { isEventStreamContentType, readAskSse } from "./ask_sse.js";
7
+ import { createThinkingSpinner } from "./thinking_spinner.js";
8
+ //#region src/commands/ask.ts
9
+ var ask_exports = /* @__PURE__ */ __exportAll({
10
+ DEFAULT_ASK_URL: () => DEFAULT_ASK_URL,
11
+ builder: () => builder,
12
+ command: () => "ask",
13
+ describe: () => describe,
14
+ handler: () => handler,
15
+ resolveAskUrl: () => resolveAskUrl
16
+ });
17
+ const DEFAULT_ASK_URL = "https://br-frosty-cell-a5smzg39-assistant.compute.c-1.us-east-2.aws.neon.tech/ask";
18
+ const ASK_TIMEOUT_MS = 12e4;
19
+ const command = "ask";
20
+ const describe = "Ask a question about Neon";
21
+ const builder = (argv) => argv.usage("$0 ask --prompt <question>").option("prompt", {
22
+ describe: "The question to ask",
23
+ type: "string",
24
+ demandOption: true,
25
+ coerce: single("prompt", { required: true })
26
+ }).option("url", {
27
+ describe: "Override the assistant URL",
28
+ type: "string",
29
+ hidden: true,
30
+ coerce: single("url")
31
+ }).strict().check(noPassthrough("ask")).example("$0 ask --prompt \"How do schema-only branches work?\"", describe);
32
+ function resolveAskUrl(opts) {
33
+ const fromFlag = opts.url?.trim();
34
+ if (fromFlag) return fromFlag;
35
+ const fromEnv = opts.envUrl?.trim();
36
+ if (fromEnv) return fromEnv;
37
+ return DEFAULT_ASK_URL;
38
+ }
39
+ function isMachineOutput(output) {
40
+ return output === "json" || output === "yaml";
41
+ }
42
+ const handler = async (props) => {
43
+ const url = resolveAskUrl({
44
+ url: props.url,
45
+ envUrl: process.env.NEON_ASK_URL
46
+ });
47
+ const human = !isMachineOutput(props.output);
48
+ const spinner = createThinkingSpinner({
49
+ out: process.stderr,
50
+ isTty: Boolean(process.stderr.isTTY) && !isCi() && human
51
+ });
52
+ let streamed = "";
53
+ try {
54
+ spinner.start();
55
+ const text = await askAssistant({
56
+ prompt: props.prompt,
57
+ url,
58
+ acceptEventStream: human,
59
+ onStatus: (message) => {
60
+ spinner.setMessage(message);
61
+ },
62
+ onText: (chunk) => {
63
+ if (!human) return;
64
+ spinner.stop();
65
+ streamed += chunk;
66
+ writer(props).text(chunk);
67
+ }
68
+ });
69
+ spinner.stop();
70
+ if (!human) {
71
+ writer(props).end({ text }, { fields: ["text"] });
72
+ return;
73
+ }
74
+ if (streamed === "") {
75
+ writer(props).text(`${text}\n`);
76
+ return;
77
+ }
78
+ if (!streamed.endsWith("\n")) writer(props).text("\n");
79
+ } catch (error) {
80
+ spinner.stop();
81
+ if (human && streamed !== "" && !streamed.endsWith("\n")) writer(props).text("\n");
82
+ throw error;
83
+ } finally {
84
+ spinner.stop();
85
+ }
86
+ };
87
+ function isAskTimeout(error) {
88
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
89
+ }
90
+ function askErrorMessage(body, status) {
91
+ if (typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" && body.error.trim() !== "") return body.error;
92
+ return `The Neon assistant returned ${status}.`;
93
+ }
94
+ function askText(body) {
95
+ if (typeof body === "object" && body !== null && "text" in body && typeof body.text === "string") return body.text;
96
+ throw new Error("The Neon assistant returned an unexpected response.");
97
+ }
98
+ async function readJsonBody(response) {
99
+ const raw = await response.text();
100
+ if (raw.trim() === "") return;
101
+ try {
102
+ return JSON.parse(raw);
103
+ } catch {
104
+ return;
105
+ }
106
+ }
107
+ async function readStreamedAsk(body, opts) {
108
+ let text = "";
109
+ let sawDone = false;
110
+ for await (const event of readAskSse(body)) switch (event.type) {
111
+ case "status":
112
+ opts.onStatus?.(event.message);
113
+ break;
114
+ case "text":
115
+ text += event.text;
116
+ opts.onText?.(event.text);
117
+ break;
118
+ case "error": throw new Error(event.error);
119
+ case "done": sawDone = true;
120
+ }
121
+ if (!sawDone) throw new Error("The Neon assistant returned an unexpected response.");
122
+ return text;
123
+ }
124
+ async function askAssistant(opts) {
125
+ let response;
126
+ try {
127
+ response = await fetch(opts.url, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ Accept: opts.acceptEventStream ? "text/event-stream" : "application/json"
132
+ },
133
+ body: JSON.stringify({ prompt: opts.prompt }),
134
+ signal: AbortSignal.timeout(ASK_TIMEOUT_MS)
135
+ });
136
+ } catch (error) {
137
+ if (isAskTimeout(error)) throw new Error("The Neon assistant did not respond in time.");
138
+ if (isNetworkError(error)) throw new Error("Could not reach the Neon assistant. Check your internet connection and try again.");
139
+ throw error;
140
+ }
141
+ if (!response.ok) {
142
+ const body = await readJsonBody(response);
143
+ throw new Error(askErrorMessage(body, response.status));
144
+ }
145
+ if (opts.acceptEventStream && isEventStreamContentType(response.headers.get("content-type") ?? void 0)) {
146
+ if (!response.body) throw new Error("The Neon assistant returned an unexpected response.");
147
+ return readStreamedAsk(response.body, opts);
148
+ }
149
+ return askText(await readJsonBody(response));
150
+ }
151
+ //#endregion
152
+ export { DEFAULT_ASK_URL, builder, command, describe, handler, resolveAskUrl, ask_exports as t };
@@ -0,0 +1,101 @@
1
+ //#region src/commands/ask_sse.ts
2
+ function isEventStreamContentType(header) {
3
+ return header?.split(";")[0]?.trim().toLowerCase() === "text/event-stream";
4
+ }
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null;
7
+ }
8
+ function parseSseBlock(block) {
9
+ let event = "message";
10
+ const dataLines = [];
11
+ for (const line of block.split(/\r?\n/)) {
12
+ if (line === "" || line.startsWith(":")) continue;
13
+ if (line.startsWith("event:")) {
14
+ event = line.slice(6).trim();
15
+ continue;
16
+ }
17
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
18
+ }
19
+ if (event === "ping") return;
20
+ if (event === "message" && dataLines.length === 0) return;
21
+ return {
22
+ event,
23
+ data: dataLines.join("\n")
24
+ };
25
+ }
26
+ function takeSseBlocks(pending) {
27
+ const events = [];
28
+ let rest = pending;
29
+ while (true) {
30
+ const lf = rest.indexOf("\n\n");
31
+ const crlf = rest.indexOf("\r\n\r\n");
32
+ let at = -1;
33
+ let sep = 0;
34
+ if (lf === -1 && crlf === -1) break;
35
+ if (crlf !== -1 && (lf === -1 || crlf < lf)) {
36
+ at = crlf;
37
+ sep = 4;
38
+ } else {
39
+ at = lf;
40
+ sep = 2;
41
+ }
42
+ const parsed = parseSseBlock(rest.slice(0, at));
43
+ rest = rest.slice(at + sep);
44
+ if (parsed) events.push(parsed);
45
+ }
46
+ return {
47
+ events,
48
+ rest
49
+ };
50
+ }
51
+ function mapAskEvent(raw) {
52
+ if (raw.event === "done") return { type: "done" };
53
+ let parsed;
54
+ try {
55
+ parsed = raw.data === "" ? void 0 : JSON.parse(raw.data);
56
+ } catch {
57
+ return;
58
+ }
59
+ if (raw.event === "status") {
60
+ if (!isRecord(parsed) || typeof parsed.message !== "string" || parsed.message === "") return;
61
+ return {
62
+ type: "status",
63
+ message: parsed.message
64
+ };
65
+ }
66
+ if (raw.event === "text") {
67
+ if (!isRecord(parsed) || typeof parsed.text !== "string" || parsed.text === "") return;
68
+ return {
69
+ type: "text",
70
+ text: parsed.text
71
+ };
72
+ }
73
+ if (raw.event === "error") return {
74
+ type: "error",
75
+ error: isRecord(parsed) && typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : "The assistant failed."
76
+ };
77
+ }
78
+ async function* readAskSse(body) {
79
+ const decoder = new TextDecoder();
80
+ const reader = body.getReader();
81
+ let pending = "";
82
+ try {
83
+ while (true) {
84
+ const { done, value } = await reader.read();
85
+ pending += decoder.decode(value ?? /* @__PURE__ */ new Uint8Array(), { stream: !done });
86
+ const taken = takeSseBlocks(pending);
87
+ pending = taken.rest;
88
+ for (const raw of taken.events) {
89
+ const event = mapAskEvent(raw);
90
+ if (!event) continue;
91
+ yield event;
92
+ if (event.type === "error" || event.type === "done") return;
93
+ }
94
+ if (done) break;
95
+ }
96
+ } finally {
97
+ reader.releaseLock();
98
+ }
99
+ }
100
+ //#endregion
101
+ export { isEventStreamContentType, readAskSse };
@@ -5,7 +5,7 @@ import { log } from "../log.js";
5
5
  import { getApiClient } from "../api.js";
6
6
  import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
7
7
  import { setAuthContext } from "../auth_context.js";
8
- import { currentContextFile, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
8
+ import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
9
9
  import { _ as upsertProfile, d as newProfileLocation, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, t as DEFAULT_PROFILE, u as locationOf } from "../_chunks/profiles-CvnFEQyd.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
11
  import { auth, refreshToken } from "../auth.js";
@@ -220,6 +220,7 @@ const ensureAuth = async (props) => {
220
220
  if (isMcpOauth(props)) return;
221
221
  if (props._[0] === "open") return;
222
222
  if (isSkillsCommand(props) || isPluginsCommand(props)) return;
223
+ if (isAskCommand(props)) return;
223
224
  if (props._[0] === "init") {
224
225
  selectCredential({
225
226
  ...credentialInputs(),
@@ -23,7 +23,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { dirname, join } from "node:path";
24
24
  import chalk from "chalk";
25
25
  import { packagesToStage, resolveConfig } from "@neon/config";
26
- import { PushConflictError, apply, assertZipWithinLimits, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, loadConfigFromFile as loadConfigFromFile$1, plan, traceNativePackages } from "@neon/config-runtime";
26
+ import { PushConflictError, apply, assertZipWithinLimits, bundleAsIs, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, loadConfigFromFile as loadConfigFromFile$1, plan, resolveEsbuildEntry, traceNativePackages, zipFunctionBundle } from "@neon/config-runtime";
27
27
  //#region src/commands/config.ts
28
28
  var config_exports = /* @__PURE__ */ __exportAll({
29
29
  applyCmd: () => applyCmd,
@@ -41,27 +41,24 @@ var config_exports = /* @__PURE__ */ __exportAll({
41
41
  planCmd: () => planCmd,
42
42
  status: () => status
43
43
  });
44
- /**
45
- * Bundle a function with neonctl's OWN bundler (the shared esbuild helper) so the
46
- * config-runtime never has to import esbuild itself. Injecting this keeps esbuild
47
- * out of config-runtime's static module graph — and therefore out of the packaged
48
- * neonctl snapshot, which resolves esbuild dynamically at deploy time.
49
- */
50
44
  const neonctlBundler = async (fn) => {
45
+ if (typeof fn.bundler === "function") return zipFunctionBundle(fn.slug, await fn.bundler(fn));
46
+ if (fn.bundler === "none") return zipFunctionBundle(fn.slug, await bundleAsIs(fn));
47
+ const entry = await resolveEsbuildEntry(fn.source);
51
48
  const externalPackages = fn.externalPackages ?? [];
52
- const { files, metafile, warnings } = await bundleEntry(fn.source, { externalPackages: externalPackages.map((pkg) => pkg.name) });
49
+ const { files, metafile, warnings } = await bundleEntry(entry, { externalPackages: externalPackages.map((pkg) => pkg.name) });
53
50
  for (const warning of warnings) log.warning(warning);
54
51
  for (const finding of findUndeclaredNativePackages({
55
52
  metafile,
56
53
  declared: externalPackages.map((pkg) => pkg.name),
57
- projectDir: dirname(fn.source)
54
+ projectDir: dirname(entry)
58
55
  })) log.warning(describeNativeFinding(fn.slug, finding));
59
56
  const staged = packagesToStage(externalPackages);
60
57
  if (staged.length === 0) return zipBundle(files);
61
58
  const traced = await traceNativePackages({
62
59
  slug: fn.slug,
63
60
  packages: staged,
64
- projectDir: dirname(fn.source)
61
+ projectDir: dirname(entry)
65
62
  });
66
63
  for (const warning of traced.warnings) log.warning(warning);
67
64
  const entries = {
@@ -7,12 +7,12 @@ import { bundleEntry } from "../utils/esbuild.js";
7
7
  import { i as resolveDevEnv } from "../_chunks/env-NbA61JR3.js";
8
8
  import { resolveFunctionsFromConfig } from "../dev/functions.js";
9
9
  import { resolveWatchInputs } from "../dev/inputs.js";
10
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
11
- import { basename, dirname, join, resolve } from "node:path";
10
+ import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
11
+ import { basename, dirname, join, relative, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import chalk from "chalk";
14
14
  import { spawn, spawnSync } from "node:child_process";
15
- import { describeNativeFinding, findUndeclaredNativePackages } from "@neon/config-runtime";
15
+ import { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry } from "@neon/config-runtime";
16
16
  import { once } from "node:events";
17
17
  //#region src/commands/dev.ts
18
18
  var dev_exports = /* @__PURE__ */ __exportAll({
@@ -239,11 +239,13 @@ const plannedToUnit = (fn, branchEnv, searchBase) => {
239
239
  fn: Object.keys(fn.env)
240
240
  },
241
241
  ...fn.externalPackages ? { externalPackages: fn.externalPackages } : {},
242
+ ...fn.bundler !== void 0 ? { bundler: fn.bundler } : {},
242
243
  configKey: JSON.stringify({
243
244
  source: fn.source,
244
245
  port: fn.port ?? null,
245
246
  env: fn.env,
246
- externalPackages: fn.externalPackages ?? null
247
+ externalPackages: fn.externalPackages ?? null,
248
+ bundler: typeof fn.bundler === "function" ? "custom" : fn.bundler ?? null
247
249
  })
248
250
  };
249
251
  };
@@ -286,7 +288,7 @@ const runSupervisor = async (units, options = {}) => {
286
288
  const bundleAndStart = async (r) => {
287
289
  let bundlePath;
288
290
  try {
289
- bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages, r.unit.slug ?? void 0);
291
+ bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages, r.unit.slug ?? void 0, r.unit.bundler);
290
292
  } catch (err) {
291
293
  r.status = "error";
292
294
  logUnit(r.unit, chalk.red("bundle failed: ") + (err instanceof Error ? err.message : String(err)));
@@ -329,7 +331,7 @@ const runSupervisor = async (units, options = {}) => {
329
331
  }, 150);
330
332
  };
331
333
  const startUnit = async (r) => {
332
- r.watcher = await startWatcher(r.unit.source, () => {
334
+ r.watcher = await startWatcher(r.unit, () => {
333
335
  restart(r);
334
336
  });
335
337
  await bundleAndStart(r);
@@ -500,22 +502,50 @@ const spawnChild = (unit, runtimePath, bundlePath) => {
500
502
  * positive would otherwise repeat the whole block for its lifetime.
501
503
  */
502
504
  const reportedFindings = /* @__PURE__ */ new Map();
503
- const writeBundle = async (source, bundleDir, externalPackages, label) => {
504
- const { files, metafile, warnings } = await bundleEntry(source, { ...externalPackages ? { externalPackages } : {} });
505
+ const writeBundle = async (source, bundleDir, externalPackages, label, bundler) => {
506
+ if (bundler !== void 0 && bundler !== "esbuild") {
507
+ const files = await bundleForDev(source, label, bundler);
508
+ return writeBundleFiles(bundleDir, files);
509
+ }
510
+ const entry = await resolveEsbuildEntry(source);
511
+ const { files, metafile, warnings } = await bundleEntry(entry, { ...externalPackages ? { externalPackages } : {} });
505
512
  for (const warning of warnings) log.warning(warning);
506
513
  const findings = findUndeclaredNativePackages({
507
514
  metafile,
508
515
  declared: externalPackages ?? [],
509
- projectDir: dirname(source)
516
+ projectDir: dirname(entry)
510
517
  });
511
518
  const signature = findings.map((f) => f.name).join(",");
512
519
  if (reportedFindings.get(source) !== signature) {
513
520
  reportedFindings.set(source, signature);
514
521
  for (const finding of findings) log.warning(describeNativeFinding(label ?? basename(source), finding));
515
522
  }
523
+ return writeBundleFiles(bundleDir, files);
524
+ };
525
+ const bundleForDev = async (source, label, bundler) => {
526
+ const fn = {
527
+ slug: label ?? basename(source),
528
+ name: label ?? basename(source),
529
+ source,
530
+ env: {},
531
+ runtime: "nodejs24",
532
+ bundler
533
+ };
534
+ return bundler === "none" ? bundleAsIs(fn) : bundler(fn);
535
+ };
536
+ const writeBundleFiles = (bundleDir, files) => {
537
+ rmSync(bundleDir, {
538
+ recursive: true,
539
+ force: true
540
+ });
516
541
  mkdirSync(bundleDir, { recursive: true });
517
- for (const [name, contents] of Object.entries(files)) writeFileSync(join(bundleDir, name), contents);
518
- return join(bundleDir, "index.mjs");
542
+ for (const [name, contents] of Object.entries(files)) {
543
+ const dest = join(bundleDir, name);
544
+ mkdirSync(dirname(dest), { recursive: true });
545
+ writeFileSync(dest, contents);
546
+ }
547
+ const entry = ["index.mjs", "index.js"].find((name) => name in files);
548
+ return join(bundleDir, entry ?? "index.mjs");
519
549
  };
520
550
  const urlFor = (port) => port === null ? chalk.red("not running") : `http://localhost:${port}`;
521
551
  const waitForReady = (child) => new Promise((resolveReady) => {
@@ -594,11 +624,19 @@ const logUnit = (unit, message) => {
594
624
  const prefix = unit.label ? chalk.dim(`[${unit.label}] `) : "";
595
625
  log.info(`${prefix}${message}`);
596
626
  };
597
- const startWatcher = async (source, restart) => {
627
+ const startWatcher = async (unit, restart) => {
598
628
  const { default: chokidar } = await import("chokidar");
599
- const initialInputs = await resolveWatchInputs(source);
600
- if (initialInputs === null) return startDirectoryWatcher(chokidar, source, restart);
601
- return startInputWatcher(chokidar, source, initialInputs, restart);
629
+ if (unit.bundler !== void 0 && unit.bundler !== "esbuild") {
630
+ const dir = statSync(unit.source, { throwIfNoEntry: false })?.isDirectory() === true ? unit.source : dirname(unit.source);
631
+ return startDirectoryWatcher(chokidar, dir, restart, { ignoreDist: false });
632
+ }
633
+ let entry = unit.source;
634
+ try {
635
+ entry = await resolveEsbuildEntry(unit.source);
636
+ } catch {}
637
+ const initialInputs = await resolveWatchInputs(entry);
638
+ if (initialInputs === null) return startDirectoryWatcher(chokidar, entry, restart, { ignoreDist: true });
639
+ return startInputWatcher(chokidar, entry, initialInputs, restart);
602
640
  };
603
641
  /**
604
642
  * Watch the neon.ts file itself for changes, firing `onChange` on every save. Used by the
@@ -645,11 +683,15 @@ const startInputWatcher = async (chokidar, source, initialInputs, restart) => {
645
683
  close: () => watcher.close()
646
684
  };
647
685
  };
648
- const startDirectoryWatcher = async (chokidar, source, restart) => {
649
- const watchedDir = dirname(source);
686
+ const startDirectoryWatcher = async (chokidar, source, restart, options) => {
687
+ const watchedDir = statSync(source, { throwIfNoEntry: false })?.isDirectory() ? source : dirname(source);
650
688
  const isIgnored = (p) => {
651
- const segments = p.split(/[/\\]/);
652
- return segments.includes("node_modules") || segments.includes(".git") || segments.includes("dist");
689
+ const rel = relative(watchedDir, p);
690
+ if (rel === "" || rel === ".") return false;
691
+ if (rel.startsWith("..")) return false;
692
+ const segments = rel.split(/[/\\]/);
693
+ if (segments.includes("node_modules") || segments.includes(".git")) return true;
694
+ return options.ignoreDist && segments.includes("dist");
653
695
  };
654
696
  const watcher = chokidar.watch(watchedDir, {
655
697
  ignoreInitial: true,
@@ -7,9 +7,10 @@ import { writer } from "../writer.js";
7
7
  import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
8
8
  import { bundleEntry } from "../utils/esbuild.js";
9
9
  import { zipBundle } from "../utils/zip.js";
10
- import { existsSync, statSync } from "node:fs";
11
- import { dirname, join } from "node:path";
12
- import { describeNativeFinding, findUndeclaredNativePackages } from "@neon/config-runtime";
10
+ import { statSync } from "node:fs";
11
+ import { dirname } from "node:path";
12
+ import { FUNCTION_SOURCE_ENTRIES } from "@neon/config";
13
+ import { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry, zipFunctionBundle } from "@neon/config-runtime";
13
14
  //#region src/commands/functions.ts
14
15
  var functions_exports = /* @__PURE__ */ __exportAll({
15
16
  aliases: () => aliases,
@@ -55,11 +56,6 @@ const writeDeploymentErrorSection = (out, dep) => {
55
56
  };
56
57
  const SLUG_PATTERN = /^[a-z0-9]{1,20}$/;
57
58
  const SLUG_HELP = "Use 1-20 lowercase letters and digits (no hyphens or other characters).";
58
- const ENTRY_CANDIDATES = [
59
- "index.ts",
60
- "index.mjs",
61
- "index.js"
62
- ];
63
59
  const POLL_INTERVAL_MS = Number(process.env.NEON_FUNCTIONS_POLL_INTERVAL_MS) || 2e3;
64
60
  const POLL_TIMEOUT_MS = Number(process.env.NEON_FUNCTIONS_POLL_TIMEOUT_MS) || 6e5;
65
61
  const command = "functions";
@@ -80,7 +76,7 @@ const builder = (argv) => argv.usage("$0 function <sub-command> [options]").opti
80
76
  demandOption: true
81
77
  }).options({
82
78
  src: {
83
- describe: "Function source: a directory containing index.ts, index.mjs, or index.js, or a path to the entry file",
79
+ describe: "Function source: a directory containing index.ts, index.js, or index.mjs (first file match is the entry), or a path to the entry file",
84
80
  type: "string"
85
81
  },
86
82
  path: {
@@ -105,6 +101,11 @@ const builder = (argv) => argv.usage("$0 function <sub-command> [options]").opti
105
101
  describe: "Wait for the deployment to finish building",
106
102
  type: "boolean",
107
103
  default: true
104
+ },
105
+ bundle: {
106
+ describe: "Bundle --src with esbuild. Use --no-bundle to zip a prebuilt directory (root must contain index.mjs or index.js) or a file of that name.",
107
+ type: "boolean",
108
+ default: true
108
109
  }
109
110
  }), (args) => deploy(args)).command("list", "List functions on the branch", (yargs) => yargs, (args) => list(args)).command("get <slug>", "Show a function's details", (yargs) => yargs.positional("slug", {
110
111
  describe: "Function slug",
@@ -144,24 +145,33 @@ const emitDeployResult = (props, deployment, fn) => {
144
145
  };
145
146
  const isTransient = (err) => isNeonApiError(err) && (err.status === void 0 || err.status === 404 || err.status >= 500);
146
147
  const deploy = async (props) => {
147
- if (props.path !== void 0 || props.entry !== void 0) throw new Error("--path and --entry were removed. Use --src <dir>; the entry point is discovered as index.ts, index.mjs, or index.js in that directory.");
148
- if (!(props.src !== void 0 || props.env !== void 0 || props.runtime !== void 0)) throw new Error(`Provide at least one option to deploy, e.g. --src or --env. See: ${getCliName()} function deploy --help.`);
148
+ if (props.path !== void 0 || props.entry !== void 0) throw new Error(`--path and --entry were removed. Use --src <dir>; the entry point is discovered as ${FUNCTION_SOURCE_ENTRIES.join(", ")} in that directory.`);
149
+ if (!(props.src !== void 0 || props.env !== void 0 || props.runtime !== void 0 || props.bundle === false)) throw new Error(`Provide at least one option to deploy, e.g. --src, --env, or --no-bundle. See: ${getCliName()} function deploy --help.`);
149
150
  if (!SLUG_PATTERN.test(props.slug)) throw new Error(`Invalid function slug "${props.slug}". ${SLUG_HELP}`);
150
151
  const src = props.src ?? ".";
151
152
  const runtime = props.runtime ?? "nodejs24";
152
153
  const environment = parseEnv(props.env);
153
- const srcStat = statSync(src, { throwIfNoEntry: false });
154
- if (srcStat === void 0) throw new Error(`--src path not found: ${src}.`);
155
- const source = srcStat.isFile() ? src : ENTRY_CANDIDATES.map((name) => join(src, name)).find((p) => existsSync(p));
156
- if (source === void 0) throw new Error(`No entry file found in ${src}. Expected one of: ${ENTRY_CANDIDATES.join(", ")}.`);
157
- const bundled = await bundleEntry(source);
158
- for (const warning of bundled.warnings) log.warning(warning);
159
- for (const finding of findUndeclaredNativePackages({
160
- metafile: bundled.metafile,
161
- declared: [],
162
- projectDir: dirname(source)
163
- })) log.warning(describeNativeFinding(props.slug, finding));
164
- const zip = zipBundle(bundled.files);
154
+ if (statSync(src, { throwIfNoEntry: false }) === void 0) throw new Error(`--src path not found: ${src}.`);
155
+ let zip;
156
+ if (!props.bundle) zip = await zipFunctionBundle(props.slug, await bundleAsIs({
157
+ slug: props.slug,
158
+ name: props.slug,
159
+ source: src,
160
+ env: {},
161
+ runtime: "nodejs24",
162
+ bundler: "none"
163
+ }, { via: "no-bundle" }));
164
+ else {
165
+ const source = await resolveEsbuildEntry(src);
166
+ const bundled = await bundleEntry(source);
167
+ for (const warning of bundled.warnings) log.warning(warning);
168
+ for (const finding of findUndeclaredNativePackages({
169
+ metafile: bundled.metafile,
170
+ declared: [],
171
+ projectDir: dirname(source)
172
+ })) log.warning(describeNativeFinding(props.slug, finding));
173
+ zip = zipBundle(bundled.files);
174
+ }
165
175
  const branchId = await branchIdFromProps(props);
166
176
  let before;
167
177
  try {
@@ -1,6 +1,7 @@
1
1
  import { t as auth_exports } from "./auth.js";
2
2
  import { t as api_exports } from "./api.js";
3
3
  import { t as api_keys_exports } from "./api_keys.js";
4
+ import { t as ask_exports } from "./ask.js";
4
5
  import { t as bootstrap_exports } from "./bootstrap.js";
5
6
  import { t as branches_exports } from "./branches.js";
6
7
  import { t as bucket_exports } from "./bucket.js";
@@ -66,6 +67,7 @@ var commands_default = [
66
67
  mcp_exports,
67
68
  plugins_exports,
68
69
  skills_exports,
70
+ ask_exports,
69
71
  data_api_exports,
70
72
  functions_exports,
71
73
  dev_exports,
@@ -0,0 +1,47 @@
1
+ //#region src/commands/thinking_spinner.ts
2
+ const FRAMES = [
3
+ "⠋",
4
+ "⠙",
5
+ "⠹",
6
+ "⠸",
7
+ "⠼",
8
+ "⠴",
9
+ "⠦",
10
+ "⠧",
11
+ "⠇",
12
+ "⠏"
13
+ ];
14
+ const INTERVAL_MS = 80;
15
+ const CLEAR_LINE = "\r\x1B[2K";
16
+ function createThinkingSpinner(opts) {
17
+ let message = "Thinking";
18
+ let frame = 0;
19
+ let timer;
20
+ let stopped = false;
21
+ const paint = () => {
22
+ if (!opts.isTty || stopped) return;
23
+ const glyph = FRAMES[frame % FRAMES.length];
24
+ frame += 1;
25
+ opts.out.write(`${CLEAR_LINE}${glyph} ${message}`);
26
+ };
27
+ return {
28
+ start() {
29
+ if (!opts.isTty || stopped) return;
30
+ paint();
31
+ timer = setInterval(paint, INTERVAL_MS);
32
+ timer.unref();
33
+ },
34
+ setMessage(next) {
35
+ message = next;
36
+ paint();
37
+ },
38
+ stop() {
39
+ if (stopped) return;
40
+ stopped = true;
41
+ if (timer) clearInterval(timer);
42
+ if (opts.isTty) opts.out.write(CLEAR_LINE);
43
+ }
44
+ };
45
+ }
46
+ //#endregion
47
+ export { createThinkingSpinner };
package/dist/context.js CHANGED
@@ -54,6 +54,7 @@ const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "ap
54
54
  const isMcpCommand = (args) => args._[0] === "mcp";
55
55
  const isSkillsCommand = (args) => args._[0] === "skills" || args._[0] === "skill";
56
56
  const isPluginsCommand = (args) => args._[0] === "plugins" || args._[0] === "plugin";
57
+ const isAskCommand = (args) => args._[0] === "ask";
57
58
  /** Raw argv is required because auth middleware runs before MCP flags are parsed. */
58
59
  const isMcpOauth = (args) => isMcpCommand(args) && argvEnablesMcpOauth(process.argv);
59
60
  const OAUTH_FALSE = /* @__PURE__ */ new Set([
@@ -134,6 +135,7 @@ const enrichFromContext = (args) => {
134
135
  if (isProfileCommand(args)) return;
135
136
  if (isMcpCommand(args)) return;
136
137
  if (isSkillsCommand(args) || isPluginsCommand(args)) return;
138
+ if (isAskCommand(args)) return;
137
139
  if (isClaimCommand(args)) return;
138
140
  const context = readContextFile(args.contextFile);
139
141
  if (!args.orgId) args.orgId = context.orgId;
@@ -240,4 +242,4 @@ const globToRegExp = (pattern) => {
240
242
  return new RegExp(`^${source}$`);
241
243
  };
242
244
  //#endregion
243
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
245
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
@@ -29,7 +29,8 @@ const resolveFunctionsFromConfig = async (cwd, branchName) => {
29
29
  source,
30
30
  ...devPort(fn.dev) !== void 0 ? { port: devPort(fn.dev) } : {},
31
31
  env: { ...fn.env },
32
- ...fn.externalPackages ? { externalPackages: fn.externalPackages.map((pkg) => pkg.name) } : {}
32
+ ...fn.externalPackages ? { externalPackages: fn.externalPackages.map((pkg) => pkg.name) } : {},
33
+ ...fn.bundler !== "esbuild" ? { bundler: fn.bundler } : {}
33
34
  };
34
35
  })
35
36
  };
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ const NO_SUBCOMMANDS_VERBS = [
35
35
  "plugin",
36
36
  "skills",
37
37
  "skill",
38
+ "ask",
38
39
  "dev",
39
40
  "deploy",
40
41
  "diff",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.11.1",
3
+ "version": "4.13.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.0.5",
67
- "@neon/config-runtime": "1.0.5",
68
- "@neon/sdk": "3.0.0"
66
+ "@neon/config": "1.1.0",
67
+ "@neon/sdk": "3.0.0",
68
+ "@neon/config-runtime": "1.1.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -97,8 +97,8 @@
97
97
  "typescript": "^5.9.0",
98
98
  "vitest": "^3.0.9",
99
99
  "@neon-internals/cli-core": "0.0.0",
100
- "@neon-internals/env-core": "0.0.5",
101
- "@neon/e2e-harness": "0.0.0"
100
+ "@neon/e2e-harness": "0.0.0",
101
+ "@neon-internals/env-core": "0.0.6"
102
102
  },
103
103
  "publishConfig": {
104
104
  "access": "public",