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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getSkillsAgentName } from "../agents.js";
|
|
2
|
+
import { SKILL_REFERENCE_URLS } from "../skills.js";
|
|
3
|
+
export async function handleSkillsPhase(options) {
|
|
4
|
+
const agentArgs = options.agent
|
|
5
|
+
? ["--agent", options.agent, "--json"]
|
|
6
|
+
: ["--json"];
|
|
7
|
+
const skillsAgent = options.agent
|
|
8
|
+
? getSkillsAgentName(options.agent)
|
|
9
|
+
: "claude-code";
|
|
10
|
+
// --status: ask agent to check
|
|
11
|
+
if (options.status) {
|
|
12
|
+
return {
|
|
13
|
+
phase: "tooling",
|
|
14
|
+
status: "skills_check",
|
|
15
|
+
nextAction: {
|
|
16
|
+
type: "agent_check",
|
|
17
|
+
checks: [
|
|
18
|
+
{
|
|
19
|
+
id: "skills",
|
|
20
|
+
description: "Check if Neon agent skills are installed in this project",
|
|
21
|
+
lookFor: [
|
|
22
|
+
"A skill file referencing 'neon-postgres' (e.g. .cursor/skills/, CLAUDE.md, .cursorrules)",
|
|
23
|
+
"A .skills/ directory with neon-related content",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
reportBack: {
|
|
28
|
+
type: "run_neon_init",
|
|
29
|
+
args: [
|
|
30
|
+
"skills",
|
|
31
|
+
"--json",
|
|
32
|
+
...(options.agent ? ["--agent", options.agent] : []),
|
|
33
|
+
...(options.update ? ["--update"] : ["--install"]),
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// --install or --update: run the skills CLI
|
|
40
|
+
if (options.install || options.update) {
|
|
41
|
+
const installCmd = `npx -y skills add neondatabase/agent-skills --skill neon-postgres --agent ${skillsAgent} -y`;
|
|
42
|
+
return {
|
|
43
|
+
phase: "tooling",
|
|
44
|
+
status: "installing_skills",
|
|
45
|
+
nextAction: {
|
|
46
|
+
type: "run_command",
|
|
47
|
+
command: installCmd,
|
|
48
|
+
description: `Installing Neon agent skills for ${skillsAgent}.`,
|
|
49
|
+
timeout: 30000,
|
|
50
|
+
onSuccess: {
|
|
51
|
+
type: "run_neon_init",
|
|
52
|
+
args: agentArgs,
|
|
53
|
+
},
|
|
54
|
+
onFailure: {
|
|
55
|
+
other: {
|
|
56
|
+
type: "run_neon_init",
|
|
57
|
+
args: agentArgs,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
skillReferences: SKILL_REFERENCE_URLS,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Default: ask if they want skills installed
|
|
65
|
+
return {
|
|
66
|
+
phase: "tooling",
|
|
67
|
+
status: "skills_available",
|
|
68
|
+
nextAction: {
|
|
69
|
+
type: "ask_user",
|
|
70
|
+
question: "Would you like to install Neon agent skills for this project? Skills provide your AI assistant with Neon-specific knowledge.",
|
|
71
|
+
options: [
|
|
72
|
+
{ value: "yes", label: "Yes, install skills" },
|
|
73
|
+
{ value: "skip", label: "Skip for now" },
|
|
74
|
+
],
|
|
75
|
+
context: "Agent skills include guides for connection patterns, Neon Auth setup, branching, migrations, and more.",
|
|
76
|
+
responseMapping: {
|
|
77
|
+
yes: {
|
|
78
|
+
args: [
|
|
79
|
+
"skills",
|
|
80
|
+
"--json",
|
|
81
|
+
...(options.agent ? ["--agent", options.agent] : []),
|
|
82
|
+
"--install",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
skip: { args: agentArgs },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isAuthenticated } from "../auth.js";
|
|
2
|
+
import { inspectProject } from "../inspect.js";
|
|
3
|
+
export async function handleStatusPhase(_options) {
|
|
4
|
+
const authed = await isAuthenticated();
|
|
5
|
+
const inspection = await inspectProject([
|
|
6
|
+
{ id: "mcp_server", description: "", lookFor: [] },
|
|
7
|
+
{ id: "database_url", description: "", lookFor: [] },
|
|
8
|
+
{ id: "skills", description: "", lookFor: [] },
|
|
9
|
+
{ id: "migrations", description: "", lookFor: [] },
|
|
10
|
+
]);
|
|
11
|
+
const hasDatabaseUrl = inspection.databaseUrl === true;
|
|
12
|
+
const mcpConfigured = inspection.mcpConfigured === true;
|
|
13
|
+
const skillsInstalled = inspection.skillsInstalled === true;
|
|
14
|
+
const migrationTool = inspection.migrationTool ?? null;
|
|
15
|
+
const hasMigrations = migrationTool !== null &&
|
|
16
|
+
migrationTool !== "none" &&
|
|
17
|
+
inspection.migrationDir !== "none";
|
|
18
|
+
const recommendations = [];
|
|
19
|
+
if (!authed) {
|
|
20
|
+
recommendations.push({
|
|
21
|
+
priority: "high",
|
|
22
|
+
message: "Not authenticated with Neon",
|
|
23
|
+
command: `neon init --agent --data '{"step":"auth"}'`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (!hasDatabaseUrl) {
|
|
27
|
+
recommendations.push({
|
|
28
|
+
priority: "high",
|
|
29
|
+
message: "No DATABASE_URL found in .env",
|
|
30
|
+
command: `neon init --agent --data '{"step":"db"}'`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!skillsInstalled) {
|
|
34
|
+
recommendations.push({
|
|
35
|
+
priority: "medium",
|
|
36
|
+
message: "Neon agent skills not detected in this project",
|
|
37
|
+
command: `neon init --agent --data '{"step":"skills","install":true}'`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (migrationTool && !hasMigrations) {
|
|
41
|
+
recommendations.push({
|
|
42
|
+
priority: "medium",
|
|
43
|
+
message: `${migrationTool} detected but no migrations found`,
|
|
44
|
+
command: `neon init --agent --data '{"step":"migrations"}'`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
auth: {
|
|
49
|
+
authenticated: authed,
|
|
50
|
+
},
|
|
51
|
+
tooling: {
|
|
52
|
+
mcpServer: {
|
|
53
|
+
configured: mcpConfigured,
|
|
54
|
+
scope: inspection.mcpScope || null,
|
|
55
|
+
},
|
|
56
|
+
skills: {
|
|
57
|
+
installed: skillsInstalled,
|
|
58
|
+
scope: inspection.skillsScope || null,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
project: {
|
|
62
|
+
databaseUrl: hasDatabaseUrl,
|
|
63
|
+
},
|
|
64
|
+
migrations: {
|
|
65
|
+
tool: migrationTool,
|
|
66
|
+
hasMigrations,
|
|
67
|
+
},
|
|
68
|
+
recommendations,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the Neon org/project from a DATABASE_URL in .env.
|
|
3
|
+
* Parses the endpoint hostname, then queries the Neon API to find
|
|
4
|
+
* which project it belongs to.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { execa } from "execa";
|
|
9
|
+
/**
|
|
10
|
+
* Extracts the Neon endpoint host from .env's DATABASE_URL or PGHOST.
|
|
11
|
+
* Returns the hostname (e.g. "ep-cool-name-12345.us-east-2.aws.neon.tech") or null.
|
|
12
|
+
*/
|
|
13
|
+
function extractNeonHost(cwd) {
|
|
14
|
+
for (const envFile of [".env", ".env.local"]) {
|
|
15
|
+
const envPath = resolve(cwd, envFile);
|
|
16
|
+
if (!existsSync(envPath))
|
|
17
|
+
continue;
|
|
18
|
+
try {
|
|
19
|
+
const content = readFileSync(envPath, "utf-8");
|
|
20
|
+
// Try DATABASE_URL
|
|
21
|
+
const dbUrlMatch = content.match(/^DATABASE_URL=.*@([^/:\s]+\.neon\.tech)/m);
|
|
22
|
+
if (dbUrlMatch)
|
|
23
|
+
return dbUrlMatch[1];
|
|
24
|
+
// Try PGHOST
|
|
25
|
+
const pgHostMatch = content.match(/^PGHOST=([^\s]+\.neon\.tech)/m);
|
|
26
|
+
if (pgHostMatch)
|
|
27
|
+
return pgHostMatch[1];
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the Neon org and project that own the endpoint in DATABASE_URL.
|
|
35
|
+
*
|
|
36
|
+
* Strategy:
|
|
37
|
+
* 1. Extract endpoint hostname from .env
|
|
38
|
+
* 2. List orgs via neonctl
|
|
39
|
+
* 3. For each org, list projects
|
|
40
|
+
* 4. For each project, get connection string and compare hostnames
|
|
41
|
+
* 5. Return the matching org/project
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveNeonContext(cwd) {
|
|
44
|
+
const neonHost = extractNeonHost(cwd);
|
|
45
|
+
if (!neonHost)
|
|
46
|
+
return null;
|
|
47
|
+
// List orgs
|
|
48
|
+
let orgs;
|
|
49
|
+
try {
|
|
50
|
+
const result = await execa("npx", ["-y", "neonctl", "orgs", "list", "--output", "json"], {
|
|
51
|
+
stdio: "pipe",
|
|
52
|
+
timeout: 30000,
|
|
53
|
+
env: { ...process.env, CI: undefined },
|
|
54
|
+
});
|
|
55
|
+
orgs = JSON.parse(result.stdout);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (orgs.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
// Search each org's projects for a matching endpoint
|
|
63
|
+
for (const org of orgs) {
|
|
64
|
+
let projects;
|
|
65
|
+
try {
|
|
66
|
+
const result = await execa("npx", [
|
|
67
|
+
"-y",
|
|
68
|
+
"neonctl",
|
|
69
|
+
"projects",
|
|
70
|
+
"list",
|
|
71
|
+
"--org-id",
|
|
72
|
+
org.id,
|
|
73
|
+
"--output",
|
|
74
|
+
"json",
|
|
75
|
+
], {
|
|
76
|
+
stdio: "pipe",
|
|
77
|
+
timeout: 30000,
|
|
78
|
+
env: { ...process.env, CI: undefined },
|
|
79
|
+
});
|
|
80
|
+
projects = JSON.parse(result.stdout);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const project of projects) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await execa("npx", [
|
|
88
|
+
"-y",
|
|
89
|
+
"neonctl",
|
|
90
|
+
"connection-string",
|
|
91
|
+
"--project-id",
|
|
92
|
+
project.id,
|
|
93
|
+
], {
|
|
94
|
+
stdio: "pipe",
|
|
95
|
+
timeout: 15000,
|
|
96
|
+
env: { ...process.env, CI: undefined },
|
|
97
|
+
});
|
|
98
|
+
const connStr = result.stdout.trim();
|
|
99
|
+
if (connStr.includes(neonHost)) {
|
|
100
|
+
return { orgId: org.id, projectId: project.id };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes a `--data` payload to a phase handler by its `step` field, without
|
|
3
|
+
* shelling out. This is the whole agent-facing surface of `neon init`:
|
|
4
|
+
*
|
|
5
|
+
* neon init --agent --data '{"step":"auth"}'
|
|
6
|
+
* neon init --agent --data '{"step":"db","projectId":"xyz"}'
|
|
7
|
+
*/
|
|
8
|
+
export async function routeDataStep(data, agent) {
|
|
9
|
+
const { step, agentId, ...rest } = data;
|
|
10
|
+
// Allow agentId override from data JSON, fall back to caller-provided agent
|
|
11
|
+
const resolvedAgent = typeof agentId === "string" ? agentId : agent;
|
|
12
|
+
// Agents sometimes nest the actual payload inside a "data" key:
|
|
13
|
+
// {"step":"setup","data":{"mode":"defaults",...}}
|
|
14
|
+
// or as a JSON string:
|
|
15
|
+
// {"step":"setup","data":"{\"mode\":\"defaults\",...}"}
|
|
16
|
+
// Unwrap it so the phase handler gets the right options.
|
|
17
|
+
if (rest.data !== undefined && Object.keys(rest).length === 1) {
|
|
18
|
+
let nested = rest.data;
|
|
19
|
+
if (typeof nested === "string") {
|
|
20
|
+
try {
|
|
21
|
+
nested = JSON.parse(nested);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// leave as-is
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (typeof nested === "object" && nested !== null) {
|
|
28
|
+
Object.assign(rest, nested);
|
|
29
|
+
delete rest.data;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
switch (step) {
|
|
33
|
+
case "auth": {
|
|
34
|
+
const { handleAuthPhase } = await import("./phases/auth.js");
|
|
35
|
+
return handleAuthPhase({
|
|
36
|
+
agent: resolvedAgent,
|
|
37
|
+
...rest,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
case "db": {
|
|
41
|
+
const { handleDbPhase } = await import("./phases/db.js");
|
|
42
|
+
return handleDbPhase({
|
|
43
|
+
agent: resolvedAgent,
|
|
44
|
+
...rest,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
case "setup": {
|
|
48
|
+
const { handleSetupPhase } = await import("./phases/setup.js");
|
|
49
|
+
return handleSetupPhase({
|
|
50
|
+
agent: resolvedAgent,
|
|
51
|
+
...rest,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
case "getting-started": {
|
|
55
|
+
const { handleGettingStartedPhase } = await import("./phases/getting_started.js");
|
|
56
|
+
return handleGettingStartedPhase({
|
|
57
|
+
agent: resolvedAgent,
|
|
58
|
+
...rest,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
case "mcp": {
|
|
62
|
+
const { handleMcpPhase } = await import("./phases/mcp.js");
|
|
63
|
+
return handleMcpPhase({
|
|
64
|
+
agent: resolvedAgent,
|
|
65
|
+
...rest,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
case "skills": {
|
|
69
|
+
const { handleSkillsPhase } = await import("./phases/skills.js");
|
|
70
|
+
return handleSkillsPhase({
|
|
71
|
+
agent: resolvedAgent,
|
|
72
|
+
...rest,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
case "migrations": {
|
|
76
|
+
const { handleMigrationsPhase } = await import("./phases/migrations.js");
|
|
77
|
+
return handleMigrationsPhase({
|
|
78
|
+
agent: resolvedAgent,
|
|
79
|
+
...rest,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
case "neon-auth": {
|
|
83
|
+
const { handleNeonAuthPhase } = await import("./phases/neon_auth.js");
|
|
84
|
+
return handleNeonAuthPhase({
|
|
85
|
+
agent: resolvedAgent,
|
|
86
|
+
...rest,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
case "status": {
|
|
90
|
+
const { handleStatusPhase } = await import("./phases/status.js");
|
|
91
|
+
return handleStatusPhase({ agent: resolvedAgent });
|
|
92
|
+
}
|
|
93
|
+
case "finalize": {
|
|
94
|
+
const { handleCleanup } = await import("./phases/cleanup.js");
|
|
95
|
+
return handleCleanup();
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unknown step: "${step}". Valid steps: auth, db, setup, getting-started, mcp, skills, migrations, neon-auth, status, finalize`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { log, spinner } from "@clack/prompts";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { dim } from "yoctocolors";
|
|
6
|
+
import { getSkillsAgentName as getSkillsAgentNameFromId } from "./agents.js";
|
|
7
|
+
/**
|
|
8
|
+
* Ensures the `skills` CLI is globally installed so npx doesn't need
|
|
9
|
+
* to download it (which can fail behind corporate proxies / sandboxes).
|
|
10
|
+
*/
|
|
11
|
+
async function ensureSkillsCli() {
|
|
12
|
+
try {
|
|
13
|
+
await execa("skills", ["--version"], { stdio: "pipe", timeout: 5000 });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Not installed — install it globally
|
|
17
|
+
try {
|
|
18
|
+
await execa("npm", ["install", "-g", "skills"], {
|
|
19
|
+
stdio: "pipe",
|
|
20
|
+
timeout: 60000,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Best effort — npx will fall back to downloading
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Base skills installed for all invocations */
|
|
29
|
+
const BASE_SKILLS = ["neon", "neon-postgres"];
|
|
30
|
+
/** Additional skills installed for preview (non-bootstrap) invocations */
|
|
31
|
+
const PREVIEW_SKILLS = [
|
|
32
|
+
"neon-object-storage",
|
|
33
|
+
"neon-functions",
|
|
34
|
+
"neon-ai-gateway",
|
|
35
|
+
];
|
|
36
|
+
/** Returns the skill list based on whether preview mode is active */
|
|
37
|
+
export function getSkillList(preview) {
|
|
38
|
+
return preview ? [...BASE_SKILLS, ...PREVIEW_SKILLS] : BASE_SKILLS;
|
|
39
|
+
}
|
|
40
|
+
const SKILL_BASE_URL = "https://neon.com/docs/ai/skills/neon-postgres/references";
|
|
41
|
+
export const SKILL_REFERENCE_URLS = {
|
|
42
|
+
gettingStarted: `${SKILL_BASE_URL}/getting-started.md`,
|
|
43
|
+
connectionMethods: `${SKILL_BASE_URL}/connection-methods.md`,
|
|
44
|
+
neonAuth: `${SKILL_BASE_URL}/neon-auth.md`,
|
|
45
|
+
serverlessDriver: `${SKILL_BASE_URL}/neon-serverless.md`,
|
|
46
|
+
neonCli: `${SKILL_BASE_URL}/neon-cli.md`,
|
|
47
|
+
devtools: `${SKILL_BASE_URL}/devtools.md`,
|
|
48
|
+
branching: `${SKILL_BASE_URL}/branching.md`,
|
|
49
|
+
neonJs: `${SKILL_BASE_URL}/neon-js.md`,
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Maps Editor display names to the skills CLI agent name.
|
|
53
|
+
*/
|
|
54
|
+
function editorToSkillsAgent(editor) {
|
|
55
|
+
switch (editor) {
|
|
56
|
+
case "Cursor":
|
|
57
|
+
return "cursor";
|
|
58
|
+
case "VS Code":
|
|
59
|
+
case "GitHub Copilot CLI":
|
|
60
|
+
return "github-copilot";
|
|
61
|
+
case "Claude CLI":
|
|
62
|
+
return "claude-code";
|
|
63
|
+
case "Codex":
|
|
64
|
+
return "codex";
|
|
65
|
+
case "OpenCode":
|
|
66
|
+
return "opencode";
|
|
67
|
+
case "Antigravity":
|
|
68
|
+
return "antigravity";
|
|
69
|
+
case "Cline":
|
|
70
|
+
case "Cline CLI":
|
|
71
|
+
return "cline";
|
|
72
|
+
case "Gemini CLI":
|
|
73
|
+
return "gemini-cli";
|
|
74
|
+
case "Goose":
|
|
75
|
+
return "goose";
|
|
76
|
+
case "Claude Desktop":
|
|
77
|
+
return "claude-code";
|
|
78
|
+
case "MCPorter":
|
|
79
|
+
return "mcporter";
|
|
80
|
+
case "Zed":
|
|
81
|
+
return "zed";
|
|
82
|
+
default:
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Installs Neon agent skills using Vercel's skills CLI.
|
|
88
|
+
*/
|
|
89
|
+
export async function installAgentSkills(selectedEditors, options) {
|
|
90
|
+
const quiet = options?.json === true;
|
|
91
|
+
const editorsWithSkills = selectedEditors.filter((e) => editorToSkillsAgent(e) !== "");
|
|
92
|
+
if (editorsWithSkills.length === 0) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const skillsSpinner = quiet ? null : spinner();
|
|
96
|
+
skillsSpinner?.start("Installing agent skills for Neon in this project...");
|
|
97
|
+
let anyFailed = false;
|
|
98
|
+
await ensureSkillsCli();
|
|
99
|
+
const skills = getSkillList(options?.preview);
|
|
100
|
+
for (const editor of editorsWithSkills) {
|
|
101
|
+
const agentName = editorToSkillsAgent(editor);
|
|
102
|
+
// Install one skill at a time — the skills CLI has a bug with multiple
|
|
103
|
+
// --skill flags where it creates directories but doesn't copy all SKILL.md files.
|
|
104
|
+
for (const skill of skills) {
|
|
105
|
+
try {
|
|
106
|
+
await execa("skills", [
|
|
107
|
+
"add",
|
|
108
|
+
"neondatabase/agent-skills",
|
|
109
|
+
"--skill",
|
|
110
|
+
skill,
|
|
111
|
+
"--agent",
|
|
112
|
+
agentName,
|
|
113
|
+
...(options?.scope === "global" ? ["-g"] : []),
|
|
114
|
+
"-y",
|
|
115
|
+
], {
|
|
116
|
+
stdio: "pipe",
|
|
117
|
+
timeout: 120000,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!quiet)
|
|
122
|
+
log.error(`Failed to install skill ${skill} for ${editor}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
123
|
+
anyFailed = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (anyFailed) {
|
|
128
|
+
skillsSpinner?.stop("Agent skills installation for this project completed with errors");
|
|
129
|
+
if (!quiet)
|
|
130
|
+
log.info("You can manually install skills by running: npx skills add neondatabase/agent-skills --skill neon --skill neon-postgres");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
skillsSpinner?.stop(dim("Agent skills installed ✓"));
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Evergreen skills: ensure skills are up to date (at most once per 12 hours)
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
const SKILLS_FRESHNESS_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
140
|
+
/**
|
|
141
|
+
* Agent skills directory paths by agent, keyed by scope.
|
|
142
|
+
* These are the directories that `skills add` writes to.
|
|
143
|
+
*/
|
|
144
|
+
const GLOBAL_SKILLS_DIRS = (() => {
|
|
145
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
146
|
+
// .agents/skills is the generic directory used by multiple agents
|
|
147
|
+
const agentsDir = resolve(home, ".agents", "skills");
|
|
148
|
+
return {
|
|
149
|
+
cursor: [resolve(home, ".cursor", "skills"), agentsDir],
|
|
150
|
+
"claude-code": [resolve(home, ".claude", "skills"), agentsDir],
|
|
151
|
+
"github-copilot": [resolve(home, ".vscode", "skills"), agentsDir],
|
|
152
|
+
codex: [resolve(home, ".codex", "skills"), agentsDir],
|
|
153
|
+
cline: [resolve(home, ".cline", "skills"), agentsDir],
|
|
154
|
+
};
|
|
155
|
+
})();
|
|
156
|
+
/**
|
|
157
|
+
* Checks whether skills were recently updated (within the freshness window).
|
|
158
|
+
* Checks both project-level (skills-lock.json mtime) and global (skills dir mtime).
|
|
159
|
+
*/
|
|
160
|
+
function skillsAreFresh(agent, requiredSkills) {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const cwd = process.cwd();
|
|
163
|
+
const projectSkillDirs = [".agents", ".cursor", ".claude"];
|
|
164
|
+
// Check project-level: ALL required skills must exist on disk
|
|
165
|
+
// and skills-lock.json must be recent
|
|
166
|
+
const lockPath = resolve(cwd, "skills-lock.json");
|
|
167
|
+
if (existsSync(lockPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const mtime = statSync(lockPath).mtimeMs;
|
|
170
|
+
if (now - mtime < SKILLS_FRESHNESS_MS) {
|
|
171
|
+
const allExist = requiredSkills.every((skill) => projectSkillDirs.some((dir) => existsSync(resolve(cwd, dir, "skills", skill, "SKILL.md"))));
|
|
172
|
+
if (allExist)
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
}
|
|
178
|
+
// Check global: ALL required skills must exist in agent-specific dirs
|
|
179
|
+
const agentName = getSkillsAgentNameFromId(agent);
|
|
180
|
+
const globalDirs = GLOBAL_SKILLS_DIRS[agentName] ?? [];
|
|
181
|
+
for (const dir of globalDirs) {
|
|
182
|
+
const allExist = requiredSkills.every((skill) => existsSync(resolve(dir, skill, "SKILL.md")));
|
|
183
|
+
if (allExist) {
|
|
184
|
+
// Check freshness of any one skill file
|
|
185
|
+
try {
|
|
186
|
+
const mtime = statSync(resolve(dir, requiredSkills[0], "SKILL.md")).mtimeMs;
|
|
187
|
+
if (now - mtime < SKILLS_FRESHNESS_MS)
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Ensures Neon agent skills are up to date. Runs `skills add` if the skills
|
|
197
|
+
* haven't been updated within the freshness window (12 hours).
|
|
198
|
+
*
|
|
199
|
+
* Designed to be called from any phase handler — cheap to call repeatedly
|
|
200
|
+
* since it's a no-op when skills are fresh.
|
|
201
|
+
*/
|
|
202
|
+
export async function ensureSkillsUpToDate(agent, scope, preview) {
|
|
203
|
+
const resolvedAgent = agent || "cursor";
|
|
204
|
+
const skills = getSkillList(preview);
|
|
205
|
+
if (skillsAreFresh(resolvedAgent, skills))
|
|
206
|
+
return true;
|
|
207
|
+
await ensureSkillsCli();
|
|
208
|
+
const agentName = getSkillsAgentNameFromId(resolvedAgent);
|
|
209
|
+
let allOk = true;
|
|
210
|
+
// Only install skills that don't already have SKILL.md on disk.
|
|
211
|
+
// Re-installing existing skills can trigger sandbox permission prompts.
|
|
212
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
213
|
+
const cwd = process.cwd();
|
|
214
|
+
const checkDirs = scope === "global"
|
|
215
|
+
? [
|
|
216
|
+
resolve(home, ".cursor", "skills"),
|
|
217
|
+
resolve(home, ".claude", "skills"),
|
|
218
|
+
resolve(home, ".agents", "skills"),
|
|
219
|
+
]
|
|
220
|
+
: [
|
|
221
|
+
resolve(cwd, ".cursor", "skills"),
|
|
222
|
+
resolve(cwd, ".claude", "skills"),
|
|
223
|
+
resolve(cwd, ".agents", "skills"),
|
|
224
|
+
];
|
|
225
|
+
const missingSkills = skills.filter((skill) => !checkDirs.some((dir) => existsSync(resolve(dir, skill, "SKILL.md"))));
|
|
226
|
+
if (missingSkills.length === 0)
|
|
227
|
+
return true;
|
|
228
|
+
// Install one skill at a time — the skills CLI has a bug with multiple
|
|
229
|
+
// --skill flags where it creates directories but doesn't copy all SKILL.md files.
|
|
230
|
+
for (const skill of missingSkills) {
|
|
231
|
+
try {
|
|
232
|
+
await execa("skills", [
|
|
233
|
+
"add",
|
|
234
|
+
"neondatabase/agent-skills",
|
|
235
|
+
"--skill",
|
|
236
|
+
skill,
|
|
237
|
+
"--agent",
|
|
238
|
+
agentName,
|
|
239
|
+
...(scope === "global" ? ["-g"] : []),
|
|
240
|
+
"-y",
|
|
241
|
+
], { stdio: "pipe", timeout: 120000 });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
allOk = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return allOk;
|
|
248
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|