neon 2.45.0 → 2.46.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 +62 -1
- package/dist/_shared/paths.js +3 -4
- package/dist/commands/bootstrap.js +12 -11
- package/dist/commands/checkout.js +7 -6
- package/dist/commands/config.js +2 -1
- package/dist/commands/data_api.js +4 -3
- package/dist/commands/env.js +5 -4
- package/dist/commands/functions.js +3 -2
- package/dist/commands/init.js +82 -37
- package/dist/commands/ip_allow.js +3 -2
- package/dist/commands/link.js +17 -16
- package/dist/commands/projects.js +3 -2
- package/dist/commands/set_context.js +5 -4
- package/dist/current_branch_fast_path.js +2 -1
- package/dist/dev/env.js +8 -7
- package/dist/index.js +2 -2
- package/dist/init/agents.js +127 -0
- package/dist/init/auth.js +77 -0
- package/dist/init/bootstrap.js +448 -0
- package/dist/init/build_config.js +2 -0
- package/dist/init/detect_agent.js +108 -0
- package/dist/init/editors.js +62 -0
- package/dist/init/enrich_output.js +71 -0
- package/dist/init/extension.js +191 -0
- package/dist/init/inspect.js +287 -0
- package/dist/init/interactive.js +651 -0
- package/dist/init/neonctl.js +184 -0
- package/dist/init/orchestrate.js +190 -0
- package/dist/init/phases/auth.js +209 -0
- package/dist/init/phases/cleanup.js +27 -0
- package/dist/init/phases/db.js +283 -0
- package/dist/init/phases/getting_started.js +228 -0
- package/dist/init/phases/mcp.js +227 -0
- package/dist/init/phases/migrations.js +251 -0
- package/dist/init/phases/neon_auth.js +135 -0
- package/dist/init/phases/setup.js +729 -0
- package/dist/init/phases/skills.js +89 -0
- package/dist/init/phases/status.js +70 -0
- package/dist/init/resolve_context.js +107 -0
- package/dist/init/route_command.js +100 -0
- package/dist/init/skills.js +248 -0
- package/dist/init/types.js +1 -0
- package/dist/init/vsix.js +111 -0
- package/dist/psql/command/cmd_meta.js +2 -2
- package/dist/psql/core/mainloop.js +1 -1
- package/dist/psql/core/startup.js +1 -1
- package/dist/psql/core/syncVars.js +3 -3
- package/dist/psql/index.js +1 -1
- package/dist/utils/cli_name.js +14 -0
- package/dist/utils/esbuild.js +1 -1
- package/dist/utils/write_sync.js +39 -0
- package/package.json +18 -12
package/dist/commands/link.js
CHANGED
|
@@ -4,6 +4,7 @@ import { applyContext, contextBranch, readContextFile, setContext, updateContext
|
|
|
4
4
|
import { isCi } from "../env.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
6
|
import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
|
|
7
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
7
8
|
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
8
9
|
import { autoPullEnvAfterPin, renderAgentPullNote } from "./env.js";
|
|
9
10
|
import { REGIONS } from "./projects.js";
|
|
@@ -34,7 +35,7 @@ export const builder = (argv) => argv
|
|
|
34
35
|
alias: "branch-id",
|
|
35
36
|
describe: "Branch name or ID to pin in the context (resolved to its ID before writing). " +
|
|
36
37
|
"Without it, link only resolves the org and project — pin a branch with " +
|
|
37
|
-
|
|
38
|
+
`\`${getCliName()} checkout <branch>\` (link never guesses a default).`,
|
|
38
39
|
type: "string",
|
|
39
40
|
},
|
|
40
41
|
params: {
|
|
@@ -76,7 +77,7 @@ export const builder = (argv) => argv
|
|
|
76
77
|
.example([
|
|
77
78
|
[
|
|
78
79
|
"$0 link --project-id polished-snowflake-12345678",
|
|
79
|
-
|
|
80
|
+
`Link an existing project (org is inferred); pin a branch later with '${getCliName()} checkout'`,
|
|
80
81
|
],
|
|
81
82
|
[
|
|
82
83
|
"$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2",
|
|
@@ -120,9 +121,9 @@ export const handler = async (props) => {
|
|
|
120
121
|
"Missing inputs and CI environment detected (no TTY for prompts).",
|
|
121
122
|
"",
|
|
122
123
|
"Use one of:",
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
` ${getCliName()} link --agent (JSON state machine for agents)`,
|
|
125
|
+
` ${getCliName()} link --project-id <project> (link to an existing project; org is inferred)`,
|
|
126
|
+
` ${getCliName()} link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)`,
|
|
126
127
|
].join("\n"));
|
|
127
128
|
process.exit(1);
|
|
128
129
|
return;
|
|
@@ -180,7 +181,7 @@ const validateInputs = (inputs) => {
|
|
|
180
181
|
throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
|
|
181
182
|
}
|
|
182
183
|
if (inputs.projectName && inputs.branch) {
|
|
183
|
-
throw new Error(
|
|
184
|
+
throw new Error(`Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then \`${getCliName()} checkout <branch>\`.`);
|
|
184
185
|
}
|
|
185
186
|
};
|
|
186
187
|
/**
|
|
@@ -338,7 +339,7 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
|
338
339
|
.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`)
|
|
339
340
|
.join(", ")
|
|
340
341
|
: "(none)";
|
|
341
|
-
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with
|
|
342
|
+
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`${getCliName()} checkout <branch>\`.`, "NOT_FOUND");
|
|
342
343
|
};
|
|
343
344
|
/**
|
|
344
345
|
* The value to persist for a branch: prefer its human-readable **name** (nicer
|
|
@@ -717,7 +718,7 @@ const runAgent = async (props, inputs) => {
|
|
|
717
718
|
context_file: props.contextFile,
|
|
718
719
|
context: { orgId, projectId },
|
|
719
720
|
project: { id: projectId },
|
|
720
|
-
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run
|
|
721
|
+
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run \`${getCliName()} checkout <branch>\` (omit the branch to list options) to pin one and pull its env vars.`,
|
|
721
722
|
});
|
|
722
723
|
return;
|
|
723
724
|
}
|
|
@@ -737,7 +738,7 @@ const runAgent = async (props, inputs) => {
|
|
|
737
738
|
name: region.name,
|
|
738
739
|
default: region.default,
|
|
739
740
|
})),
|
|
740
|
-
next_command_template:
|
|
741
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name ${shellArg(projectName)} --region-id <region_id>`,
|
|
741
742
|
});
|
|
742
743
|
return;
|
|
743
744
|
}
|
|
@@ -781,7 +782,7 @@ const runAgent = async (props, inputs) => {
|
|
|
781
782
|
// the instruction rather than silently dropped.
|
|
782
783
|
const projects = await listAllProjects(props, orgId);
|
|
783
784
|
const branchNote = branch
|
|
784
|
-
? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then
|
|
785
|
+
? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then \`${getCliName()} checkout ${branch}\`.`
|
|
785
786
|
: "";
|
|
786
787
|
emitAgent({
|
|
787
788
|
status: "needs_project",
|
|
@@ -796,9 +797,9 @@ const runAgent = async (props, inputs) => {
|
|
|
796
797
|
})),
|
|
797
798
|
create_option: {
|
|
798
799
|
instruction: "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
|
|
799
|
-
next_command_template:
|
|
800
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`,
|
|
800
801
|
},
|
|
801
|
-
next_command_template:
|
|
802
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
|
|
802
803
|
});
|
|
803
804
|
};
|
|
804
805
|
const emitAgent = (response) => {
|
|
@@ -865,7 +866,7 @@ const buildNeedsOrgResponse = (resolution) => {
|
|
|
865
866
|
status: "needs_org",
|
|
866
867
|
instruction: "This Neon API key is organization-scoped, so the CLI cannot list the user's organizations and no existing project was found to auto-detect the org ID. Ask the user for their Neon organization ID (visible in the Neon Console under the org's Settings page, formatted like `org-bitter-breeze-12345678`) and re-run the next_command_template with that --org-id.",
|
|
867
868
|
options: [],
|
|
868
|
-
next_command_template:
|
|
869
|
+
next_command_template: `${getCliName()} link --agent --org-id <org_id>`,
|
|
869
870
|
};
|
|
870
871
|
}
|
|
871
872
|
const orgs = resolution.orgs;
|
|
@@ -875,7 +876,7 @@ const buildNeedsOrgResponse = (resolution) => {
|
|
|
875
876
|
? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking."
|
|
876
877
|
: `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ? "" : "s"} they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.`,
|
|
877
878
|
options: orgs.map((org) => ({ id: org.id, name: org.name })),
|
|
878
|
-
next_command_template:
|
|
879
|
+
next_command_template: `${getCliName()} link --agent --org-id <org_id>`,
|
|
879
880
|
};
|
|
880
881
|
};
|
|
881
882
|
const toAgentError = (err) => {
|
|
@@ -954,7 +955,7 @@ const resolveInteractiveBranch = async (props, projectId) => {
|
|
|
954
955
|
const picked = await pickBranchInteractively(branches, {
|
|
955
956
|
message: "Which branch would you like to link?",
|
|
956
957
|
nonInteractiveMessage: "No branch could be selected without an interactive terminal. " +
|
|
957
|
-
|
|
958
|
+
`Re-run \`${getCliName()} link\` interactively, or \`${getCliName()} checkout <branch>\` to pin one.`,
|
|
958
959
|
});
|
|
959
960
|
if (picked.kind === "existing") {
|
|
960
961
|
const existing = branches.find((b) => b.id === picked.branchId);
|
|
@@ -1031,7 +1032,7 @@ const printSummary = (_props, summary) => {
|
|
|
1031
1032
|
}
|
|
1032
1033
|
else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
1033
1034
|
lines.push("");
|
|
1034
|
-
lines.push(
|
|
1035
|
+
lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
1035
1036
|
}
|
|
1036
1037
|
lines.push("");
|
|
1037
1038
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
@@ -4,6 +4,7 @@ import { updateContextFile } from "../context.js";
|
|
|
4
4
|
import { isCi } from "../env.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
6
|
import { projectCreateRequest, projectUpdateRequest, } from "../parameters.gen.js";
|
|
7
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
7
8
|
import { getComputeUnits } from "../utils/compute_units.js";
|
|
8
9
|
import { psql } from "../utils/psql.js";
|
|
9
10
|
import { writer } from "../writer.js";
|
|
@@ -392,11 +393,11 @@ The organization ID has been saved in ${props.contextFile}
|
|
|
392
393
|
|
|
393
394
|
If you'd like to change the default organization later, use
|
|
394
395
|
|
|
395
|
-
|
|
396
|
+
${getCliName()} link --org-id <org_id>
|
|
396
397
|
|
|
397
398
|
Or to clear the context file and forget the default organization
|
|
398
399
|
|
|
399
|
-
|
|
400
|
+
${getCliName()} link --clear
|
|
400
401
|
|
|
401
402
|
`);
|
|
402
403
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { applyContext } from "../context.js";
|
|
2
2
|
import { log } from "../log.js";
|
|
3
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
3
4
|
export const command = "set-context";
|
|
4
|
-
export const describe =
|
|
5
|
+
export const describe = `Deprecated: use \`${getCliName()} link\`. Set the .neon context (raw write).`;
|
|
5
6
|
export const builder = (argv) => argv.usage("$0 set-context [options]").options({
|
|
6
7
|
"project-id": {
|
|
7
8
|
describe: "Project ID",
|
|
@@ -17,9 +18,9 @@ export const builder = (argv) => argv.usage("$0 set-context [options]").options(
|
|
|
17
18
|
},
|
|
18
19
|
});
|
|
19
20
|
export const handler = (props) => {
|
|
20
|
-
log.warning(
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
log.warning(`\`${getCliName()} set-context\` is deprecated and will be removed in a future release. ` +
|
|
22
|
+
`Use \`${getCliName()} link\` instead — it verifies inputs and infers the org for you ` +
|
|
23
|
+
`(or \`${getCliName()} link --no-checks\` for the same write-without-checks behavior).`);
|
|
23
24
|
const context = {
|
|
24
25
|
projectId: props.projectId,
|
|
25
26
|
orgId: props.orgId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { contextBranch, currentContextFile, readContextFile, } from "./context.js";
|
|
2
2
|
import { log } from "./log.js";
|
|
3
|
+
import { getCliName } from "./utils/cli_name.js";
|
|
3
4
|
/**
|
|
4
5
|
* Offline fast path for `(config) status --current-branch` (used by shell prompts).
|
|
5
6
|
*
|
|
@@ -35,7 +36,7 @@ cwd = process.cwd()) => {
|
|
|
35
36
|
process.stdout.write(`${branch}\n`);
|
|
36
37
|
}
|
|
37
38
|
else {
|
|
38
|
-
log.info(
|
|
39
|
+
log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
39
40
|
process.exitCode = 1;
|
|
40
41
|
}
|
|
41
42
|
return true;
|
package/dist/dev/env.js
CHANGED
|
@@ -2,6 +2,7 @@ import { loadConfigFromFile } from "@neon/config";
|
|
|
2
2
|
import { plan, pullConfig } from "@neon/config-runtime";
|
|
3
3
|
import { fetchEnvReusingSecrets, } from "@neon/env/runtime";
|
|
4
4
|
import { log } from "../log.js";
|
|
5
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
5
6
|
/** The API-targeting options every runtime call forwards from the context. */
|
|
6
7
|
const apiOptions = (ctx) => ({
|
|
7
8
|
...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
|
|
@@ -58,7 +59,7 @@ export const resolveNeonEnvVars = async (ctx) => {
|
|
|
58
59
|
if (config) {
|
|
59
60
|
if (!ctx.projectId || !ctx.branchId) {
|
|
60
61
|
throw new MissingBranchContextError("Found a neon.ts but could not resolve the project/branch. " +
|
|
61
|
-
|
|
62
|
+
`Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass ` +
|
|
62
63
|
"--project-id / --branch.");
|
|
63
64
|
}
|
|
64
65
|
// Resolve env from the policy with its `preview.functions` removed. Functions carry no
|
|
@@ -86,8 +87,8 @@ export const resolveNeonEnvVars = async (ctx) => {
|
|
|
86
87
|
// the functions API here and only mints a storage credential when a bucket exists.
|
|
87
88
|
return await fetchAndProject(pulled.config, ctx);
|
|
88
89
|
}
|
|
89
|
-
throw new MissingBranchContextError(
|
|
90
|
-
|
|
90
|
+
throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / ` +
|
|
91
|
+
`\`${getCliName()} checkout\`) or pass --project-id and --branch.`);
|
|
91
92
|
};
|
|
92
93
|
/**
|
|
93
94
|
* `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
|
|
@@ -113,8 +114,8 @@ export const resolveDevEnv = async (ctx) => {
|
|
|
113
114
|
return {
|
|
114
115
|
vars: {},
|
|
115
116
|
skipped: {
|
|
116
|
-
reason:
|
|
117
|
-
|
|
117
|
+
reason: `no linked Neon branch — run \`${getCliName()} link\`, then ` +
|
|
118
|
+
`\`${getCliName()} checkout <branch>\`, to inject DATABASE_URL and friends`,
|
|
118
119
|
},
|
|
119
120
|
};
|
|
120
121
|
}
|
|
@@ -167,8 +168,8 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
|
167
168
|
const names = missing.map((change) => change.identifier).join(", ");
|
|
168
169
|
throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
|
|
169
170
|
"does not have it yet, so the matching env vars cannot be injected. " +
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
`Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), ` +
|
|
172
|
+
`then re-run \`${getCliName()} dev\`.`);
|
|
172
173
|
};
|
|
173
174
|
/**
|
|
174
175
|
* A planned change that provisions a branch-level resource the branch lacks: a
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import yargs from "yargs";
|
|
3
2
|
import { hideBin } from "yargs/helpers";
|
|
4
3
|
import { analyticsMiddleware, closeAnalytics, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent, } from "./analytics.js";
|
|
@@ -13,6 +12,7 @@ import { isNetworkError, matchErrorCode, NETWORK_ERROR_MESSAGE, } from "./errors
|
|
|
13
12
|
import { showHelp } from "./help.js";
|
|
14
13
|
import { log } from "./log.js";
|
|
15
14
|
import pkg from "./pkg.js";
|
|
15
|
+
import { getCliName } from "./utils/cli_name.js";
|
|
16
16
|
import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
|
|
17
17
|
const NO_SUBCOMMANDS_VERBS = [
|
|
18
18
|
// `api <path>` has a handler but no subcommands (like `status`), so the
|
|
@@ -159,7 +159,7 @@ builder = builder
|
|
|
159
159
|
.group("version", "Global options:")
|
|
160
160
|
.alias("version", "v")
|
|
161
161
|
.completion()
|
|
162
|
-
.scriptName(
|
|
162
|
+
.scriptName(getCliName())
|
|
163
163
|
.epilog("For more information, visit https://neon.com/docs/reference/neon-cli")
|
|
164
164
|
.wrap(null)
|
|
165
165
|
.fail(false);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All agents that can be configured via neon-init.
|
|
3
|
+
* Aligns with add-mcp's supported agents table.
|
|
4
|
+
* https://github.com/neondatabase/add-mcp#supported-agents
|
|
5
|
+
*/
|
|
6
|
+
export const ALL_CONFIGURABLE_AGENTS = [
|
|
7
|
+
{
|
|
8
|
+
editor: "Cursor",
|
|
9
|
+
addMcpId: "cursor",
|
|
10
|
+
hint: "Neon Local Connect extension",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
editor: "VS Code",
|
|
14
|
+
addMcpId: "vscode",
|
|
15
|
+
hint: "Neon Local Connect extension",
|
|
16
|
+
},
|
|
17
|
+
{ editor: "Claude CLI", addMcpId: "claude-code", hint: "MCP Server" },
|
|
18
|
+
{
|
|
19
|
+
editor: "Claude Desktop",
|
|
20
|
+
addMcpId: "claude-desktop",
|
|
21
|
+
hint: "MCP Server",
|
|
22
|
+
},
|
|
23
|
+
{ editor: "Codex", addMcpId: "codex", hint: "MCP Server" },
|
|
24
|
+
{ editor: "OpenCode", addMcpId: "opencode", hint: "MCP Server" },
|
|
25
|
+
{ editor: "Antigravity", addMcpId: "antigravity", hint: "MCP Server" },
|
|
26
|
+
{ editor: "Cline", addMcpId: "cline", hint: "MCP Server" },
|
|
27
|
+
{ editor: "Cline CLI", addMcpId: "cline-cli", hint: "MCP Server" },
|
|
28
|
+
{ editor: "Gemini CLI", addMcpId: "gemini-cli", hint: "MCP Server" },
|
|
29
|
+
{
|
|
30
|
+
editor: "GitHub Copilot CLI",
|
|
31
|
+
addMcpId: "github-copilot-cli",
|
|
32
|
+
hint: "MCP Server",
|
|
33
|
+
},
|
|
34
|
+
{ editor: "Goose", addMcpId: "goose", hint: "MCP Server" },
|
|
35
|
+
{ editor: "MCPorter", addMcpId: "mcporter", hint: "MCP Server" },
|
|
36
|
+
{ editor: "Zed", addMcpId: "zed", hint: "MCP Server" },
|
|
37
|
+
];
|
|
38
|
+
export function getAddMcpAgentId(editor) {
|
|
39
|
+
const agent = ALL_CONFIGURABLE_AGENTS.find((a) => a.editor === editor);
|
|
40
|
+
if (!agent) {
|
|
41
|
+
throw new Error(`No add-mcp agent ID found for editor: ${editor}`);
|
|
42
|
+
}
|
|
43
|
+
return agent.addMcpId;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Maps a raw agent identifier (as reported by agents or passed via --agent)
|
|
47
|
+
* to the add-mcp compatible agent ID.
|
|
48
|
+
*
|
|
49
|
+
* This handles aliases like "copilot" → "vscode", "claude" → "claude-code", etc.
|
|
50
|
+
*/
|
|
51
|
+
const AGENT_ALIAS_TO_MCP_ID = {
|
|
52
|
+
cursor: "cursor",
|
|
53
|
+
copilot: "vscode",
|
|
54
|
+
"github-copilot": "vscode",
|
|
55
|
+
"vs-code": "vscode",
|
|
56
|
+
vscode: "vscode",
|
|
57
|
+
claude: "claude-code",
|
|
58
|
+
"claude-code": "claude-code",
|
|
59
|
+
"claude-desktop": "claude-desktop",
|
|
60
|
+
codex: "codex",
|
|
61
|
+
opencode: "opencode",
|
|
62
|
+
antigravity: "antigravity",
|
|
63
|
+
cline: "cline",
|
|
64
|
+
"cline-cli": "cline-cli",
|
|
65
|
+
"gemini-cli": "gemini-cli",
|
|
66
|
+
gemini: "gemini-cli",
|
|
67
|
+
goose: "goose",
|
|
68
|
+
windsurf: "windsurf",
|
|
69
|
+
"github-copilot-cli": "github-copilot-cli",
|
|
70
|
+
mcporter: "mcporter",
|
|
71
|
+
zed: "zed",
|
|
72
|
+
};
|
|
73
|
+
export function resolveAddMcpAgentId(rawAgent) {
|
|
74
|
+
const resolved = AGENT_ALIAS_TO_MCP_ID[rawAgent.toLowerCase()];
|
|
75
|
+
if (!resolved) {
|
|
76
|
+
throw new Error(`Unknown agent: "${rawAgent}". Supported agents: ${Object.keys(AGENT_ALIAS_TO_MCP_ID).join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Maps a raw agent identifier to the skills CLI agent name.
|
|
82
|
+
*/
|
|
83
|
+
export function getSkillsAgentName(agent) {
|
|
84
|
+
switch (agent.toLowerCase()) {
|
|
85
|
+
case "cursor":
|
|
86
|
+
return "cursor";
|
|
87
|
+
case "copilot":
|
|
88
|
+
case "vscode":
|
|
89
|
+
case "vs-code":
|
|
90
|
+
case "github-copilot":
|
|
91
|
+
return "github-copilot";
|
|
92
|
+
case "claude":
|
|
93
|
+
case "claude-code":
|
|
94
|
+
return "claude-code";
|
|
95
|
+
case "codex":
|
|
96
|
+
return "codex";
|
|
97
|
+
case "opencode":
|
|
98
|
+
return "opencode";
|
|
99
|
+
case "antigravity":
|
|
100
|
+
return "antigravity";
|
|
101
|
+
case "cline":
|
|
102
|
+
return "cline";
|
|
103
|
+
case "gemini-cli":
|
|
104
|
+
return "gemini-cli";
|
|
105
|
+
case "goose":
|
|
106
|
+
return "goose";
|
|
107
|
+
case "claude-desktop":
|
|
108
|
+
return "claude-code";
|
|
109
|
+
case "cline-cli":
|
|
110
|
+
return "cline";
|
|
111
|
+
case "gemini":
|
|
112
|
+
return "gemini-cli";
|
|
113
|
+
case "windsurf":
|
|
114
|
+
return "windsurf";
|
|
115
|
+
case "github-copilot-cli":
|
|
116
|
+
return "github-copilot";
|
|
117
|
+
case "mcporter":
|
|
118
|
+
return "mcporter";
|
|
119
|
+
case "zed":
|
|
120
|
+
return "zed";
|
|
121
|
+
default:
|
|
122
|
+
// Fall back to "cursor" as a safe default — skills CLI uses the
|
|
123
|
+
// agent name to pick the output directory (.agents/skills, .cursor/skills, etc.)
|
|
124
|
+
// and "cursor" uses .agents/skills which works for all agents.
|
|
125
|
+
return "cursor";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import { inspectCredentials, interpretCredentials, } from "../_shared/credentials.js";
|
|
4
|
+
import { resolveConfigFile } from "../_shared/paths.js";
|
|
5
|
+
import { DEFAULT_PROFILE } from "../_shared/profiles.js";
|
|
6
|
+
/**
|
|
7
|
+
* Ensures neonctl is authenticated by running a command that triggers auth if needed
|
|
8
|
+
* This will automatically start the OAuth flow if the user isn't already authenticated
|
|
9
|
+
*/
|
|
10
|
+
export async function ensureNeonctlAuth(options) {
|
|
11
|
+
const quiet = options?.json === true;
|
|
12
|
+
// If already authenticated (e.g. ran in a terminal before), we can proceed
|
|
13
|
+
const existingToken = await getNeonctlAccessToken();
|
|
14
|
+
if (existingToken)
|
|
15
|
+
return true;
|
|
16
|
+
try {
|
|
17
|
+
// Use execa to authenticate with neonctl
|
|
18
|
+
await execa("npx", ["-y", "neonctl", "me"], {
|
|
19
|
+
// Shows OAuth URL and prompts to the user
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
// Unset CI so neonctl doesn't refuse to open the browser (e.g. when run from agent chat)
|
|
22
|
+
env: { ...process.env, CI: undefined },
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
28
|
+
if (!quiet) {
|
|
29
|
+
if (msg.includes("interactive auth") || msg.includes("CI")) {
|
|
30
|
+
log.error("Auth requires an interactive terminal. Run neon init in your system terminal (outside the chat) to sign in.");
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
log.error(`Authentication failed: ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Checks whether neonctl has stored OAuth credentials.
|
|
41
|
+
*/
|
|
42
|
+
export async function isAuthenticated() {
|
|
43
|
+
const token = await getNeonctlAccessToken();
|
|
44
|
+
return token !== null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The credential the Neon CLI has stored, or `null` when there is none.
|
|
48
|
+
*
|
|
49
|
+
* Shares the reader with `neon` and `@neon/env` rather than keeping a copy — the inline path
|
|
50
|
+
* resolution this replaced was the third implementation of "where is the config directory", and
|
|
51
|
+
* it also looked only for `access_token`, so an account signed in with an API key read as not
|
|
52
|
+
* authenticated and got sent to a browser. See `shared/cli-core/README.md`.
|
|
53
|
+
*
|
|
54
|
+
* `null` means *absent*, and only absent. A file that exists and cannot be read throws: this
|
|
55
|
+
* value decides whether to start a browser sign-in, and a sign-in overwrites the file it could
|
|
56
|
+
* not read — as a different account, if a different one is chosen. Catching every error here
|
|
57
|
+
* made a damaged credential indistinguishable from a fresh machine.
|
|
58
|
+
*/
|
|
59
|
+
async function getNeonctlAccessToken() {
|
|
60
|
+
const { path } = resolveConfigFile("credentials.json");
|
|
61
|
+
const read = inspectCredentials(path);
|
|
62
|
+
if (read.kind === "absent")
|
|
63
|
+
return null;
|
|
64
|
+
if (read.kind === "unusable") {
|
|
65
|
+
throw new Error(`${read.reason}. Replace it deliberately with \`neon profile create ${DEFAULT_PROFILE} --force\`, or delete the file.`);
|
|
66
|
+
}
|
|
67
|
+
// `neon init` has no profile selection — it reads the default credential and refuses
|
|
68
|
+
// when one is named, so `DEFAULT` is the only profile this can ever be about.
|
|
69
|
+
const credential = interpretCredentials(read.credentials, {
|
|
70
|
+
path,
|
|
71
|
+
profile: DEFAULT_PROFILE,
|
|
72
|
+
});
|
|
73
|
+
if (credential.kind === "api_key")
|
|
74
|
+
return credential.apiKey;
|
|
75
|
+
const token = read.credentials.access_token;
|
|
76
|
+
return typeof token === "string" && token.trim() !== "" ? token : null;
|
|
77
|
+
}
|